I'm in a similar boat (using GraphQL), and I've elected to stay as far away from cast_assoc
as possible. This is less because of the "create" scenario and more because of the "update" scenario.
Looking at the documentation for cast_assoc
, you'll see it says...
- If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
- If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)
Scenario 1 is your standard create, meaning your data needs to look similar to your nested input above. (it actually needs to be a list of maps for the email key.
Let's say a person adds a second email (you indicated this is a one-to-many above). If your input looks like:
%{
id: 12345,
username: "test",
emails: [
%{email: "test_2@123.com"}
}
}
...this triggers both Scenario 1 (new param, no ID) and Scenario 4 (child with ID isn't given) effectively removing all prior emails. Which means your update params actually need to look like:
%{
id: 12345,
username: "test",
emails: [
%{id: 1, email: "test@123.com"},
%{email: "test_2@123.com}
]
}
...which, to me, means queueing up a lot of extra data within requests. For something like emails -- of which a user is unlikely to have few -- the cost is low. For a more-abundantly-created association, a pain.
Rather than always putting cast_assoc
into your User.changeset
, one option would be creating a specific changeset for registration, that only uses cast once:
defmodule MyApp.UserRegistration do
[...schema, regular changeset...]
def registration_changeset(params) do
%MyApp.User{}
|> MyApp.Repo.preload(:emails)
|> changeset(params)
|> cast_assoc(:emails, required: true, with: &MyApp.Email.changeset(&1, &2))
end
end
You'll still need to provide a nested emails
field in your input, which is maybe a bummer, but at least then you're not polluting your normal User changeset with cast_assoc.
One last thought: Rather than have your client care about nesting, you could do that in your registration-specific resolver function?