4
votes

I'm getting rather confused about Ecto when it comes to adding a new association to an existing model. Let's say I have some discussion board where users can like a post, and I have the following models:

  schema "discussion_items" do
    many_to_many :likes, XXX.Tag, join_through: "discussion_items_likes"
    timestamps()
  end

  schema "likes" do
    belongs_to :user, XXX.User
    timestamps()
  end

Now lets say I have an existing DiscussionItem and I want to add a like association to this, here's what Im trying (simplified):

user = Repo.one(User)
discussion_item = Repo.one(from d in DiscussionItem, preload [:likes])

like_changeset = Like.changeset(%Like{})
                     |> Ecto.Changeset.put_assoc(:user,   user)
changeset = Ecto.Changeset.change(discussion_item) 
                |>  Ecto.Changeset.put_assoc(:likes, [like_changeset])

Ecto.update!(changeset)

What I am finding is that this will create the Like and the association correctly the first time but then complain afterwards that I need additional information in the form of some id:

you are attempting to change relation :likes of XXX.DiscussionItem, but there is missing data

If you are attempting to update an existing entry, please make sure you include the entry primary key (ID) alongside the data.

If you have a relationship with many children, at least the same N children must be given on update. By default it is not possible to orphan embed nor associated records, attempting to do so results in this error message.

So here's the things that confuse me (there are a few):

  1. This error message seems to suggest that in order to update one associated item, I need to provide a change set with ALL items including the one that changed. This seems nuts, so I'm guessing I'm reading this wrong.
  2. I don't want to update the likes association -- I want to add one. Is put_assoc the wrong choice here?
  3. Do I really need to preload the likes just to insert an item? Even on update I don't want to load the whole collection of associations to change one.
  4. Do I need to load User and Dicussion_item to add a like here?
1
Seems that I found the answer is that put_assoc requires all existing items plus the one I want to add ran through change sets themselves. This seems highly inefficient so hopefully there is a better way. - Owen
Why are you using many_to_many for likes and discussion items? Shouldn't a like count for only a single discussion item at a time? - Justin Wood
I have several things I want to like, discussion items is just one of them so there will be several join tables. It's really beside the point anyway, what I really want to know is how to insert a new item in a many to many relationship without loading the world, the example above is not so important - Owen
this is probably the best thing to do. It is a little more verbose, but I think it is worth it for how simple everything becomes. - Justin Wood
Cheers Justin, shame I do have to do this but thats much better that the alternative. - Owen

1 Answers

5
votes

This error message seems to suggest that in order to update one associated item, I need to provide a change set with ALL items including the one that changed. This seems nuts, so I'm guessing I'm reading this wrong.

Yes you need to preload. From the put_assoc docs:

This function requires the association to have been preloaded in the changeset struct. Missing data will invoke the :on_replace behaviour defined on the association.

The example given in the docs:

# We can associate at any time post and tags together using changesets
post
|> Repo.preload(:tags) # Load existing data
|> Ecto.Changeset.change() # Build the changeset
|> Ecto.Changeset.put_assoc(:tags, [tag]) # Set the association
|> Repo.update!

I don't want to update the likes association -- I want to add one. Is put_assoc the wrong choice here?

put_assoc is OK here only if you don't mind preloading all the existing likes. If you want to avoid the preload, then a Join Schema looks like the best option.

Do I really need to preload the likes just to insert an item? Even on update I don't want to load the whole collection of associations to change one.

For the convenience of using put_assoc you'll need to preload. The alternative is to use a Join Schema, allowing you to explicitly insert/update the relationship independently of the entity either side.

Do I need to load User and Dicussion_item to add a like here?

No, you only need the IDs. You can construct the Like changeset with the user_id field populated in the params.

Similarly, with a DiscussionItemLikes join schema, you can set the like_id and discussion_item_id.

Ecto.Multi Might be a clean way to create a %Like{} and a %DiscussionItemLike{} in a single transaction.

Multi.new()
|> Multi.insert(insert_like_changeset(params))
|> Multi.insert(insert_discussion_item_like_changeset(params))
|> Repo.transaction()