Hi,
Just started working on my first Phoenix/Elixir app and ran into a problem with Ecto.
In my app, I have an Artist and Song model. They have a many_to_many relationship via Performance model
schema "artists" do
field :name, :string
field :channel_id, :string
field :channel_title, :string
many_to_many :songs, Song, join_through: "performances"
has_many :performances, Performance
timestamps()
end
schema "songs" do
field :name, :string
many_to_many :artists, Artist, join_through: "performances"
has_many :performances, Performance
timestamps()
end
schema "performances" do
field :yt_video_id, :string
field :yt_view_count, :integer
field :yt_title, :string
field :verified, :boolean, default: false
field :published_date, Ecto.Date
belongs_to :artist, Artist
belongs_to :song, Song
timestamps()
end
The way the database is populated is via performances table.
When I add an record to performance, it will always come with artist name, song name and the relevant performance fields
I want to first try to retrieve artist from the artists table, and only create if it is not found
The same goes for song. After these two records are found/created. I will then create a performance record to associate them up.
Based on my understanding, I should use put_assoc in this case since artist and song are created independently from performance
This is how my changeset for performance is written now
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:yt_video_id, :yt_view_count, :yt_title, :verified, :published_date])
|> validate_required([:yt_video_id, :yt_view_count, :yt_title, :verified, :published_date])
|> put_assoc(:song, get_or_create_song(params["song"]))
|> put_assoc(:artist, get_or_create_artist(params["artist"]))
end
def get_or_create_artist(artist_params) do
case artist_params["name"] || [] do
[] -> []
artist_name ->
Repo.get_by(Artist, name: artist_name) ||
Repo.insert!(Artist.changeset(%Artist{}, artist_params))
end
end
def get_or_create_song(song_params) do
case song_params || [] do
[] -> []
song_params ->
case song_params["name"] || [] do
[] -> []
song_name ->
Repo.get_by(Song, name: song_name) ||
Repo.insert!(Song.changeset(%Song{}, song_params))
end
end
end
I ran into this problem for edit route for performance
i.e http://localhost:4000/performances/10/edit
The error message:
no function clause matching in anonymous fn/1 in Ecto.Changeset.Relation.struct_pk/2
The call stack:
Request: GET /performances/9/edit
** (exit) an exception was raised:
** (FunctionClauseError) no function clause matching in anonymous fn/1 in Ecto.Changeset.Relation.struct_pk/2
(ecto) lib/ecto/changeset/relation.ex:357: anonymous fn([]) in Ecto.Changeset.Relation.struct_pk/2
(ecto) lib/ecto/changeset/relation.ex:225: Ecto.Changeset.Relation.single_change/6
(ecto) lib/ecto/changeset.ex:1083: Ecto.Changeset.put_relation/5
(covered) web/models/performance.ex:26: Covered.Performance.changeset/2
(covered) web/controllers/performance_controller.ex:63: Covered.PerformanceController.edit/2
And the line that causes trouble is
|> put_assoc(:song, get_or_create_song(params[“song”]))
Some questions I have:
- Is my assumption that put_assoc should be used in this case? I assume yes because I do not want duplicated artist/song for every entry of performance.
- The thing is that for edit route, there is no need to go through the artist/song retrieval/creation flow, since the performance record already has the data, in that case, should I be adding a different flow for changeset for edit, or should artist/song retrieval/creation be part of performances changeset?
This is the blogpost that I have been following http://blog.plataformatec.com.br/2016/12/many-to-many-and-upserts/
Any feedback or help is appreciated