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?
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.
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.
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.
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.
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.
I hope it helps.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With