一人もくもく会 α verでサービス開始しました。

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