acrolink

acrolink

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?

Showing Posts 1 to 4

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.

pierreabreup

pierreabreup

@Laetitia Changeset.prepare_changes works exactly you said, but I guess the @acrolink point is more about the right way to design this code for validation. There is no strict boundary about where the code must be written (in the same module or another module).

In my projects, I always prefer to place business rules inside another module. For me and my team, it sounds an organized way to split responsibilities, works very well to us.

Of course, this strategy comes a side effect: you will have data validation in two different places for the same entity (post, user, comment, question, etc). You have to feel comfortable with it.

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews