ActiveHashを使ってRailsで区分値を扱う方法


2014年 01月 12日

Railsでの区分値の扱いについて考える の続きです。

区分値情報をDBに保存しておくか、アプリにのみ保存しておくのか、悩ましい所です。
DBに区分値を保存しておくと、ActiveRecordなオブジェクトになって扱いやすいという利点があります。
しかし、DBにもアプリにも区分値の情報(ProductTypeの1はLADIESであるといった情報)を持つ事になり、二重管理の状態となる可能性があります。

一カ所変えたら対になるもう一方の修正もしないといけない、という状態は、システム保守の観点からはよろしくありません。
私は過去にアプリ側に区分値情報を更新したのに、DB側に区分値情報を入れ忘れていた! という失敗を体験しました。

区分値情報をアプリ、DB両方に持ってるのはやめたい。でもActiveRecordライクなオブジェクトで区分値を扱いたい。
ActiveRecordのデータソースがclass内に定義したhashとかファイルになっていればいいのに。そんなときに利用するのがActiveHashです。

今回はActiveHashを使って区分値を扱う方法について説明します。
利用するRailsのバージョンは4.0.2です。

ActiveHashのインストール

Gemfileに

gem 'active_hash'

と追記して、bundle install。
これでActiveHashが利用できるようになります。

ActiveHashの使い方

試しに、区分ProductTypeをActiveHashで定義してみます。

class ProductType < ActiveHash::Base
  self.data = [
    {id: 1, name: 'レディース', alias_name: '女性用'},
    {id: 2, name: 'メンズ', alias_name: '男性用'},
    {id: 3, name: 'キッズ', alias_name: '子供用'},
  ]
end

これでActiveRecordのように、allメソッドが利用できます。

[11] pry(main)> ProductType.all
=> [#<ProductType:0x007fdc88f287f0
  @attributes={:id=>1, :name=>"レディース", :alias_name=>"女性用"}>,
 #<ProductType:0x007fdc88f28408
  @attributes={:id=>2, :name=>"メンズ", :alias_name=>"男性用"}>,
 #<ProductType:0x007fdc88f28048
  @attributes={:id=>3, :name=>"キッズ", :alias_name=>"子供用"}>]

first, last, count, where なんかも利用できます。

[11] pry(main)> ProductType.all
=> [#<ProductType:0x007fdc88f287f0
  @attributes={:id=>1, :name=>"レディース", :alias_name=>"女性用"}>,
 #<ProductType:0x007fdc88f28408
  @attributes={:id=>2, :name=>"メンズ", :alias_name=>"男性用"}>,
 #<ProductType:0x007fdc88f28048
  @attributes={:id=>3, :name=>"キッズ", :alias_name=>"子供用"}>]

[12] pry(main)> ProductType.first
=> #<ProductType:0x007fdc88f287f0
 @attributes={:id=>1, :name=>"レディース", :alias_name=>"女性用"}>

[13] pry(main)> ProductType.last
=> #<ProductType:0x007fdc88f28048
 @attributes={:id=>3, :name=>"キッズ", :alias_name=>"子供用"}>

[14] pry(main)> ProductType.count
=> 3

[15] pry(main)> ProductType.where(alias_name: '子供用')
=> [#<ProductType:0x007fdc88f28048
  @attributes={:id=>3, :name=>"キッズ", :alias_name=>"子供用"}>]

関連を定義する

以下のようなProductモデルがあるとします。product_type_id には ProductTypeのidが入っています。

# == Schema Information
#
# Table name: products
#
#  id              :integer          not null, primary key
#  name            :string(255)
#  product_type_id :integer
#

class Product < ActiveRecord::Base
end

ActiveHash::Associationsを使うと、ProductからProductTypeのbelongs_toを定義することができます。

class Product < ActiveRecord::Base
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :product_type
end

これでProductTypeにも簡単にアクセスできます。

[29] pry(main)> Product.all
  Product Load (0.4ms)  SELECT `products`.* FROM `products`
  => [#<Product id: 1, name: "カシミアのセーター", product_type_id: 1, 
      created_at: "2014-01-01 06:59:44", updated_at: "2014-01-01 12:54:05">,
      #<Product id: 2, name: "XXXブランドのスーツ", product_type_id: 2, 
      created_at: "2014-01-01 06:59:51", updated_at: "2014-01-01 12:54:36">]

[30] pry(main)> Product.first.product_type
  Product Load (0.8ms)  SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
  => #<ProductType:0x007fdc890174e0
  @attributes=
  {:id=>1, :product_type=>"Ladies", :name=>"レディース", :alias_name=>"女性用"}>

[31] pry(main)> Product.first.product_type.name
  Product Load (0.5ms)  SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
  => "レディース"

区分値にenumのようにアクセスする

指定した商品が女性ものかどうかを判定したいとき、以下のようなコードが書きたいです。

product = Product.first
if (product.product_type == ProductType::LADIES)
  # 女性用の商品の場合の処理
end

この場合、ActiveHashのenum_accessorを利用します。

class ProductType < ActiveHash::Base
  include ActiveHash::Enum

  self.data = [
    {id: 1, product_type: 'Ladies', name: 'レディース', alias_name: '女性用'},
    {id: 2, product_type: 'Mens', name: 'メンズ', alias_name: '男性用'},
    {id: 3, product_type: 'Kids', name: 'キッズ', alias_name: '子供用'},
  ]

  enum_accessor :product_type
end

ProductType::LADIES みたいにアクセスできるようにするため、dataにproduct_type を追加し、enum_accessorの定義を追加しました。
ProductType::Ladies ではアクセスできません。enum_accessorを使うと、必ずProductType::MENSのような大文字でのアクセスとなります。

[8] pry(main)> ProductType::LADIES
=> #<ProductType:0x007f3d6e6f6118
 @attributes=
  {:id=>1, :product_type=>"Ladies", :name=>"レディース", :alias_name=>"女性用"}>

これで、product.product_type == ProductType::LADIES といった比較も可能となります。

[12] pry(main)> Product.all
  Product Load (0.4ms)  SELECT `products`.* FROM `products`
  => [#<Product id: 1, name: "カシミアのセーター", product_type_id: 1, 
      created_at: "2014-01-01 06:59:44", updated_at: "2014-01-01 12:54:05">,
      #<Product id: 2, name: "XXXブランドのスーツ", product_type_id: 2, 
      created_at: "2014-01-01 06:59:51", updated_at: "2014-01-01 12:54:36">]

[13] pry(main)> Product.first.product_type == ProductType::MENS
  Product Load (0.8ms)  SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
=> false

[14] pry(main)> Product.first.product_type == ProductType::LADIES
  Product Load (0.4ms)  SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
=> true

区分値の定義をYAMLに切り出す

最後は、データソースをYAMLにする方法を紹介します。

ProductTypeモデルの中にデータがべた書きされているのは、なんだか見た目によくありませんね。
そんなときは、データソースをYamlにできる、ActiveYamlを利用します。

Rails.root の config/master/product_type.yml に以下のようなデータを用意します。

- id: 1
  product_type: Ladies
  name: 'レディース'
  alias_name: '女性用'
- id: 2
  product_type: Mens
  name: 'メンズ'
  alias_name: '男性用'
- id: 3
  product_type: Kids
  name: 'キッズ'
  alias_name: '子供用'

ProductTypeはActiveYamlを利用して、以下のように定義します。

class ProductType < ActiveYaml::Base
  include ActiveHash::Enum

  set_root_path "config/master"
  set_filename "product_type"

  enum_accessor :product_type
end

set_root_pathには読み込みたいyamlが存在するディレクトリを、set_filenameにはyamlのファイル名を記述します。

使い方はActiveHash::Baseで定義していたときと変わりません。
これでモデル内にベタッと書いていたデータを外出しすることができました。

ActiveHashでは、この他ActiveFileを使ってデータソースを普通のファイルにできたりします。
詳しくは、 https://github.com/zilkey/active_hash を読んでください。

ActiveHashで区分値を扱うのは、良いのか、悪いのか。

今回はActiveHashを利用して区分値を扱う方法を紹介しました。
しかし、区分値にActiveHashを使うのが良いのか悪いのか、正直な所わかっておりません。

ProductTypeのyamlのalias_nameに ‘女性用’ とか文字列をベタうちしているわけですが、これは本来localesのja.yml に書いておくべきなんじゃないか、とは思っております。これでは他言語化対応できないですしね。
しかし、product_type.yml 内から locales/ja.yml を呼ぶのもおかしな話だし。。。うむー。

区分値の扱いについて、これだという結論は今の所でておりません。

Rails4.1 からは、ActiveRecordでenumを扱うための仕組みが導入されます。区分値管理はenumを使った方がシンプルでよいのかもしれません。ただ、Rails4.1のenumでも、日本語をどのように扱ったらよいのかはよくわかりません。
Rails4.1 の enumについては https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb を参考にしてください。

皆様はRailsで区分値をどのように扱っているのでしょうか?