dotdotdotPaul

dotdotdotPaul

Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’m spoiled by ActiveRecord, where I can just set the association to either a persisted or unpersisted object, and write a validation that ensures the child is “there”.

My example is a table/model, let’s call it Rating, and it belongs_to a Place (ie. field is place_id in the ratings table).

I figure there are three ways the set this association: One, we specify the place_id in the changeset directly. Two, we put_assoc an existing Place struct after the changeset options. Three, we have a Map with the Place parameters in the changeset under the :place key (and then use “cast_assoc”). So this is what I’ve got:

defmodule Rating do
  use MyApp.Web, :model
  schema "ratings" do
    field :rating, :integer
    belongs_to :place, MyApp.Place
    timestamps()
  end
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:rating, :place_id])
    |> cast_assoc(:place)
    |> assoc_constraint(:place)
    |> validate_required([:rating])
  end

In a test, I have this:

changeset = Rating.changeset(%Rating{}, { rating: 5 })
refute changeset.valid?, "Expected error on place constraint"  # 1
assert {:error, problem } = Repo.insert(changeset)  # 2

The refute fails, because no error is generated. I found some notes that “valid?” may not actually do any of the database queries necessary to ensure the parent object actually exists, so I thought maybe that would happen during the actual insert(), so I commented that line out and asserted on the next. However, that fails, too, and I can see that I get back :ok as a status, and a record saved with place_id nil.

If I validate place_id is required, then I can’t make this work where I either put_assoc an existing record, or pass in a Map of parameters.

Is it not possible to set up a singular changeset function to validate the belongs_to reference in all three ways it could be passed in? If assoc_constraint isn’t checking for non-nil associations, what do I need to make that work?

…Paul

PS> The migration has “add :place_id, references(:places, on_delete: delete_all)” if that matters.

Showing Posts 1 to 10

dsissitka

dsissitka

I think adding null: false might be what you’re looking for.

dotdotdotPaul

dotdotdotPaul OP

I tried this just now (good idea, if I know I want that constraint in the database from the get-go), but the valid? call still doesn’t return a failure, and if I go for the insert, I don’t get an {:error, changeset} response, I get an exception for not_null_violation… I guess that’s an improvement, so I don’t get bad records, but I’d rather be able to handle the error like any other validation…

dsissitka

dsissitka

Adding null: false added a not-null constraint to the place_id column. That won’t be validated until you hit the database, until you Repo.insert.

See Ecto.Changeset — Ecto v3.14.0.

Is assoc_constraint(:place) still there?

dotdotdotPaul

dotdotdotPaul OP

Yeah, I didn’t change the changeset stuff, just added the null constraint to the database.

wfgilman

wfgilman

I think you want the following given your schema:

def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:rating, :place_id])
  |> validate_required([:rating]
  |> assoc_constraint(:place)
end

For a :belongs_to association, use assoc_constraint/3 for validation. It let’s Ecto check whether the Place to which the rating belongs exists. cast_assoc/3 would go on the Place schema to check Rating. Don’t use validate_required/3 to check association constraints (as instructed here).

I struggled with the different changeset validations in the same context. I outlined my findings here: Ecto Association vs Foreign Key Constraints - #2 by wfgilman

dotdotdotPaul

dotdotdotPaul OP

I have assoc_constraint there. It doesn’t seem to do anything if the changeset doesn’t reference the ID or the association at all, and if I put the “not null” constraint in the database, I get an exception rather than a changeset with errors set. (BTW, your findings were where I started :wink: )

wfgilman

wfgilman

Ah, I see now. Two things:

First, I don’t think cast_assoc/3 is doing anything in your changeset and can be left out.

Second, I think you’re not seeing the test results you expect because of your assertions. To get a proper error message from assoc_constraint/3 you need to remove the association from the database and then try to insert your record.

For example (insert/1 and build/2 are ExMachina functions.)

test "changeset is invalid if place is missing" do
  place = insert(:place)
  rating_params = build(:rating, place: place)

  Repo.delete!(place)

  changeset = Rating.changeset(%Rating{}, rating_params)

  assert {:error, changeset} = Repo.insert(changeset)
  assert changeset.errors[:place] == {"does not exist", []}
end

Because assoc_constraint/3 checks the DB, you must try to insert to generate the changeset error message.

dotdotdotPaul

dotdotdotPaul OP

cast_assoc is there “in case” the association is passed as a Map in the params.

Your example is using a record with an existing ID (even though the ID is missing when you do the insert). So the place_id gets set to an integer that doesn’t exist in the database, and the insert then fails to find it – but if the place_id is nil, assoc_constraint doesn’t bother looking anything up. So if the params passed in don’t make reference to place_id, and doesn’t have the Map to use to create the dependency automagically, there is nothing that enforces that there is something being assigned to the association…

This may be an unfortunate chicken-and-egg problem, and maybe I just need two changesets, one where the Place is already created, and either passed as a place_id or added as a put_assoc, and another that relies on the params hash and cast_assoc

wfgilman

wfgilman

What if you just slip :place_id into validate_required/3? Then you can be assured that you’ll get a nice error message if either :place_id is null, or if the :place_id record has been deleted before Rating could be inserted (via assoc_constraint/3).

dotdotdotPaul

dotdotdotPaul OP

I can put it into validate_required, but then valid? fails if you’re casting the association as part of the params Map (because it won’t have done any database actions yet, the ID won’t get set). If I have two changeset functions, one for “casting the association” and one for “existing association”, I could validate the ID is set in the latter…

But I was kinda hoping to have a single changeset function that had all the validations there (for cases where there are a lot more fields with a lot more validations)…

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews