Say i have a User who has many types of Address and we want to have at least one Address to consider the User as valid.
For User changeset
def changeset(user, attrs) do
user
|> cast(attrs, [:name])
|> cast_assoc(:addresses, required: true)
|> validate_required([:name])
end
I believe i have to use an Ecto.Multi to first insert the user and then insert_all addresses after. However the changeset correctly disallows this because we don’t have at least one address yet. A chicken and egg problem it seems. I would hate to loosen the validation required: true as it would allow “illegal” users to be created.
I would expect this to work as-is - pass the output of User.changeset above to Repo.insert and Ecto should handle inserting the child Address records as part of the same transaction.
cast_assoc/3 will take care of adding the user_id for you if you’re creating the address as a child of a new user.
If the parameter does not contain an ID, the parameter data will be passed to MyApp.Address.changeset/2 with a new struct and become an insert operation
Your address changeset function is requiring the user_id, which would make sense if you were adding the address separately after creating the user, but in this case it’s causing the error at the changeset stage because the id will only be added when you run the Repo operation.
If you want to keep the behaviour of requiring a user_id for direct operations on address, create a separate changeset function for child operations and call it explicitly from the user changeset.
# in address.ex
def changeset(address, attrs) do
cast(attrs, [:user_id, :street])
|> other_checks()
end
def child_changeset(address, attrs) do
cast(attrs, [:street])
|> other_checks()
end
def other_checks(changeset) do
# general validation
end
# in user. ex
def changeset(user, attrs) do
user
|> cast(attrs, [:name])
|> cast_assoc(:addresses, required: true, with: &MyApp.Address.child_changeset/2)
|> validate_required([:name])
end
One of the problems with this is when you want to make user changes unrelated to addresses then you’d still have to preload them. Obviously the solution is to create other, specific user changesets for the different situations and use them accordingly. For example rename this one changeset_with_addresses and use it to enforce this business logic only for creating new users or bulk-updating addresses for them.