Эликсир Феникс обновил вложенную ассоциацию «многие ко многим» - PullRequest
3 голосов
/ 06 ноября 2019

Я использую Эликсир и Феникс. У меня есть схемы Venues.Team и Accounts.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

Я хочу иметь возможность включить аргумент jobs в параметр attrs при обновлении Team. Это должно вставить ассоциацию в поле :employees.

Я могу сделать это в интерактивной оболочке эликсира (из документации 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()

Какой самый лучший / самый чистый / самый обычный способ сделать это?

1 Ответ

3 голосов
/ 06 ноября 2019

Вы можете обновить, используя 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 %>
...