Railsアジャイル本の疑問
Railsアジャイル本(と勝手に略)の開発パートを読み終えて、実際にコーディングして良い感じのオンラインストアを完成させたのだが、一つだけ疑問が残っている。
モデル間のリレーションの話なのだが、このオンラインストアでは注文に関係するモデルを3つ定義している。商品(Product)と品目(LineItem)、注文(Order)だ。
Productは複数のLineItemを持っている(has_many)。これはつまり、Productは複数のLineItemから参照されるということを意味する。もちろんその逆に、個々のLineItemはそれぞれ任意のProductを指している(belongs_to)。
一方Orderの方はというと、こちらも複数のLineItemを持っている(has_many)。持っていると言うよりは、Orderの中には複数のLineItemが含まれていると言った方がわかりやすいか。逆(belongs_to)もディレクティブ宣言されている。
つまり、Product-LineItem-Order間には、多対一、一対多の関係が出来上がっている。なぜLineItemを設けるのかがわからない。Product-Order間で、多対多の関係を作れば良いのではないのか。LineItemを挟むメリットとしては、店員が売れ筋商品を調べるための機能を加えるときに、少しだけ実装が楽になる(LineItemのテーブルから商品IDで検索し、カウントする等)ぐらいではないのか。しかし同じようなことを、Product、Orderの2つだけでも実現できる(全ての注文をなめて、商品ごとにそれぞれカウントしていくしかないが)。
なぜ疑問に思ったかというと、今実際にコーディングしている自分のアプリケーションでも同様のモデルを実装しようとしていたからだ。先日配信もしたが、ProSteamerをRailsで書き直そうとしている。
簡単に言うと、まず登録ユーザのモデル(User)と、Steamのゲーム情報のモデル(Game)を用意しておく。あるユーザは複数のゲームを持っていると考えられる。一方、あるゲームも複数のユーザに遊ばれている。つまりUser、Game共に、has_and_belongs_to_manyと宣言しようとしていた。
だが、どうなんだろう…。新たにGameItemなるモデルを作って、先ほどのLineItemのように多対一、一対多の関係を作った方が良いのか…。
読み進めてたら答えが書いてあったので追記。
まず勘違いしてたことから。多対多の関係を作る場合でも、外部キーの組を保存するテーブルを作らないとダメなようだ。つまりUserモデル(usersテーブル)と、Gameモデル(gameテーブル)の例で言うと、games_usersという結合テーブルを作らないといけないらしい。gamesとusersはアルファベット順で。migrationファイルを記述するとしたら
def self.up create_table :games_users, :id => false do |t| t.integer :game_id t.integer :user_id end end
こんな感じで。{:id => false}を付けることで、このテーブルには主キーを設定しないようにする。また本書が勧めているように外部キー制約を付けるも良し。てっきり、usersテーブルとgamesテーブルにそれぞれの外部キーを持たせるんだと思ってた。
これだけで良いのに、じゃあなんで本書ではLineItemなる新たなモデルを作って、多対一、一対多の関係を作ったか。それは関係に何か特別な意味を持たせるため。例えば、userはgameを所持しているわけだが、そのgameに対する評価(rating)という属性も保存しておきたいとしよう。当然userは自分が持っているgameに対してしか評価できない。この場合はRatingのようなモデルを作り、さらにusersとgamesの外部キーも持つようにする。
def self.up create_table :ratings do |t| t.integer :rating t.integer :game_id t.integer :user_id end end
この場合は主キーは持たせる。外部キーを持っているけど、普通のテーブル。これを結合テーブルとして使うには、
class Game << ActiveRecord::Base has_many :ratings has_many :users, :through => :ratings end class User << ActiveRecord::Base has_many :ratings has_many :games, :through => :ratings end class Rating << ActiveRecord::Base belongs_to :game belongs_to :user end
このように:throughオプションを使うことで、結合テーブルを使った多対多のように
user = User.find_by_id(1) user.games.each do |g| puts g.title end
と書いて、所持リストを表示できる。
あーすっきり。