Railsでの区分値の扱い、皆様どのようにしておられるでしょうか?
区分値とは、例えば性別情報(1: MALE, 2: FEMALE)とか、服を扱っているシステムの場合は商品種別(1: LADIES, 2: MENS, 3: KIDS)の事を指します。
私は区分値情報をDBに保存しておこうか、アプリ側でのみもっておこうか、毎回悩まされます。
区分値をDBに保存しておくと、外部キー制約もつけられるしActiveRecordでも扱いやすいといったメリットがあります。
しかし、アプリとDB両方に区分値情報を持っているとデータの二重管理になってしまいます。
DB側の区分値とアプリ側の区分値が食い違ってる! なんていう事態も発生します。
ならば、いっその事DBに保存するのはやめて、区分値情報をアプリ側にのみ持っていた方がよいのでは、というのが最近の私の考えです。
今回はRailsで区分値を扱う方法について考えてみます。
区分値をアプリで保持するとして、どうやって定義したらよいでしょうか。moduleを使って、
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
end
みたいな感じで定義してみましょう。
商品がレディースかどうか判定したい場合は、
if(product.product_type == ProductType::LADIES)
# レディース商品の処理を書く
end
で判定可能です。moduleと定数を組み合わせる事で、C言語でいうEnumのようにアクセスできます。
さて、商品種別の全てのIDを取得したい場合、どのような実装をするでしょうか。
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
def self.all
[LADIES, MENS, KIDS]
end
end
上記のようなall関数を定義すれば、
[29] pry(main)> ProductType.all
=> [1, 2, 3]
で、すべての区分値情報が取得できますが、いけてない実装ですね。
区分値をひとつ追加したら、allも変更しないといけない。allを変更し忘れてて、全ての区分値とれてなかった、なんていう事態になるわけです。こういう二重管理は極力無くしたいものです。
ということで、以下のように実装します。
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
def self.all
self.constants.map { |v| const_get(v) }
end
end
constantsでmoduleに定義されている定数が取得できます。これで二重管理の問題はなくなりました。
次に、ProductTypeの定数に対応する名前があり、その名前の情報をIDから引いてきたい場合、どうしましょうか。
例えば、ProductType::LADIESを、対応する日本語名「レディース」に変換したい場合です。
試しに次のように実装してみました。
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
NAME = { LADIES => 'レディース', MENS => 'メンズ', KIDS => 'キッズ' }
def self.all
self.constants.map { |v| const_get(v) }
end
end
ProductType::LADIESをレディースに変換したい場合は、以下のように記述します。
[49] pry(main)> ProductType::NAME[ProductType::LADIES]
=> "レディース"
なんだか微妙ですね。しかも重大なミスがありました。
[47] pry(main)> ProductType.all
=> [1, 2, 3, {1=>"レディース", 2=>"メンズ", 3=>"キッズ"}]
あれ、allの定義がおかしくなってる! しょうがない、以下のように修正するか。
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
NAME = { LADIES => 'レディース', MENS => 'メンズ', KIDS => 'キッズ' }
ALL = [LADIES, MENS, KIDS]
def self.all
ALL
end
end
二重管理問題復活ですね。
さらに、LADIESは女性用、MENSは男性用、といった具合に、名前の別名をつけたい、という要件がでてきたとします。
module ProductType
LADIES = 1
MENS = 2
KIDS = 3
NAME = { LADIES => 'レディース', MENS => 'メンズ', KIDS => 'キッズ' }
ALIAS_NAME = { LADIES => '女性用', MENS => '男性用', KIDS => '子供用' }
ALL = [LADIES, MENS, KIDS]
def self.all
ALL
end
end
まあこんな感じになりますよね。
さらにさらに、次のような商品モデルがあるとします。
# == Schema Information
#
# Table name: products
#
# id :integer not null, primary key
# name :string(255)
# product_type_id :integer
#
class Product < ActiveRecord::Base
end
product_type_id には 1,2,3 といったProductTypeのID情報が入っています。
画面に以下のような商品一覧を表示したい場合は、どうやって実装しましょうか。
Productモデルに以下のような関数を定義して、商品種別を表示できるようにしましょう。
class Product < ActiveRecord::Base
def product_type_name
ProductType::NAME[self.product_type_id]
end
end
さらにさらにさらに、先ほど定義したProductTypeのalias_name(LADIESは女性用といったデータ)を引いてきたい場合は、def product_type_alias_name とか定義することになります。。。
ここまで来て、ふと思うわけです。
あれ、ProductTypeめちゃくちゃ扱いにくくないか、と。
毎回毎回、わざわざこんなに関数定義しないといけないわけ!?
もしProductTypeがActiveRecordオブジェクトなら、ですよ。
# == Schema Information
#
# Table name: product_types
#
# id :integer not null, primary key
# name :string(255)
# alias_name :string(255)
#
class ProductType < ActiveRecord::Base
end
と定義しておいて、DBに値をいれておけば、
ProductType.all
で全ての商品種別が取得できます。わざわざallを自前で実装するなんてことないわけです。
Productに定義したproduct_type_name も
class Product < ActiveRecord::Base
belongs_to :product_type
end
と定義しておけば、商品の商品種別を取得したい場合も、
product = Product.first
product.product_type.name # レディース
product.product_type.alias_name # 女性用
と表示できるわけです。ActiveRecordオブジェクトになっていた方が、圧倒的に使いやすいですね。
もう区分値DBにのみいれといた方がいいんじゃないか。
という結論に達したとき。
次のコードはどうやって書いたらいいのでしょうか。
if(product.product_type == ProductType::LADIES)
# レディース商品の処理を書く
end
うん、かけないね。
しょうがない、区分値をDBに入れておき、アプリ側にもmoduleで定義しておこう!としたのが、私が前関わってたプロジェクトでの出来事です。アプリとDB両方で区分値情報を持っている状態です。
で、長い間改良を加えていると、アプリでは区分値追加してたのにDBに区分値情報入れ忘れてた! とかいう事故も起こるわけです。
区分値情報をアプリ、DB両方に持ってるのはやめたい。でもActiveRecordライクなオブジェクトで区分値を扱いたい。
ActiveRecordのデータソースがclass内に定義したhashとかファイルになっていれば、完璧なんだけどな。
そんなときに利用するのが ActiveHash です。
ActiveHashを利用すれば、
class ProductType < ActiveHash::Base
self.data = [
{id: 1, name: 'レディース', alias_name: '女性用'},
{id: 2, name: 'メンズ', alias_name: '男性用'},
{id: 3, name: 'キッズ', alias_name: '子供用'},
]
end
のように定義しておくだけで、ActiveRecordライクにProductTypeにアクセスできるようになります。
Productモデルでbelongs_toもできます。
ProductType::LADIES みたいにアクセスできるようにもなります。
今回は長くなってしまったのでここまで。
次回ActiveHashの使い方を紹介します。