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.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
dsissitka
I think adding
null: falsemight be what you’re looking for.dotdotdotPaul
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
Adding
null: falseadded a not-null constraint to theplace_idcolumn. That won’t be validated until you hit the database, until youRepo.insert.See Ecto.Changeset — Ecto v3.14.0.
Is
assoc_constraint(:place)still there?dotdotdotPaul
Yeah, I didn’t change the changeset stuff, just added the null constraint to the database.
wfgilman
I think you want the following given your schema:
For a
:belongs_toassociation, useassoc_constraint/3for validation. It let’s Ecto check whether the Place to which the rating belongs exists.cast_assoc/3would go on the Place schema to check Rating. Don’t usevalidate_required/3to 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
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
)
wfgilman
Ah, I see now. Two things:
First, I don’t think
cast_assoc/3is 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/3you need to remove the association from the database and then try to insert your record.For example (
insert/1andbuild/2are ExMachina functions.)Because
assoc_constraint/3checks the DB, you must try to insert to generate the changeset error message.dotdotdotPaul
cast_associs 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_constraintdoesn’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 andcast_assoc…wfgilman
What if you just slip
:place_idintovalidate_required/3? Then you can be assured that you’ll get a nice error message if either:place_idis null, or if the:place_idrecord has been deleted before Rating could be inserted (viaassoc_constraint/3).dotdotdotPaul
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)…