For people looking for more validation, the code below works pretty well for me.
Using this code helps when you want to deal with a new Map
or an existing Struct
or an existing id was given for the association…
def put_or_cast_or_constraint_assoc(changeset, name) do
{:assoc, %{owner_key: key, related: type}} = changeset.types[name] |> IO.inspect
if changeset.changes[key] do
assoc_constraint(changeset, name)
else
case val = changeset.params[name |> Atom.to_string] do
%correct_type{} when correct_type == type ->
put_assoc(changeset, name, val)
%wrong_type{} ->
add_error(changeset, name, "Wrong struct given to #{key}", given: wrong_type, wanted: type)
%{} ->
cast_assoc(changeset, name, required: true)
_ ->
add_error(changeset, name, "No valid #{name} nor #{key}")
end
end
end
2 Likes
Not sure why this hasn’t been mentioned before (maybe this is a new addition in Ecto), but this seems to be the answer to this issue https://hexdocs.pm/ecto/Ecto.Changeset.html#foreign_key_constraint/3
I had the same problem recently and this thread was very helpful.
It seems that foreign_key_constraint
will only work if you provide the foreign key field while cast_assoc
will work if you provide a map - hence @wojtekmach’s solution to validate both cases.
For future reference, a simple use-case to illustrate how it can be done individually might be…
schema "posts" do
field :title, :string
has_many :comments, Comment
end
schema "comments" do
field :text, :string
belongs_to :post, Post
end
Inserting a comment referencing an existing post:
def changeset(comment, attrs) do
comment
|> cast(attrs, [:text])
|> assoc_constraint(:post) # or foreign_key_constraint(:post_id)
|> validate_required([:text])
end
create_comment(%{text: "Nice post!", post_id: 1 })
Inserting a comment with a post all at once:
def changeset(comment, attrs) do
comment
|> cast(attrs, [:text])
|> cast_assoc(:post, required: true)
|> validate_required([:text])
end
create_comment(%{text: "Nice post!", post: %{title: "How to..." } })
1 Like
This is apples and oranges. foreign_key_constraint
just provides a changeset error message when a constraint in the database is violated - which happens if you provide a foreign key value that is not defined in the referenced table. It provides nothing in terms of casting associations, or ensuring that associations are defined.
1 Like
Inserting a comment with a post all at once:
def changeset(comment, attrs) do
comment
|> cast(attrs, [:text])
|> cast_assoc(:post, required: true)
|> validate_required([:text])
end
If I may ask (being quite new to phoenix), using cast_assoc makes the test suuuper slow (about 500ms, compared to some milliseconds without inserting this extra record). We’re inserting two records here instead of one; but still; am i right to doubt such duration is normal ?
I’ve searched a lot and it still doesn’t seem like there’s a good way to handle the case where you’re creating a child record for a has_many/belongs_to by just passing in the foreign key as an :assoc_id param?
I can’t seem to find anything that actually validates if assoc_id is nil, apart from validates_required…but the docs explicitly say don’t do that. foreign_key_constraint
doesn’t do anything if the key is nil/null. cast_assoc
expects that you’re modifying/creating the related record, rather just referencing it.
Is the only recommended way to go via the parent?