acrolink

acrolink

Where to write custom changeset validation functions which rely on current database values / records?

Here is an example of a custom validation:

@our_url "https://our-bucket.s3.amazon.com"
def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, @required_fields)
  |> validate_required(@required_fields)
  |> validate_from_s3_bucket(:url)
end
def validate_from_s3_bucket(changeset, field, options \\ []) do
  validate_change(changeset, field, fn _, url ->
    case String.starts_with?(url, @our_url) do
      true -> []
      false -> [{field, options[:message] || "Unexpected URL"}]
    end
  end)
end

This can be written in the schema definition file. But where to write custom changeset validation functions which test database tables (of other models) for certain values and based on that data flag the transaction as valid or not? I guess making Ecto queries for B inside schema definition of A is not the correct way to do it. Any ideas? Thank you.

Example / Clarification

I have this in my schema definition:

  def changeset(record, attrs) do
    record
    |> cast(attrs, [:issued_at, :due_for_return, :returned_at, :fee, :fee_paid_at, :book_id])
    |> validate_required([])
    |> validate_book(:book_id)
  end


  defp validate_book(changeset, field) do
    case changeset.valid? do
      true ->
        book_id = get_field(changeset, field)
        case  Repo.get(Mango.Books.Book, book_id) do
          nil -> add_error(changeset, :book_id, "Book not found..")
          book -> {:ok, book}
            case book.status do
              :available -> changeset
              _ -> add_error(changeset, :book_id, "Book is already out..")
            end
        end
      _ ->
        changeset
    end
  end

As you can see I am doing some Repo query validation against some other model (books). My question is this the place do this or should I move all of this outside of my schema definition?

Most Liked

idi527

idi527

I usually avoid making request to a database inside changesets. Instead, I use more explicit multis or transactions. So your second example I would write as:

  def changeset(record, attrs) do
    cast(record, attrs, [:issued_at, :due_for_return, :returned_at, :fee, :fee_paid_at])
  end

  defp valid_book_multi(multi, book_id) do
    alias Ecto.Multi
     
    Multi.run(multi, :book, fn _changes -> 
       case Book.get(book_id) do
          %Book{status: :available} = available_book -> {:ok, available_book}
          %Book{status: status} = not_available_book -> {:error, Book.error_msg(:status, status)}
          nil -> {:error, :not_found}
       end
    end)
  end

  @spec create_record(%{(String.t | atom) => term}, book_id: pos_integer) :: {:ok, %Record{}} | {:error, Ecto.Changeset.t}
  def create_record(attrs, book_id: book_id) do
    alias Ecto.Multi

   record_changeset = changeset(%Record{book_id: book_id}, attrs)

    Multi.new()
    |> valid_book_multi(book_id)
    |> Multi.insert(:record, record_changeset)
    |> Repo.transaction()
    |> case do
      {:ok, %{record: record}} -> {:ok, record}
      {:error, :book, reason, _changes} -> # collect errors into record changesets
    end
  end
jeremyjh

jeremyjh

Changeset validation functions should be limited to pure functions that use the data available to them in the Changeset and arguments you pass. They are not “models”. Business rules that require data access, messaging etc should be implemented in another module - if you are using Phoenix it would be a context module - but regardless its just a module with functions.

Laetitia

Laetitia

What about using Ecto.Changeset.prepare_changes ?

Provides a function to run before emitting changes to the repository.

Such function receives the changeset and must return a changeset, allowing developers to do final adjustments to the changeset or to issue data consistency commands.

The given function is guaranteed to run inside the same transaction as the changeset operation for databases that do support transactions.

I will run some validation and returns an invalid changeset if it failed.

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement