TLDR:
_Don’t assume that passing ids will be enough for ecto to create the association. You have to preload structs, if you dont ecto will try to create new records via :on_replace with the id and you will get a unique: pkey error
I have user who I want to associate to many programs.
I have a program that can have many users.
from my edit for for a given user I want to be able to select multiple programs via a multi select.
So lets start with my migration
defmodule MyApp.Repo.Migrations.CreateJoinProgramsUsers do
use Ecto.Migration
def change do
create table(:programs_users, primary_key: false) do
add :program_id, references(:programs)
add :user_id, references(:users)
end
end
end
I feel confident that the migration is correct but here it is for reference just incase.
Now on to the my user class.
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.Accounts.User
schema "users" do
...
many_to_many :programs, MyApp.Organization.Program, join_through: "programs_users"
...
end
@doc false
def changeset(%User{} = user, attrs) do
user
|> MyApp.Repo.preload(:programs)
|> cast(attrs, [...])
|> put_assoc(:programs, pull_programs(attrs))
end
def pull_programs(attrs) do
case attrs do
%{"programs" => _} ->
attrs["programs"]
|> Enum.map fn(id) ->
{id, ""} = Integer.parse(id)
%{id: id }
end
_ ->
[]
end
end
end
I dont feel confident that I have the changeset setup correct. I could use a review on that.
Ok so heres where I’m having an issue.
iex(1)> user = Accounts.get_user!(3)
%MyApp.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
...
programs: [],
...
}
iex(2)> Accounts.update_user(user, %{"programs" => ["1"]})
[debug] QUERY OK db=0.3ms
begin []
[debug] QUERY ERROR db=2.1ms
INSERT INTO "programs" ("id","inserted_at","updated_at") VALUES ($1,$2,$3) [1, {{2017, 9, 29}, {20, 42, 18, 606804}}, {{2017, 9, 29}, {20, 42, 18, 606812}}]
[debug] QUERY OK db=0.4ms
rollback []
** (Ecto.ConstraintError) constraint error when attempting to insert struct:
* unique: programs_pkey
If you would like to convert this constraint into an error, please
call unique_constraint/3 in your changeset and define the proper
constraint name. The changeset has not defined any constraint.
(ecto) lib/ecto/repo/schema.ex:495: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
(elixir) lib/enum.ex:1255: Enum."-map/2-lists^map/1-0-"/2
(ecto) lib/ecto/repo/schema.ex:481: Ecto.Repo.Schema.constraints_to_errors/3
(ecto) lib/ecto/repo/schema.ex:213: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
(ecto) lib/ecto/association.ex:953: Ecto.Association.ManyToMany.on_repo_change/4
(ecto) lib/ecto/association.ex:330: anonymous fn/7 in Ecto.Association.on_repo_change/6
(elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/association.ex:327: Ecto.Association.on_repo_change/6
(elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/association.ex:293: Ecto.Association.on_repo_change/3
(ecto) lib/ecto/repo/schema.ex:624: Ecto.Repo.Schema.process_children/4
(ecto) lib/ecto/repo/schema.ex:691: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
(ecto) lib/ecto/adapters/sql.ex:620: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
(db_connection) lib/db_connection.ex:1275: DBConnection.transaction_run/4
(db_connection) lib/db_connection.ex:1199: DBConnection.run_begin/3
(db_connection) lib/db_connection.ex:790: DBConnection.transaction/3
iex(3) :(
I’m not sure why its blowing up at the db with a unique_constraint when I dont have a unique_constraint set for that field. Also note that I’m not trying to create a new program but rather assign it to the user.
Thoughts?
EDIT: Ok I just now noticed the insert into the programs. INSERT INTO "programs"
What do I need to do to only make the link to programs but not create one?
EDIT 2: I think I understand now, it is not enough to just pass an id, but rather I need to preload the struct for that given association. I"m got the error because I assumed that the id alone was enough to satisfy the insert into the join table. The mistake I made was ecto didn’t see the struct as an already existing row because its not preloaded and thus tried to make a new Product with the same ID causing the constraint error on the primary key. Had I read the documentation better I would have probably understood this better.
When updating the data, 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. Preloading is not necessary for newly built structs
– Ecto.Changeset — Ecto v3.11.1
The next step I have is to learn about the best pattern for preloading like this. One idea from the slack channel was.
I needed to create m2m association and I solved it by using a virtual field which stores the selected IDs from the multiple select and then I create the records manually – @amarsahinovic
I guess that makes the most logical sense
Any how I’ll leave this hear in hopes it helps the next person.
[/quote]
**_
I have user who I want to associate to many programs.
I have a program that can have many users.
I want to be able to select multiple programs via a multi select while I edit a given user. So from there I only have IDs to work with in my passed params.
So lets start with my migration
defmodule MyApp.Repo.Migrations.CreateJoinProgramsUsers do
use Ecto.Migration
def change do
create table(:programs_users, primary_key: false) do
add :program_id, references(:programs)
add :user_id, references(:users)
end
end
end
I feel confident that the migration is correct but here it is for reference just incase.
Now on to the my user class.
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.Accounts.User
schema "users" do
...
many_to_many :programs, MyApp.Organization.Program, join_through: "programs_users"
...
end
@doc false
def changeset(%User{} = user, attrs) do
user
|> MyApp.Repo.preload(:programs)
|> cast(attrs, [...])
|> put_assoc(:programs, pull_programs(attrs))
end
def pull_programs(attrs) do
case attrs do
%{"programs" => _} ->
attrs["programs"]
|> Enum.map fn(id) ->
{id, ""} = Integer.parse(id)
%{id: id }
end
_ ->
[]
end
end
end
I dont feel confident that I have the changeset setup correct. I could use a review on that. (“read below: its because I don’t ”)
Ok so heres where I’m having an issue.
iex(1)> user = Accounts.get_user!(3)
%MyApp.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
...
programs: [],
...
}
iex(2)> Accounts.update_user(user, %{"programs" => ["1"]})
[debug] QUERY OK db=0.3ms
begin []
[debug] QUERY ERROR db=2.1ms
INSERT INTO "programs" ("id","inserted_at","updated_at") VALUES ($1,$2,$3) [1, {{2017, 9, 29}, {20, 42, 18, 606804}}, {{2017, 9, 29}, {20, 42, 18, 606812}}]
[debug] QUERY OK db=0.4ms
rollback []
** (Ecto.ConstraintError) constraint error when attempting to insert struct:
* unique: programs_pkey
If you would like to convert this constraint into an error, please
call unique_constraint/3 in your changeset and define the proper
constraint name. The changeset has not defined any constraint.
(ecto) lib/ecto/repo/schema.ex:495: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
(elixir) lib/enum.ex:1255: Enum."-map/2-lists^map/1-0-"/2
(ecto) lib/ecto/repo/schema.ex:481: Ecto.Repo.Schema.constraints_to_errors/3
(ecto) lib/ecto/repo/schema.ex:213: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
(ecto) lib/ecto/association.ex:953: Ecto.Association.ManyToMany.on_repo_change/4
(ecto) lib/ecto/association.ex:330: anonymous fn/7 in Ecto.Association.on_repo_change/6
(elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/association.ex:327: Ecto.Association.on_repo_change/6
(elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/association.ex:293: Ecto.Association.on_repo_change/3
(ecto) lib/ecto/repo/schema.ex:624: Ecto.Repo.Schema.process_children/4
(ecto) lib/ecto/repo/schema.ex:691: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
(ecto) lib/ecto/adapters/sql.ex:620: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
(db_connection) lib/db_connection.ex:1275: DBConnection.transaction_run/4
(db_connection) lib/db_connection.ex:1199: DBConnection.run_begin/3
(db_connection) lib/db_connection.ex:790: DBConnection.transaction/3
iex(3) :(
I’m not sure why its blowing up at the db with a unique_constraint when I dont have a unique_constraint set for that field. Also note that I’m not trying to create a new program but rather assign it to the user.
Thoughts?
EDIT: Ok I just now noticed the insert into the programs. INSERT INTO "programs"
What do I need to do to only make the link to programs but not create one?
EDIT 2: I think I understand now, it is not enough to just pass an id, but rather I need to preload the struct for that given association. I"m got the error because I assumed that the id alone was enough to satisfy the insert into the join table. The mistake I made was ecto didn’t see the struct as an already existing row because its not preloaded and thus tried to make a new Product with the same ID causing the constraint error on the primary key. Had I read the documentation better I would have probably understood this better.
When updating the data, 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. Preloading is not necessary for newly built structs
– Ecto.Changeset — Ecto v3.11.1
The next step I have is to learn about the best pattern for preloading like this. One idea from the slack channel was.
I needed to create m2m association and I solved it by using a virtual field which stores the selected IDs from the multiple select and then I create the records manually – @amarsahinovic
I guess that makes the most logical sense
Any how I’ll leave this hear in hopes it helps the next person.