Ruby
Rails
ransack

ransack、フォーム作成時にmatcherが同名になった時の解決方法

ransackでフォームフィールド名が同じになってしまう問題

まず起きることはないが、どんな状況で起き得るか

Userモデル
name: String # 本名
profile_name: String # サイト上で表示する名前
......
Profileモデル
name: String # いろいろな経緯があって生まれてしまったモデルの名前
......

この状況でransackのフォームを作成すると問題が起きる

  • どちらも profile_name_cont になってしまうのだ。
ransack_form.slim.erb
= f.label :profile_name_cont, 'サイト上の表示名(user.profile_name)'
= f.check_box(:profile_name_cont)

= f.label :profile_name_cont, 'Profileの名前(user.profile.name)'
= f.check_box(:profile_name_cont)

この場合 同じクラスのメソッドが優先されるので検索されるのは

user.profile_name だけであり、user.profile.name の検索は行われない。

結論:処理としては User モデルの profile_name カラムの検索だけちゃんと動作する。

2通りの解決方法

  • 設計ミスなのでカラム名を変更する、おそらくこれがもっとも良い解決方法。
  • user.profile.nameは ransack を使わずに検索を実装、検索結果を結合する。

検索結果を結合する場合の実装方法

user.profile.name の検索は ransack に載せることができないので
別で検索処理を作成して `結果を結合merge` する戦法をとった

検索結果のマージ

基本的な部分は省略するが、ransackと通常の検索結果の merge はこんな感じでできる。

merge
result = User.ransack(ransack_params).result.order(id: :desc)
result.joins(:profile).merge(Profile.where(name: "ほげ"))

試してみたがダメだった delegateransackable_attributes を使った実装

ransackable_attributes で検索可能な要素一覧を表示できる

User.ransackable_attributes

これ毎に Search Matchers が利用可能なので、ここに profile_dot_name を追加
そのキーワードでも検索できるようにしてみた。

  def self.ransackable_attributes(auth_object = nil)
    %w(name profile_name profile_dot_name)
  end

profile_dot_nameで検索した際に Profile モデルの nameを参照するように
delegateを定義。

user.rb
delegate :name, to: :profile, prefix: :profile_dot

結論:ransackが直接SQLを発行してしまうため、delegateで定義した profile_dot_name は利用できないのである。

Mysql2::Error: Unknown column 'users.profile_dot_name' in 'where clause': SELECT  `users`.* FROM `users` WHERE 
.....

ちなみに結構ライブラリの中身書き換えてエラー出ないようにしてみたものの最終的にここで詰まって死亡しました