Phoenixでmany_to_manyを設定してDBからデータを取得して表示するのは非常に簡単。
ではformで新規登録したり更新したりする際に一緒にmany_to_manyのデータを更新するのはどのようにするのか一通り試してみた。
例として、Postに複数のTagが紐付いているパターンで考える。
PostsTagのモデルはなく、join_throughにて文字列でposts_tagsのテーブルを指定しているだけ。
formではTagをカンマ区切りで設定できるという仕様。
とりあえず入力欄として使用するためのtag_namesというvirtualフィールドを作っておく。
field :tag_names, :string, virtual: true
そしてフォームの入力欄を追加。
<div class="form-group">
<%= label f, :tag_names, class: "control-label" %>
<%= text_input f, :tag_names, class: "form-control" %>
</div>
ここまではシンプルで難しいことはない。
既に登録されているデータを更新する際に、上記の入力欄に表示を行うための処理。
とくに難しいことはなく、tag_namesにカンマ区切りの値を入れておくだけ。
モデルに値をセットする関数を追加。
def prepare_form(changeset) do
tag_names = Enum.map(get_field(changeset, :tags), fn(tag) -> tag.name end)
|> Enum.join(",")
put_change(changeset, :tag_names, tag_names)
end
これをedit時に呼び出すだけ。
changeset = Post.changeset(post)
|> Post.prepare_form
とりあえず、Tagを登録するためのRepoを作った。
(Tagのモデル内に実装しても良いのかもしれないが、
デフォルトでモデルにはRepoがaliasされていないことからモデル内ではRepoを使用しない方が良い想定なのかということも考慮し、
専用のRepoを作る形とした。
実際どういった形が望ましいのかは不明)
文字列でtag_namesを渡すと保存したTagの配列を取得する関数がメイン。
defmodule App.TagRepo do
import App.Repo
import Ecto.Changeset
alias App.Tag
def save_tags(tag_names) do
tags = tag_names_to_tags(tag_names)
|> Enum.map(fn(tag) ->
case get_by(Tag, name: tag.name) do
nil ->
Tag.changeset(tag)
|> insert!
saved_tag -> saved_tag
end
end)
end
def tag_names_to_tags(tag_names) do
String.split(tag_names, ",")
|> Enum.map(fn(name) -> %Tag{name: name} end)
end
end
これをcreateとupdateで呼び出すだけ。
新しく入力されたタグは新しいTagとして保存され、posts_tagsも登録される。
タグが減った場合はposts_tags(のみ)も減る。
post = Repo.get!(Post, id)
|> Repo.preload(:tags)
tags = TagRepo.save_tags(post_params["tag_names"])
changeset = Post.changeset(post, post_params)
|> Ecto.Changeset.put_assoc(:tags, tags)
updateの方のみ、上記のようにpreloadが必要となる。
また、タグが減った場合エラーになるので、モデルに下記のon_replaceの追記も必要。
many_to_many :tags, App.Tag, join_through: "posts_tags", on_replace: :delete
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント