データベースにテーブルを作らずに ActiveRecord を利用する

ActiveRecord 向けのビューヘルパーメソッドを使いたい

複雑な検索条件などをフォーム上で表現するとき、ページのリロード時の値の保存や値のパースに便利なように、モデルクラスを利用したいときがあるかもしれない。たとえば、

<%= select 'search_cond', 'demographic', [["10歳未満", 1], ["10代", 2], ["20代", 3], ["30歳以上", 4]] %>

などとしたいのである。問題は当然ながら search_conds などというテーブルがデータベースに存在しないことである。text_field や select など *_tag というポストフィックスが付かないビューヘルパメソッドはすべて、ActiveRecord::Base オブジェクトの存在を前提としている。どうしたらいいだろうか?

ActiveRecord をどう騙すか?

一つの戦略は、ビューヘルパメソッドを使う範囲において、ActiveRecord があたかも search_conds というテーブルがあるかのように振舞うようにする、というものだ。ビューヘルパにとっては、ActiveRecord オブジェクトの属性にアクセスできれば満足である。そして、ActiveRecord の属性は、型キャストのためにテーブルのカラム情報を参照する。これが狙い目である。

よくよく調べてみると、ActiveRecord::Base.columns というメソッドが ActiveRecord::ConnectionAdapters::Column(のデータベース固有のサブクラス)の インスタンスの配列を返している。ここで、適当なカラムの配列を返してやれば、ActiveRecord はデータベースから取得したカラム情報だと勘違いしてくれるだろう。

実装

結論から言うと次のようにすればよいようだ。

class SearchCond < ActiveRecord::Base
  include ActiveRecord::ConnectionAdapters

  t = @@table_definition = TableDefinition.new(ActiveRecord::Base.connection)
  t.column :name, :string
  t.column :demographic, :integer
  t.column :written_at, :datetime

  def self.columns
    @columns ||= @@table_definition.columns.map do |c|
      Column.new(c.name.to_s, c.default, c.sql_type, c.null)
    end
  end
end

上の例では、これで SearchCond は name:string, demographic:integer, written_at:datetime というカラムをもつ search_conds というテーブルが存在するかのように振舞う。(もちろんこれは SearchCond.columns についてだけである。SearchCond.find とか実行すれば、当然エラーになる)TableDefinition オブジェクトを使っているので、マイグレーションファイルと書式は同一である。

きちんと確認はとれていないが、実は SearchCond#valid? なども動くみたいだ。フォームの検証用にも使えるかもしれない。きちんと属性の型キャストが行われている分、ActiveForm プラグインよりマシかもしれない。