Elixir ecto 2 создать ассоциацию many_to_many

Как установить связь "многие ко многим" с ecto 2? В качестве примера приложения я хочу создать сообщение, которое может быть в нескольких категориях. Категории уже существуют. Например:

[%Category{id: "1", name: "elixir"}, %Category{id: "2", name: "erlang"}]

Я использую Ecto 2 beta 0. Пример проекта называется Ecto2.

Я определил две модели:

defmodule Ecto2.Post do
  use Ecto2.Web, :model
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    many_to_many :categories, Ecto2.Category, join_through: "posts_categories", on_replace: :delete
    timestamps
  end

  @required_fields ~w(title)
  @optional_fields ~w()
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:categories)  # not suitable?
  end
end

defmodule Ecto2.Category do
  use Ecto2.Web, :model

  schema "categories" do
    field :name, :string

    timestamps
  end

  @required_fields ~w(name)
  @optional_fields ~w()
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

Я пробовал сделать это так:

post = Repo.get!(Post, 1) |> Repo.preload(:categories)
changeset = Post.changeset(post, %{"title"=> "bla", "categories"=> [%{id: "1"}]})
Repo.update!(changeset)

Но cast_assoc в Post.changeset не подходит для этой задачи, он хочет создать совершенно новую категорию вместо связанной. Что мне использовать вместо этого? build_assoc? Но в документации build_assoc не упоминается, что это полезно с many_to_many. Как мне его использовать? Должен ли я тогда поместить build_assoc в Post.changeset или использовать его в контроллере Phoenix.


person Roel Kramer    schedule 20.02.2016    source источник
comment
Я вижу, что приведенный ниже ответ показывает способ решить эту проблему за пределами набора изменений Post, используя put_assoc (: Categories, [category1]) (возможно, в контроллере и т. Д.). Вы нашли способ решить эту проблему в самом наборе изменений Post? Как вы упомянули, cast_assoc создает новые модели, а не связывается с существующими.   -  person Morgz    schedule 27.09.2016


Ответы (2)


Вы можете присоединиться через таблицу, передав строку, например «posts_categories», или через схему, передав схему, например MyApp.PostCategory. Я предпочитаю присоединяться через схему, так как могут быть включены временные метки. Допустим, вы выбираете соединение через схему вместо таблицы:

  1. Вам необходимо создать отдельную таблицу (например, posts_categories) для связи many_to_many, к которой нужно присоединиться.

```

def change do
  create table(:posts_categories) do
    add :post_id, references(:posts)
    add :category_id, references(:categories)
    timestamps
  end
end
  1. Создайте схему для таблицы, созданной на шаге 1. В папке web \ models создайте файл post_category.ex:

```

defmodule Ecto2.PostCategory do
use Ecto2.Web, :model

schema "posts_categories" do
  belongs_to :post, Ecto2.Post
  belongs_to :category, Ecto2.Category
  timestamps
end

def changeset(model, params \\ %{}) do
  model
  |> cast(params, [])
end
end

Ecto beta 2 изменилась: с пустой карты на пустую, а cast \ 4 на cast \ 3. Проверьте журнал изменений.

  1. Добавьте эту строку в схему поста:

    many_to_many :categories, Ecto2.Category, join_through: Ecto2.PostCategory

  2. Добавьте эту строку в схему своей категории:

many_to_many :posts, Ecto2.Post, join_through: Ecto2.PostCategory

Вот и все! Теперь вы можете обновлять как ``

post1 = Repo.get!(Post, 1)
category1 = Repo.get!(Category, 1)

post1
|> Repo.preload(:categories)
|> Post.changeset(%{})
|> put_assoc(:categories, [category1])
|> Repo.update!

```

person William Shea    schedule 28.02.2016

После спокойного сна и некоторого покопания в модульных тестах ecto я нашел частичный ответ. Правильная функция для вызова - Ecto.Changeset.put_assoc. Он возвращает набор изменений. Остальная часть вопроса находится внизу этого ответа.

def run_insert_1 do
  c1 = Repo.get!(Category, 1)
  c2 = %Category{name: "cat 2"}

  # Inserting
  changeset =
    %Post{title: "1"}
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:categories, [c1, c2])
  post = Repo.insert!(changeset)
  IO.inspect post
end

def run_insert_2 do
  c1 = Repo.insert! %Category{name: "cat 1"}
  c2 = %Category{name: "cat 2"}

  # Inserting
  changeset =
    %Post{title: "1"}
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:categories, [c1, c2])
  post = Repo.insert!(changeset)
  IO.inspect post
end

def run_update do
  c1 = Repo.insert! %Category{name: "cat update"}
  c2 = %Category{name: "cat 2"}
  post = Repo.get!(Post, 1) |> Repo.preload(:categories)
  # Updating
  changeset =
    post
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:categories, [c1])
  post = Repo.update!(changeset)
  IO.inspect post
end

Это частичное решение, потому что, если я хочу обновить связанные категории (в публикации уже есть список связанных категорий), мне нужно сначала удалить, а затем сохранить пустой список категорий. Можно ли сделать это за один раз?

def run_update_2 do
  c2 = Repo.get!(Tag, 2)
  # Assumes Post 1 already has  a few categories in it (for example after
  # running run_update()
  post = Repo.get!(Post, 1) |> Repo.preload(:categories)

  # Remove and add again
  changeset =
    post
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:categories, [])
  IO.inspect changeset
  post = Repo.update!(changeset)

  changeset =
    post
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:categories, [c2])

  post = Repo.update!(changeset)
  IO.inspect post
end
person Roel Kramer    schedule 21.02.2016