Управление ассоциацией "многие-ко-многим"

Скажем, у меня есть модель Post, которая принадлежит многим тегам:

defmodule MyApp.Post do
  use MyApp.Web, :model

  schema "tours" do
    field :title, :string
    field :description, :string
    has_many :tags, {"tags_posts", MyApp.Tag}
  end

  # …
end

При сохранении сообщения я получаю список tags_ids из поля multiselect следующим образом:

tags_ids[]=1&tags_ids[]=2

Вопрос в том, как привязать теги к сообщению о сохранении в Phoenix?

Ответ 1

Вложенные изменения еще не поддерживаются в ecto: https://github.com/elixir-lang/ecto/issues/618 Вы должны сохранить теги самостоятельно.

В следующих фрагментах кода я выберу tag_ids и вставляю их в таблицу соединений, если Post.changeset/2 дает мне действительный результат. Для удержания выбранных тегов в Форме я добавлено виртуальное поле, которое мы можем прочитать в форме и установить значение по умолчанию. Это не лучшее решение, но оно работает для меня.

PostController

def create(conn, %{"post" => post_params}) do
  post_changeset = Post.changeset(%Post{}, post_params)

  if post_changeset.valid? do
    post = Repo.insert!(post_changeset)

    case Dict.fetch(post_params, "tag_ids") do
      {:ok, tag_ids} ->

        for tag_id <- tag_ids do
          post_tag_changeset = PostTag.changeset(%PostTag{}, %{"tag_id" => tag_id, "post_id" => post.id})
          Repo.insert(post_tag_changeset)
        end
      :error ->
        # No tags selected
    end

    conn
    |> put_flash(:info, "Success!")
    |> redirect(to: post_path(conn, :new))
  else
    render(conn, "new.html", changeset: post_changeset)
  end
end

PostModel

schema "posts" do
  has_many :post_tags, Stackoverflow.PostTag
  field :title, :string
  field :tag_ids, {:array, :integer}, virtual: true

  timestamps
end

@required_fields ["title"]
@optional_fields ["tag_ids"]

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

PostTagModel (JoinTable для создания многих-многих ассоциаций)

schema "post_tags" do
  belongs_to :post, Stackoverflow.Post
  belongs_to :tag, Stackoverflow.Tag

  timestamps
end

@required_fields ["post_id", "tag_id"]
@optional_fields []

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

PostForm

<%= form_for @changeset, @action, fn f -> %>

  <%= if f.errors != [] do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below:</p>
      <ul>
      <%= for {attr, message} <- f.errors do %>
        <li><%= humanize(attr) %> <%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :title, "Title" %>
    <%= text_input f, :title, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= label f, :tag_ids, "Tags" %>
    <!-- Tags in this case are static, load available tags from controller in your case -->
    <%= multiple_select f, :tag_ids, ["Tag 1": 1, "Tag 2": 2], value: (if @changeset.params, do: @changeset.params["tag_ids"], else: @changeset.model.tag_ids) %>
  </div>

  <div class="form-group">
    <%= submit "Save", class: "btn btn-primary" %>
  </div>

<% end %>

Если вы хотите обновлять теги, у вас есть два варианта.

  • Удалить все и вставить новые записи
  • Ищите изменения и сохраните существующие записи

Надеюсь, это поможет.

Ответ 2

Первое, что вы хотите сделать, это исправить модели. Ecto предоставляет синтаксис has_many through: для отношений "многие ко многим" . Вот документы.

Для отношения "многие ко многим" требуется таблица соединений, поскольку ни теги, ни сообщения не могут иметь внешние ключи, указывающие прямо друг на друга (что создало бы отношения "один ко многим" ).

Ecto требует, чтобы вы определяли отношения таблицы "один ко многим", используя has_many до отношения "многие ко многим" , которое использует has_many through:.

В вашем примере это будет выглядеть так:

defmodule MyApp.Post do

  use MyApp.Web, :model

  schema "posts" do
    has_many :tag_posts, MyApp.TagPost
    has_many :tags, through: [:tag_posts, :tags]

    field :title, :string
    field :description, :string
  end

  # …
end

Это предполагает, что у вас есть таблица соединений tag_posts, которая выглядит примерно так:

defmodule MyApp.TagPost do

  use MyApp.Web, :model

  schema "tag_posts" do
    belongs_to :tag, MyApp.Tag
    belongs_to :post, MyApp.Post

    # Any other fields to attach, like timestamps...
  end

  # …
end

Убедитесь, что вы хотите видеть все сообщения, связанные с данным тегом, которые вы определяете в другом виде в модели тегов:

defmodule MyApp.Tag do

  use MyApp.Web, :model

  schema "posts" do
    has_many :tag_posts, MyApp.TagPost
    has_many :posts, through: [:tag_posts, :posts]

    # other post fields
  end

  # …
end

Затем в вашем контроллере вы хотите создать новые теги tag_post с идентификатором сохраненного сообщения и идентификатором тегов из вашего списка.