I have a question about how to handle Ecto changesets that include associations. I’ve worked through several options but as far as I can tell they all work counter to the way changeset/2 works for standard fields or seem dangerous.
tl;dr
Is there a standard way to put_assoc when a association change is present in the changeset/2 params that also works if the associated item isn’t included (and so doesn’t need to be modified)?
There are several options I’ve run across and they all seem to have a downside or inconsistency:
Just pass the associated item’s id like a regular field.
This works but feels messy and opens the possibility of bypassing something important that Ecto would do when a new association is made.
params = %{mypost | user_id = "a nice uuid"}
Create the original struct with build_assoc
def create_settings(%MyApp.Users.User{} = user, attrs \\ %{}) do
Ecto.build_assoc(user, :settings)
|> Settings.changeset(attrs)
|> Repo.insert()
end
Works nicely for creating the item. However, the associated id field isn’t included in the list of changes in the changeset. This isn’t a dealbreaker but it is confusing and could open the door for issues.
The bigger challenge is that there is no way to change the associated item using changeset/2. To make a change you would have to use something else that would work but now we have a changeset/2 function that doesn’t actually handle all the changes for an item.
cast_assoc in changeset/2
This works but will try to modify the associated items in a way that isn’t desired in many use cases - when a user owns an item for example.
put_assoc in changeset/2
def changeset(settings, attrs) do
settings
|> cast(attrs, [:timezone])
|> put_assoc(:user, attrs.user)
|> validate_required([:timezone])
end
Again, this works great but we’ve now accidentally required :user in our changesets. If you don’t put a user in params it will throw a messy error. This breaks the standard way changeset/2 is used. Usually, requirements are enforced by a requirement function of some sort and fields are optional unless required.
Is there a good way to handle this?
I don’t see why associations shouldn’t be handled in a way that is consistent with other changeset data while still using unique assoc functions to make sure any associated operations happen.
Is there a standard way to put_assoc when a change is present in the params that also works if the associated item isn’t included (and so doesn’t need to be modified)?
I’m tempted to write a “patch_assoc” that includes the change if present but I want to be as idiomatic as possible and not go down any unnecessary rabbit holes.