リソースとテーブルのギャップを飛び越える

2010年10月20日(水)
川尻 剛

テーブル・ビューで実現する

複数のテーブルを参照するSQLの機能として、誰もが思いつくのは、テーブル・ビューです。幸い、ActiveRecordでは、対応するデータがテーブル・ビューであっても、通常通りに動作します。ここでは、販売に対するテーブル・ビューを作成して、それに対応するActiveRecordクラスを定義してみます。

テーブル・ビューは、コンソール上でも作成できますが、Migrationからでも作成できます。次のように、executeメソッドで指定することで作成できます*1

  • [*1] ただし、executeでは、schema.rb(現在のテーブル構成を記録するもの)には反映されません。テスト実行時には工夫が必要です。
01<!--//--><![CDATA[// ><!--
02 
03class CreateShopSales < ActiveRecord::Migration
04  def self.up
05    execute <<-SQL
06      CREATE VIEW shop_sales AS
07        SELECT st.id AS id
08          , st.item_id
09          , st.unit
10          , sp.address
11          , sp.delivered_on
12        FROM
13          shop_stocks AS st, shop_shipments AS sp
14        WHERE
15          st.id = sp.stock_id ;
16    SQL
17  end
18  def self.down
19    execute <<-SQL
20      DROP VIEW shop_sales ;
21    SQL
22  end
23end
24 
25//--><!

あとは、作成したテーブル・ビュー(shop_sales)に対応するActiveRecordを、ほかのモデルと同じ手順で定義するだけです。ただし、保存処理は、独自に定義する必要があります。保存処理の実装例を、次に示します。

01<!--//--><![CDATA[// ><!--
02 
03module Shop
04  def self.table_name_prefix
05    'shop_'
06  end
07 
08  class TableView::Sale < ActiveRecord::Base
09    belongs_to :item, :class_name => 'Shop::Physical::Item'
10    validates_presence_of :item_id, :unit, :address
11 
12    def save(validate = true)
13      return false unless validate ? valid? : true
14      self.class.transaction do
15        item = Physical::Item.find(item_id)
16        if Physical::Stock.over?(item.id, unit)
17          raise ActiveRecord::RecordInvalid.new(self)
18        end
19        stock = Physical::Stock.create(:item_id => item, :unit => -unit)
20        Physical::Shipment.create(:stock_id => stock, :address => address)
21        Physical::Proceed.create(:amount => stock.unit * item.price)
22      end
23      self.id = stock.id
24      return self
25    end
26 
27  end
28end
29 
30//--><!

保存処理は、トランザクションを使用し、失敗した時にロール・バックするようにしました。

ここで、論理モデルの話からは少し離れますが、名前空間について補足しておきます。上記のコードは、次の図4に示すように、modelsディレクトリに分けて配置しています。

図4: 各ファイルの配置

shopで分けた理由は、モデル名の重複を避けることと、分離しやすくすることです。例えば、Shop以下をプラグイン化して再利用する場合、Shop以外のコンポーネントからは、次のように、モジュール付きで呼び出す必要があります。これにより、コード上から依存関係を見つけやすくなります。

1<!--//--><![CDATA[// ><!--
2 
3# Shop::SalesControllerから呼び出し
4TableView::Sale.find(params[:id])
5# Xxx::YyyControllerから呼び出し
6Shop::TableView::Sale.find(params[:id])
7 
8//--><!

同様の理由から、テーブル名にもshopという接頭語を付けて定義します。これは、上記のコードにあるように、table_name_prefixメソッドを使用することによって自動的に、テーブル名に付加されてマッピングされます。これらは、一般的によく使われるテクニックなので、覚えておくと良いでしょう。

さらに、今回は、実テーブルに対応するモデルをphysicalディレクトリに配置し、テーブル・ビューに対応するモデルをtable_viewディレクトリに配置しました。これは、使えるAPIが双方で一部異なることを、モジュール名の違いによって開発者に示す狙いがあります。

それでは、定義したモデルを、コンソールから操作してみましょう。

01<!--//--><![CDATA[// ><!--
02 
03> Shop::TableView::Sale.all
04 => [#<Shop::TableView::Sale id: 1, item_id: 1, unit: 2, address: "Tokyo", delivered_on: nil>, #<Shop::TableView::Sale id: 2, item_id: 2, unit: 3, address: "Sapporo", delivered_on: nil>]
05> Shop::TableView::Sale.find(1)
06 => #<Shop::TableView::Sale id: 1, item_id: 1, unit: 2, address: "Tokyo", delivered_on: nil>
07> Shop::TableView::Sale.find(1).item
08 => #<Shop::Physical::Item id: 1, name: "apple", price: 100>
09 
10> sale = Shop::TableView::Sale.new
11> sale.save
12 => false
13> sale.errors
14 => #<OrderedHash {:item_id=>["can't be blank"], :address=>["can't be blank"]}>
15 
16> Shop::TableView::Sale.new(:item_id => 1, :unit=>1, :address=>'Sapporo').save
17 => #<Shop::TableView::Sale id: 3, item_id: 1, unit: 1, address: "Sapporo", delivered_on: nil>
18 
19//--><!

コントローラ側で呼び出すような処理が、正常に動作していることが分かります。scaffoldが生成するような、シンプルで美しいコントローラを実装できそうです。また、販売に関連するロジックも、ほかのモデルから分離させて、本クラスに集約できそうです。

株式会社日立ソリューションズ

技術開発本部 Rubyセンタ所属
1977年生まれ。入社後、自社フレームワークの開発を経てRIAの調査などに関わる。
最近は「お客様とチームがより幸せなるマネジメント」を目標としてプロジェクトマネジメントを勉強中。著書に「Java開発者のためのAjax実践開発入門」(共著)など。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています