【问题标题】:Elixir Phoenix update nested many-to-many associationElixir Phoenix 更新嵌套的多对多关联
【发布时间】:2019-11-06 09:05:09
【问题描述】:

我正在使用 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()

最好/最干净/最传统的方法是什么?

【问题讨论】:

    标签: database elixir associations ecto


    【解决方案1】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多