Railsでの区分値の扱いについて考える の続きです。
区分値情報をDBに保存しておくか、アプリにのみ保存しておくのか、悩ましい所です。
DBに区分値を保存しておくと、ActiveRecordなオブジェクトになって扱いやすいという利点があります。
しかし、DBにもアプリにも区分値の情報(ProductTypeの1はLADIESであるといった情報)を持つ事になり、二重管理の状態となる可能性があります。
一カ所変えたら対になるもう一方の修正もしないといけない、という状態は、システム保守の観点からはよろしくありません。
私は過去にアプリ側に区分値情報を更新したのに、DB側に区分値情報を入れ忘れていた! という失敗を体験しました。
区分値情報をアプリ、DB両方に持ってるのはやめたい。でもActiveRecordライクなオブジェクトで区分値を扱いたい。
ActiveRecordのデータソースがclass内に定義したhashとかファイルになっていればいいのに。そんなときに利用するのがActiveHashです。
今回はActiveHashを使って区分値を扱う方法について説明します。
利用するRailsのバージョンは4.0.2です。
Gemfileに
gem 'active_hash'
と追記して、bundle install。
これで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
=> "レディース"
指定した商品が女性ものかどうかを判定したいとき、以下のようなコードが書きたいです。
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にする方法を紹介します。
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を使うのが良いのか悪いのか、正直な所わかっておりません。
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で区分値をどのように扱っているのでしょうか?