LoginSignup
1
3

More than 5 years have passed since last update.

ネストの深い項目を更新するためのモデルの書き方

Last updated at Posted at 2017-04-19

中間テーブルで扱うデータがpolymorphicになっていて、それを該当モデルからthroughを使って呼び出すまでは簡単だったんだけれど、それをView側で更新するのが大変だったのでメモとして書いておく。

has_manyでpolymorphicな関連を取得

まずはpolymorphicなモデルを扱う中間テーブルのモデル。

Combinationモデル
class Combination < ApplicationRecord
  belongs_to :group, polymorphic: true
  belongs_to :group_item, polymorphic: true

  validates :group_item, presence: true
  validates :group, presence: true
  validates_uniqueness_of :group_id, scope: [:group_type, :group_item_id, :group_item_type]  end
end

次に、Memoモデル。
メモを確認できるメンバーを、中間テーブルcombinationsを通じて取得する。
has_manyを2つ定義している。

Memoモデル
class Memo < ApplicationRecord

  has_many :combinations, as: :group, dependent: :destroy, inverse_of: :group
  has_many :members,
           through: :combinations,
           source: :group_item,
           source_type: 'User',
           autosave: false

  accepts_nested_attributes_for :members
end

本来group_itemはpolymorphicなので、どんなモデルでも入り得るのだが、source_typeをUserにすることで、Userのインスタンスとして取得できるようにしてある。

次にUserモデル。一応載せていますが、今回の主役はMemoモデルです。

Userモデル
class User < ApplicationRecord
  has_many :combinations, as: :group_item, dependent: :destroy
  has_many :memos, inverse_of: :user, dependent: :destroy
end

こうしておくと、メモが見られるメンバーをすぐ取得できます(メモが見られる、見られないの実装は省略)

memo = Memo.first
memo.members # メモが見られるメンバーを取得

View側の実装

View側では、fields_forを使って実装していきます。
肝になるのは、include_id: false, multiple: trueの部分と、fm.check_boxinclude_hidden: falseです。

<%= simple_form_for @memo do |f| %>
  <%= f.input :title %>
  <%= f.input :content %>
  <%= f.fields_for :members, User.all, include_id: false, multiple: true do |fm| %>
    <label>
      <%= fm.check_box :id,
                       {
                         include_hidden: false,
                         checked: @memo.member_ids.include?(fm.object.id)
                       },
                       fm.object.id %>
      <%= fm.object.name %>
    </label>
  <% end %>
  <%= f.button :submit %>
<% end %>

include_id: false

fields_forでinclude_id: falseを指定しない場合、デフォルトでidがhiddenフィールドが自動的に追加されてしまい、必ず送られてしまいます。その結果、チェックボックスに関係なく、全員がメモを見られるメンバーになってしまいます。ですので、自動的にidのhiddenフィールドが作られないようにします。

include_hidden: false

check_boxは、デフォルトでチェックが入っている時の値と、入っていないときの値を送ることができます。
入ってない場合の値はhiddenフィールドに設定されます。

今回のパターンではメンバーになるUserのidを送りたいのですが、チェックが入ってない場合は何も送る必要がないので、hiddenフィールド自体を出さないようにします。

Controllerの実装

controller側では、strong parametersの実装で、membersを値が渡ってくることを許可します。

追記(2019-02-12)

更新の際の動作がうまく動いていませんでした。controllerに修正が必要でした。
具体的には、members_attributesに該当するチェックボックスを全て外した状態で更新しようとすると、members_attributesが送られてこないため、あとで説明するmembers_attributes=メソッドが呼ばれず@memo.membersが更新されませんでした。
members_attributesが送られていない場合は@memo.members.clearを呼ぶことで解決しました。

class MemosController < ApplicationController

  def create
    Memo.transaction do
      @memo = Memo.new(memo_params)
      @memo.save!
    end
  rescue => e
    render :new
  end

  def update
    Memo.transaction do
      @memo.members.clear if memo_params[:members_attributes].blank?
      @memo.update!(memo_params)
    end
  rescue => e
    render :edit
  end


  private
  def memo_params
    params.require(:memo).permit(
      :title,
      :content,
      { members_attributes: [:id] }
    )
  end
end

これで終わったかに見えるのですが、実は終わってません…。
@memoを保存する前に、memoにmembersのデータが保存されていることを確認しにいってしまい、エラーが発生します。
@memoはまだ保存されていないので、membersがあるはずもなく、落ちる)

Memoモデルのmembers_attributes=の上書き

これは、members_attributesのデータを元にmemo.membersにデータを反映する過程で発生します。そのため、members_attributes=メソッドを上書きします。(members_attributes=メソッドはhas_manyの設定により、ActiveRecordにより、動的に作られるメソッドです。)

Memoモデル
class Memo < ApplicationRecord
  # (略)

  def members_attributes=(attributes)
    self.members = User.where(id: attributes.values.pluck('id')
    super
  end

  # (略)
end

これにより、中間テーブルの存在を薄くしてシンプルにMemoとUserのやりとりができるようになりました!

まとめ

polymorphicなデータを扱うときは、リレーションの設定が複雑になるのでハマりやすいですが、設定さえすれば割と綺麗に実装できました。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3