管理多对多关联

时间:2015-07-26 19:30:21

标签: elixir phoenix-framework

说,我有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列表,如下所示:

tags_ids[]=1&tags_ids[]=2

问题是如何在凤凰城保存时将标签链接到帖子?

2 个答案:

答案 0 :(得分:9)

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

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

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

如果您想更新标签,您有两种选择。

  1. 全部删除并插入新条目
  2. 查找更改,并保留现有条目
  3. 我希望它有所帮助。

答案 1 :(得分:8)

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

多对多关系需要连接表,因为标签和帖子都不能将外键直接指向彼此(这将创建一对多关系)。

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

确保您是否希望能够查看与给定标记相关联的所有帖子,并在Tag模型中以另一种方式定义关系:

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_posts,其中包含您要保存的帖子的ID以及列表中标记的ID。