Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manage many-to-many association

Say, I have Post model which belongs to many Tags:

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

When saving a Post I get tags_ids list from multiselect field like that:

tags_ids[]=1&tags_ids[]=2

The question is how to link Tags to the Post on save in Phoenix?

like image 970
fey Avatar asked Jul 26 '15 19:07

fey


People also ask

How do you manage a many-to-many relationship?

When you have a many-to-many relationship between dimension-type tables, we provide the following guidance: Add each many-to-many related entity as a model table, ensuring it has a unique identifier (ID) column. Add a bridging table to store associated entities. Create one-to-many relationships between the three tables.

What is an example of a many-to-many relationship?

A many-to-many relationship exists when one or more items in one table can have a relationship to one or more items in another table. For example: Your Order table contains orders placed by multiple customers (who are listed in the Customers table), and a customer may place more than one order.

Why is many-to-many relationships a problem?

The problem with many-to-many relationships is that it can cause duplications in the returned datasets, which can result in incorrect results and might consume excessive computing resources. This section provides solutions and workarounds to common scenarios with many-to-many relationships.

What is the difference between one-to-many and many-to-many?

One-to-many: A record in one table is related to many records in another table. Many-to-many: Multiple records in one table are related to multiple records in another table.


2 Answers

Nested changesets are not supported yet in ecto: https://github.com/elixir-lang/ecto/issues/618 You must be save the tags by yourself.

In the following code snippets I will pick the tag_ids and insert them into the join table if the Post.changeset/2 give me a valid result. For hold the selected tags in Form I added an virtual field that we can read in form and setup a default. It's not the finest solution but it works for me.

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 for create many to many association)

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

If you want to update tags, you have two options.

  1. Delete all and insert new entries
  2. Look for changes, and keep the existing entries

I hope it helps.

like image 146
Fabi755 Avatar answered Nov 03 '22 01:11

Fabi755


The first thing you want to do is to fix up the models. Ecto provides the has_many through: syntax for many-to-many relationships. Here are the docs.

A many-to-many relationship requires a join table since neither tags nor posts can have foreign keys pointing directly at one another (that would create a one-to-many relationship).

Ecto requires that you define the one-to-many join table relationship using has_many prior to the many-to-many relationship that uses has_many through:.

With your example, it would look like:

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

This assumes that you have a join table tag_posts that looks something like:

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

Make sure if you want to be able to see all the posts associated with a given tag, that you define the relationship the other way in the Tag model:

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

Then, in your controller, you want to create new tag_posts with both the ID of the post you are saving, and the id of the tags from your list.

like image 23
The Brofessor Avatar answered Nov 03 '22 01:11

The Brofessor