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.