dotdotdotPaul
Ecto: Validating belongs_to association is not nil?
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.
Marked As Solved
wojtekmach
it’s pretty hacky, but perhaps this would work for you?
defmodule Rating do
# ...
def changeset(rating, params \\ %{}) do
cast(rating, params, ~w(rating place_id))
|> validate_required(~w(rating)a)
|> cast_or_constraint_assoc(:place)
end
defp cast_or_constraint_assoc(changeset, name) do
{:assoc, %{owner_key: key}} = changeset.types[name]
if changeset.changes[key] do
assoc_constraint(changeset, name)
else
cast_assoc(changeset, name, required: true)
end
end
end
I’d stick to exposing two different changeset functions though (and probably have a 3rd private changeset function that has common stuff)
Also Liked
jeremyjh
It does do that - if it is passed in as place_id it is validated with assoc_constraint; otherwise it is checked with cast_assoc required: true.
However the code as listed is really not complete because it does not handle updates to the record that do not include a change to the place; e.g. record is loaded from the db, place_id is set, but because place_id is not changed cast_assoc fails on it. In my project I’m using a slightly modified version that will pass the changeset if it contains the key (place_id) already:
def cast_or_constraint_assoc(changeset, name) do
{:assoc, %{owner_key: key}} = changeset.types[name]
#assoc id was directly set? confirm its valid
if changeset.changes[key] do
assoc_constraint(changeset, name)
else
#assoc key is already present, and not changed? do nothing
if Map.get(changeset.data, key) do
changeset
else
#we need to insert a new assoc (or errors)
cast_assoc(changeset, name, required: true)
end
end
end
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
josevalim
I understsand now, thank you. @wojtekmach sounds like a good way to go about this.








