mythicalprogrammer
Insert many to many ecto was harder because lack of info
It’s a bit of ranting.
I just hope this is constructive.
Anyway the problem:
I have a perfumes table which have a many to many relationship with companies table. So there is a join table perfume_company_joins (I know I’m very good at naming).
Anyway, the companies table is mostly static. It’s like a look up table. Users are able to submit a request to add a new perfume, sign this form html template and add how many company that was involve in building the perfume.
The problem is I couldn’t for the life of me find the solution online. All of the solutions out there assume that both companies and perfumes record I’m inserting are new. I’m trying to just say, “Hey man use this existing company record here and here’s the existing company id.”
I kept on bouncing between cast_assoc and put_assoc for my change set (Line 38).
36 perfume
37 |> cast(attrs, [:perfume_name, :concentration, :gender, :perfume_description, :picture_url, : year_released, :month_released, :day_released])
38 |> put_assoc(:companies)
After two days of reading and trying different things.
I re-reading the doc and blog post out there for cast_assoc and put_assoc. I thought why not try it on the join table.
IT FREAKING WORKS! Well after dicking around cast_assoc and it didn’t work I tried put_assoc and it worked (Line 38).
29 def changeset(perfume, attrs) do
30 company_ids = attrs["company"]
31 company_id_records = company_ids
32 |> Enum.map(
33 fn(company_id) -> %{
34 company_id: String.to_integer(company_id)
35 } end)
36 perfume
37 |> cast(attrs, [:perfume_name, :concentration, :gender, :perfume_description, :picture_url, : year_released, :month_released, :day_released])
38 |> put_assoc(:perfume_company_joins, company_id_records)
41 |> validate_required([:perfume_name, :gender, :perfume_description])
42 end
Anyway, yeah my bad for the rant. I hope this help someone out there. Yall have an awesome weekend.
Most Liked
peerreynders
See: Example: Adding tags to a post
TLDNR: For put_assoc to work as expected it needs to be handed full schema structs, not just IDs (plain maps or keyword lists lead to Ecto trying to insert a new record on the other end of the association).
This works without referring to the association table:
# Usually in MusicDB.Album
def changeset(album, params \\ %{}) do
# obtain genres as full schema structs
genres = fetch_selected_genres(params["genres"])
album
|> EC.cast(params, [:title])
|> EC.put_assoc(:genres, genres)
|> EC.validate_required([:title])
end
(I also find that it is usually necessary to show the schemas involved to make sense of some of the changeset and association code)
# file: lib/music_db/artist.ex
defmodule MusicDB.Artist do
use Ecto.Schema
import Ecto.Changeset
alias MusicDB.{Artist, Album}
schema "artists" do
field(:name)
field(:birth_date, :date)
field(:death_date, :date)
timestamps()
has_many(:albums, Album)
has_many(:tracks, through: [:albums, :tracks])
end
def changeset(artist, params) do
artist
|> cast(params, [:name, :birth_date, :death_date])
|> validate_required([:name])
end
def changeset(%MusicDB.Band{} = band) do
{:ok, birth_date} = Date.new(band.year_started, 1, 1)
{:ok, death_date} = Date.new(band.year_ended, 12, 31)
changeset(
%Artist{
name: band.name,
birth_date: birth_date,
death_date: death_date
},
%{}
)
end
def changeset(%MusicDB.SoloArtist{} = solo_artist) do
name =
"#{solo_artist.name1} #{solo_artist.name2} #{solo_artist.name3}"
|> String.trim()
changeset(
%Artist{
name: name,
birth_date: solo_artist.birth_date,
death_date: solo_artist.death_date
},
%{}
)
end
end
# file: lib/music_db/album.ex
defmodule MusicDB.Album do
use Ecto.Schema
alias MusicDB.{Artist, Track, Genre}
schema "albums" do
field(:title, :string)
timestamps()
belongs_to(:artist, Artist)
has_many(:tracks, Track)
many_to_many(:genres, Genre, join_through: "albums_genres")
end
end
# file: lib/music_db/album_genre.ex
defmodule MusicDB.AlbumGenre do
use Ecto.Schema
alias MusicDB.{Album, Genre}
schema "albums_genres" do
belongs_to(:albums, Album)
belongs_to(:genres, Genre)
end
end
# file: lib/music_db/genre.ex
defmodule MusicDB.Genre do
use Ecto.Schema
alias MusicDB.Album
schema "genres" do
field(:name)
field(:wiki_tag)
timestamps()
many_to_many(:albums, Album, join_through: "albums_genres")
end
end
# file: music_db/priv/repo/playground.exs
#
# http://www.pragmaticprogrammer.com/titles/wmecto
# https://pragprog.com/titles/wmecto/source_code
# http://media.pragprog.com/titles/wmecto/code/wmecto-code.zip
#
# pg_ctl -D /usr/local/var/postgres start
# mix format ./priv/repo/playground.exs
# mix run ./priv/repo/playground.exs
#
defmodule AppInfo do
def string() do
Application.loaded_applications()
|> Enum.map(&to_app_keyword/1)
|> Enum.sort_by(&map_app_name/1)
|> Enum.map_join(", ", &app_keyword_to_string/1)
end
defp to_app_keyword({app, _, vsn}),
do: {app, vsn}
defp app_keyword_to_string({app, vsn}),
do: "#{app}: #{vsn}"
defp map_app_name({app, _}),
do: app
end
defmodule Playground do
import Ecto.Query
alias Ecto.Changeset, as: EC
alias Ecto, as: E
alias MusicDB.Repo
alias MusicDB.{Artist, Album, Genre}
def query_bill_evans do
from(a in Artist, where: a.name == "Bill Evans")
|> Repo.one()
end
def cons_integer(text, values) do
case Integer.parse(text) do
{value, _rest} ->
[value | values]
_ ->
values
end
end
def fetch_selected_genres(selected) do
ids = Enum.reduce(selected, [], &cons_integer/2)
case ids do
[] = empty ->
empty
_ ->
from(g in Genre, where: g.id in ^ids)
|> Repo.all()
end
end
# Usually in MusicDB.Album
def changeset(album, params \\ %{}) do
# obtain genres as full schema structs
genres = fetch_selected_genres(params["genres"])
album
|> EC.cast(params, [:title])
|> EC.put_assoc(:genres, genres)
|> EC.validate_required([:title])
end
def make_params do
%{
"title" => "Test Album",
"genres" => ["1","2"]
}
end
def play do
# Note: Album needs to be associated with
# with an Artist so we use build_assoc
# to create the "shell" schema struct
# for the Album
#
query_bill_evans()
|> E.build_assoc(:albums)
|> changeset(make_params())
|> Repo.insert()
end
end
IO.puts(AppInfo.string())
IO.inspect(Playground.play())
$ mix run ./priv/repo/playground.exs
asn1: 5.0.9, compiler: 7.4.4, connection: 1.0.4, crypto: 4.5.1, db_connection: 2.0.5, decimal: 1.6.0, ecto: 3.0.6, ecto_sql: 3.0.5, elixir: 1.9.1, hex: 0.20.1, inets: 7.0.9, jason: 1.1.2, kernel: 6.4.1, logger: 1.9.1, mariaex: 0.9.1, mix: 1.9.1, music_db: 0.1.0, postgrex: 0.14.1, public_key: 1.6.7, ssl: 9.3.5, stdlib: 3.9.2, telemetry: 0.3.0
18:46:13.559 [debug] QUERY OK source="artists" db=4.4ms decode=0.6ms queue=1.2ms
SELECT a0."id", a0."name", a0."birth_date", a0."death_date", a0."inserted_at", a0."updated_at"
FROM "artists" AS a0 WHERE (a0."name" = 'Bill Evans') []
18:46:13.566 [debug] QUERY OK source="genres" db=0.8ms queue=0.9ms
SELECT g0."id", g0."name", g0."wiki_tag", g0."inserted_at", g0."updated_at"
FROM "genres" AS g0 WHERE (g0."id" = ANY($1)) [[2, 1]]
18:46:13.569 [debug] QUERY OK db=0.2ms
begin []
18:46:13.574 [debug] QUERY OK db=1.2ms
INSERT INTO "albums" ("artist_id","title","inserted_at","updated_at")
VALUES ($1,$2,$3,$4) RETURNING "id" [2, "Test Album", ~N[2019-08-03 22:46:13], ~N[2019-08-03 22:46:13]]
18:46:13.576 [debug] QUERY OK db=0.7ms
INSERT INTO "albums_genres" ("album_id","genre_id")
VALUES ($1,$2) [16, 1]
18:46:13.577 [debug] QUERY OK db=0.2ms
INSERT INTO "albums_genres" ("album_id","genre_id")
VALUES ($1,$2) [16, 2]
18:46:13.583 [debug] QUERY OK db=6.0ms
commit []
{:ok,
%MusicDB.Album{
__meta__: #Ecto.Schema.Metadata<:loaded, "albums">,
artist: #Ecto.Association.NotLoaded<association :artist is not loaded>,
artist_id: 2,
genres: [
%MusicDB.Genre{
__meta__: #Ecto.Schema.Metadata<:loaded, "genres">,
albums: #Ecto.Association.NotLoaded<association :albums is not loaded>,
id: 1,
inserted_at: ~N[2019-08-03 14:35:27],
name: "jazz",
updated_at: ~N[2019-08-03 14:35:27],
wiki_tag: "Jazz"
},
%MusicDB.Genre{
__meta__: #Ecto.Schema.Metadata<:loaded, "genres">,
albums: #Ecto.Association.NotLoaded<association :albums is not loaded>,
id: 2,
inserted_at: ~N[2019-08-03 14:35:27],
name: "live",
updated_at: ~N[2019-08-03 14:35:27],
wiki_tag: "Concert"
}
],
id: 16,
inserted_at: ~N[2019-08-03 22:46:13],
title: "Test Album",
tracks: #Ecto.Association.NotLoaded<association :tracks is not loaded>,
updated_at: ~N[2019-08-03 22:46:13]
}}
$
mythicalprogrammer
multiple_select returns a list of ids. put_assoc requires a map or a keyword list.
Tried your idea it doesn’t work.
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









