I'm using Elixir & Phoenix. I have the schemas Venues.Team
and Accounts.Job
with a many-to-many
relationship between them.
defmodule Runbook.Venues.Team do
use Ecto.Schema
import Ecto.Changeset
schema "teams" do
field :name, :string
belongs_to :venue, Runbook.Venues.Venue
many_to_many :employees, Runbook.Accounts.Job, join_through: "jobs_teams"
timestamps()
end
@doc false
def changeset(team, attrs) do
team
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
and
defmodule Runbook.Accounts.Job do
use Ecto.Schema
import Ecto.Changeset
schema "jobs" do
field :role, :string
belongs_to :user, Runbook.Accounts.User
belongs_to :venue, Runbook.Venues.Venue
many_to_many :teams, Runbook.Venues.Team, join_through: "jobs_teams"
timestamps()
end
@doc false
def changeset(job, attrs) do
job
|> cast(attrs, [:role])
|> validate_required([:role])
end
end
I have the default Venues.update_team/2
method:
def update_team(%Team{} = team, attrs) do
team
|> Team.changeset(attrs)
|> Repo.update()
end
I want to be able to include a jobs
argument inside the attrs
parameter when updating a Team
. This should insert an association into the :employees
field.
I can do this in the interactive elixir shell (from ElixirSchool docs)
team = Venues.get_team!(1)
team_changeset = Ecto.Changeset.change(team)
team_add_employees_changeset = team_changeset |> put_assoc(:employees, [job])
Repo.update!(team_add_employees_changeset)
But I'm not sure how to abstract this to the update_team
method, which builds the changeset up without a database call.
When I try to do it:
%Team{}
|> Team.changeset(%{id: 1, name: "Floor"})
|> put_assoc(:employees, [job])
|> Repo.update()
I get an error complaining that the :employees
association is not loaded:
** (Ecto.NoPrimaryKeyValueError) struct `%Runbook.Venues.Team{__meta__: #Ecto.Schema.Metadata<:built, "teams">, employees: #Ecto.Association.NotLoaded<association :employees is not
loaded>, id: nil, inserted_at: nil, name: nil, updated_at: nil, venue: #Ecto.Association.NotLoaded<association :venue is not loaded>, venue_id: nil}` is missing primary key value
(ecto) lib/ecto/repo/schema.ex:898: anonymous fn/3 in Ecto.Repo.Schema.add_pk_filter!/2
(elixir) lib/enum.ex:1948: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/repo/schema.ex:312: Ecto.Repo.Schema.do_update/4
I can sorta do it like this:
changeset = Team.changeset(%Team{}, %{id: 1, name: "Floor"})
team = Venues.get_team!(1) |> Repo.preload(:employees)
preloaded_changeset = %Ecto.Changeset{changeset | data: team}
preloaded_changeset |> put_assoc(:employees, [job]) |> Repo.update()
(untested, a cleaner version of the below)
%Ecto.Changeset{tc | data: Venues.get_team!(1) |> Repo.preload(:employees)} |> put_assoc(:employees, [job]) |> Repo.update()
What is the best/cleanest/most conventional way to do this?
You can update by using Ecto.Changeset.cast_assoc/3
def update_team(%Team{} = team, attrs) do
team
|> Repo.preload(:employees) # has to be preloaded to perform update
|> Team.changeset(attrs)
|> Ecto.Changeset.cast_assoc(:employees, with: &Job.changeset/2)
|> Repo.update
end
update form
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
<div class="form-group">
<%= inputs_for f, :job, fn cf -> %>
<%= label cf, :job_param_1 %>
<%= text_input cf, :job_param_1 %>
<%= error_tag cf, :job_param_1 %>
<% end %>
</div>
<div class="form-group">
<%= inputs_for f, :job, fn cf -> %>
<%= label cf, :job_param_2 %>
<%= text_input cf, :job_param_2 %>
<%= error_tag cf, :job_param_2 %>
<% end %>
</div>
<div>
<%= submit "Save" %>
</div>
<% end %>