2018-08-30に更新

Phoenixでmany_to_manyのフォームを対応

Phoenixでmany_to_manyを設定してDBからデータを取得して表示するのは非常に簡単。
ではformで新規登録したり更新したりする際に一緒にmany_to_manyのデータを更新するのはどのようにするのか一通り試してみた。

form

例として、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とtags_postsを登録する

とりあえず、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開発者

Crieitの開発者です。 主にLAMPで開発しているWebエンジニアです(在宅)。大体10年程。 記事でわかりにくいところがあればDMで質問していただくか、案件発注してください。 業務依頼、同業種の方からのコンタクトなどお気軽にご連絡ください。 業務経験有:PHP, MySQL, Laravel5, CakePHP3, JavaScript, RoR 趣味:Elixir, Phoenix, Node, Nuxt, Express, Vue等色々

Crieitはαバージョンで開発中です。進捗は公式Twitterアカウントをフォローして確認してください。 興味がある方は是非記事の投稿もお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか
関連記事

コメント