Railsでの区分値の扱いについて考える


2014年 01月 05日

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情報が入っています。
画面に以下のような商品一覧を表示したい場合は、どうやって実装しましょうか。

786628ed-c78a-971d-487f-c134c7c354e8.png

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に区分値情報入れ忘れてた! とかいう事故も起こるわけです。

解決策: ActiveHashを使う

区分値情報をアプリ、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の使い方を紹介します。