【Rails】静的データのモデル化にactive_hash gemが便利・・!

Railsアプリを作っていて、画面で更新する必要のないデータを扱う時って意外と多い気がします。そんな時は、fixtureからActiveRecordオブジェクトとして読み込んで扱うのが一般的なやり方と思います。ただ、この時にmigrationを作るのやDBカラムの管理作業(rake db:seedとか)がなんとなく煩雑でもっといいやり方ないかなー、と思ってました。かれこれ半年ほど。

ところが、先日見つけたactive_hashというgemを使うと、このような場合にDBテーブルを作ることなくyamlやJSON、RubyのハッシュをActiveRecordライクに扱えて、更にオブジェクトにhas_many等の関連付もできてライフチェンジングなのです。。!

DBスキーマを決めるのって結構慎重さを要するので、このActiveHashを使ってさっくりとモデル実装→必要になった時にActiveRecord化という開発ができて気持ちよさそう。

これぞ求めていたソリューションや〜。というわけで、active_hashのご紹介です。

リポジトリはこちら→ active_hash

[追記:2014-11-08]

この記事active_hashの項目にもあるように、ActiveRecordでいうjoinsincludesによる複数テーブルにまたがる検索を行おうとするとactive_hashでは無理ゲーに近いので、素直にActiveRecordを使うことをおすすめします。

[\追記]

基本的な使い方

モデル定義

まず、モデルを定義するには次のようにします(READMEより抜粋)。

class Country < ActiveHash::Base
  self.data = [
    {:id => 1, :name => "US"},
    {:id => 2, :name => "Canada"}
  ]
end

self.dataにハッシュの配列を入れてやる感じですね。こうすれば、後はActiveHashがよしなにフィールドを作ってくれます。 また、addクラスメソッドを用いて次のようにも定義できます。

class Country < ActiveHash::Base
  field :name
  add :id => 1, :name => "US"
  add :id => 2, :name => "Canada"
end

この場合、addする前にfield(複数の場合はfields)メソッドを使ってモデルのデータフィールドを宣言しておく必要があります。 僕はこの形式が好みです。

同様に create メソッドを使うこともできます。

class Country < ActiveHash::Base
  field :name
  create :id => 1, :name => "US"
  create :id => 2, :name => "Canada"
end

インスタンスの取得

ActiveRecordと同じようなメソッドが使えます(完全に同じというわけではないので注意)。

Country.all             # => すべての Country オブジェクトを返します
Country.count           # => .data 配列の長さを返します
Country.first           # => 最初の country オブジェクトを返します
Country.last            # => 最後の country オブジェクトを返します
Country.find 1          # => id の一致する _最初_ の Country オブジェクトを返します
Country.find [1,2]      # => id の一致する _すべて_ の Country オブジェクトを Array で返します
Country.find :all       # => .all と同様です
Country.find :all, args # => 2番目の引数は無視されますが、ActiveRecordとの互換のために有用です
Country.find_by_id 1    # => id の一致する _最初_ の Country オブジェクトを返します
Country.where(name: "US") # => 条件に当てはまる _すべて_ のCountryオブジェクトをArrayで返します

フィールド名による取得もActiveRecordのように豊富なメソッドはありませんが可能です(これらのメソッドは動的に定義されています)。

Country.find_by_name "foo"                    # => name の一致する _最初_ の Country オブジェクトを返します
Country.find_all_by_name "foo"                # => name の一致する _すべて_ の Country オブジェクトを Array で返します
Country.find_by_id_and_name 1, "Germany"      # => id と name の一致する最初のオブジェクトを返します
Country.find_all_by_id_and_name 1, "Germany"  # => id と name の一致するすべてのオブジェクトを Array で返します

インスタンスのメソッド

取得したインスタンスでは次のようなメソッドが使えます。

Country#id          # => id か nil
Country#id=         # => id 属性をセット
Country#quoted_id   # => id を数値で返します
Country#to_param    # => id を文字列で返します
Country#new_record? # => Country.all で取得したデータに含まれれば true 含まれなければ false
Country#readonly?   # => 常に true (静的データなので!)
Country#hash        # => id のハッシュ値 (または nil のハッシュ値)
Country#eql?        # => オブジェクトの種類と id を比較します。 id が nil の場合は false

ユーザーが定義したフィールドのアクセサなども定義されています。

Country#name        # => name フィールドの値を返します
Country#name?       # => name が存在していれば true
Country#name=       # => name をセット

関連付け

次の様に、ActiveHashオブジェクト間の関連を定義できます。has_manybelongs_tohas_one 等、ActiveRecordで使えるものなら大体使えると思います。

class Country < ActiveHash::Base
  include ActiveHash::Associations
  has_many :people
  field :name
  add id: 1, name: 'US'
  add id: 2, name: 'Canada'
end

class Person < ActiveHash::Base
  include ActiveHash::Associations
  belongs_to :country
  fields :name, :gender

  add id: 1, name: 'John', gender: :male, country_id: 1
  add id: 2, name: 'Mike', gender: :male, country_id: 1
  add id: 3, name: 'Amy', gender: :female, country_id: 2
end

country = Country.first
people = country.people
people.first.name # => "John"

同様に、ActiveHashとActiveRecordオブジェクト間の関連定義も可能です(READMEを参照)。ただし、ActiveRecordオブジェクトの子として複数のActiveHashのオブジェクトを持つことはできません。これは、ActiveHashオブジェクトの定義にDBのActiveRecordオブジェクトのidをハードコードすることになり、依存関係の逆転が起こってしまうためのようです。

Yamlをロードして、データに使う

active_hash の ActiveYaml を使って、データの補填にYamlを使えます。

class Country < ActiveYaml::Base
  set_root_path Rails.root.join('db/fixtures')
  set_filename 'countries'

  include ActiveHash::Associations
  has_many :people
end

class Person < ActiveYaml::Base
  set_root_path Rails.root.join('db/fixtures')
  set_filename 'people'

  include ActiveHash::Associations
  belongs_to :country
end

複数のYamlからデータを読み込むには、次のようにします。

class Country < ActiveYaml::Base
  use_mutliple_files
  set_filenames "europe", "america", "asia", "africa"
end

上の用な感じで、Yamlでデータを定義できます。関連定義も普通に使えます。

db/fixtures/countries.yml

- id: 1
  name: US
- id: 2
  name: Canada
- id: 3
  name: Mexico

us:
  id: 1
  name: US
canada:
  id: 2
  name: Canada
mexico:
  id: 3
  name: Mexico

db/fixtures/countries.yml

- id: 1
  name: John
  gender: :male
  country_id: 1
- id: 2
  name: Mike
  gender: :male
  country_id: 1
- id: 3
  name: Amy
  gender: :female
  country_id: 1

機能に関連するREADMEの見出し一覧

以上、僕のセレクトで機能を駆け足で紹介してきましたが、他にもたくさんの機能があるのでREADMEを参照してください。機能に関連する見出し一覧を下に書いておきます。