Elixir Phoenix 更新嵌套多对多关联

Elixir Phoenix update nested many-to-many association

我正在使用 Elixir 和 Phoenix。我有模式 Venues.TeamAccounts.Job,它们之间有 many-to-many 关系。

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

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

我有默认的Venues.update_team/2方法:

def update_team(%Team{} = team, attrs) do
  team
  |> Team.changeset(attrs)
  |> Repo.update()
end

我希望在更新 Team 时能够在 attrs 参数中包含一个 jobs 参数。这应该在 :employees 字段中插入一个关联。

我可以在交互式 elixir 中做到这一点 shell(来自 ElixirSchool 文档)

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)

但我不确定如何将其抽象为 update_team 方法,该方法无需数据库调用即可构建变更集。

当我尝试这样做时:

%Team{}
|> Team.changeset(%{id: 1, name: "Floor"})
|> put_assoc(:employees, [job])
|> Repo.update()

我收到一条错误消息,抱怨未加载 :employees 关联:

** (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

我可以这样做:

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()

(未经测试,下面是更清晰的版本)

 %Ecto.Changeset{tc | data: Venues.get_team!(1) |> Repo.preload(:employees)} |> put_assoc(:employees, [job]) |> Repo.update()

执行此操作的 best/cleanest/most 常规方法是什么?

您可以使用 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

更新表格

<%= 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 %>