管理多对多关联

Manage many-to-many association

说,我有 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

保存 Post 时,我从多选字段中得到 tags_ids 列表:

tags_ids[]=1&tags_ids[]=2

问题是如何将 link 标签添加到 Post on save in Phoenix?

您要做的第一件事是修复模型。 Ecto 为多对多关系提供 has_many through: 语法。 Here are the docs.

多对多关系需要连接 table 因为标签和 post 都不能有直接指向彼此的外键(这会创建一对多关系).

Ecto 要求您在使用 has_many through:.[=18= 的多对多关系之前使用 has_many 定义一对多连接 table 关系]

用你的例子,它看起来像:

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

这假设您有一个看起来像这样的联接 table 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

如果您希望能够看到与给定标签关联的所有 post,请确保您在标签模型中以其他方式定义关系:

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

然后,在您的控制器中,您想要使用您正在保存的 post 的 ID 和列表中的标签 ID 创建新的 tag_posts。

还不支持嵌套变更集:https://github.com/elixir-lang/ecto/issues/618您必须自己保存标签。

在下面的代码片段中,如果 Post.changeset/2 给我一个有效结果,我将选择 tag_ids 并将它们插入到联接 table 中。为了在表单中保留选定的标签,我添加了一个虚拟字段,我们可以在表单中读取它并设置默认值。这不是最好的解决方案,但对我有用。

后控制器

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, Whosebug.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, Whosebug.Post
  belongs_to :tag, Whosebug.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

邮政表格

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

如果你想更新标签,你有两个选择。

  1. 全部删除并插入新条目
  2. 查找更改,并保留现有条目

希望对您有所帮助。