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つだけでも実現できる(全ての注文をなめて、商品ごとにそれぞれカウントしていくしかないが)。


なぜ疑問に思ったかというと、今実際にコーディングしている自分のアプリケーションでも同様のモデルを実装しようとしていたからだ。先日配信もしたが、ProSteamerRailsで書き直そうとしている。

簡単に言うと、まず登録ユーザのモデル(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

と書いて、所持リストを表示できる。


あーすっきり。