Reputation: 16749
I'm trying to create a self-referencing many_to_many
relationship in Ecto 2. I followed this blogpost and got it working so far. However trying to update the associations with Ecto.Changeset.put_assoc
always leads to an error. And I don't understand why.
This is the setup:
First the migrations to create users and the association table for contacts (every user can have multiple contacts which are also users):
# priv/repo/migrations/create_users_table.ex
defmodule MyApp.Repo.Migrations.CreateUsersTable do
use Ecto.Migration
def change do
create table(:users) do
add :username, :string
end
create unique_index(:users, [:username])
end
end
# priv/repo/migrations/create_contacts_table.ex
defmodule MyApp.Repo.Migrations.CreateContactsTable do
use Ecto.Migration
def change do
create table(:contacts) do
add :user_id, references(:users, on_delete: :nothing), primary_key: true
add :contact_id, references(:users, on_delete: :nothing), primary_key: true
timestamps()
end
end
end
Now the models:
defmodule MyApp.User do
use MyApp.Web, :model
alias MyApp.Contact
schema "users" do
field :username, :string
# Add the many-to-many association
has_many :_contacts, MyApp.Contact
has_many :contacts, through: [:_contacts, :contact]
timestamps
end
# Omitting changesets
end
defmodule MyApp.Contact do
use MyApp.Web, :model
alias MyApp.User
schema "contacts" do
belongs_to :user, User
belongs_to :contact, User
end
end
This now works:
user = Repo.get!(User, 1) |> Repo.preload :contacts
user.contacts
Now I'm trying to parse a string of comma-separated IDs, fetch the users, turn them into changesets and attach them as contacts to another user
# Parse string and get list of ids
contact_ids = String.split("2, 3, 4", ",") |> Enum.map(&String.trim/1)
# Get contacts
contacts = Enum.map(contact_ids, fn(id) ->
Repo.get! User, id
end)
# Turn them into changesets
contact_changesets = Enum.map(contacts, &Ecto.Changeset.change/1)
# Update the associations
result = user |> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:contacts, contact_changesets)
|> Repo.update
The error I'm getting is
** (ArgumentError) cannot put assoc `contacts`, assoc `contacts` not found. Make sure it is spelled correctly and properly pluralized (or singularized)
(ecto) lib/ecto/changeset.ex:568: Ecto.Changeset.relation!/4
(ecto) lib/ecto/changeset.ex:888: Ecto.Changeset.put_relation/5
But I can preload the association and I can also manually create associations. So I could loop over the contact_ids and do this:
result = user
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:contacts, [Contact.changeset(%Contact{}, %{user_id: user_id, contact_id: contact_id})])
|> Repo.insert
What am I doing wrong here?
Upvotes: 2
Views: 963
Reputation: 4081
I couldn't reproduce the problem with my own associations. I have a feeling you might at some point be working with %MyApp.Contact{}
instead of %MyApp.User{}
? Can you check that and report back?
Something that I did notice (but would not produce this error): you are trying to put MyApp.User
changesets into the :contacts
association, which requires MyApp.Contact
changesets.
You could try to use many_to_many
. You can be sure you get back MyApp.User
with it, so there will be less edge cases like this. It was specifically made for these types of associations anyways.
MyApp.User schema:
schema "users" do
field :username, :string
many_to_many :contacts, MyApp.User, join_through: MyApp.Contact, join_keys: [user_id: :id, contact_id: id]
timestamps
end
I added the join_keys
option, because I think without it Ecto might get confused in this situation. I suggest you try with and without it.
With many_to_many
you can insert MyApp.User
changesets directly into the :contacts
association as well, which seems to be what you want to do.
Upvotes: 2