Active Record の :include オプション
またまたN+1問題ネタです。
前回の記事で、N+1問題を回避するのにJOIN SQLを使うと、返されるオブジェクトの構造がイケていないということを書きました。
自分で、オブジェクトモデルをつくってみたわけですが、ちゃんとしたやり方がありました。
多くのO-Rマッピングフレームワークでは関連の取得はLAZYに行われますが、N+1問題の状況にあっては、これをEAGERに取得することでSQL発行回数を抑えることができます。返されるオブジェクトもちゃんとしたモデルの構造をしています。
:include オプションを使うと、貪欲に関連先テーブルのデータを取得しに行きます。
Book.all(:include => [:reviews]).each { |book| book.reviews.each { |review| puts review.body } }
このコードで発行されるSQLです。
Book Load (2.0ms) SELECT "books".* FROM "books" Review Load (0.6ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id IN (1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51))
BOOKSテーブルを全件検索した後、関連するREVIEWSテーブルのレコードを貪欲に取得しています。
ただし、取得するデータがあまりに大きい場合は、EAGERな取得はメモリを圧迫します。
メモリと性能のトレードオフということになります。
これは状況に応じて見極めていくしかないですね。
特異クラスの使い方
前の記事の最後に出したコードで特異クラスを使ってます。
def build_book books = Book.joins('LEFT JOIN reviews ON books.id = reviews.book_id'). select('books.id bid, reviews.id rid, reviews.book_id, reviews.body') res = {} books.each { |rec| book = res[rec.bid] if book == nil book = Book.new class << book attr_accessor :reviews end res[rec.bid] = book book.id = rec.bid book.reviews = Array.new end 〜略〜 } res.values end
通常のO-Rマッピングの操作(findなど)でActive Recordが返してくるモデルには、関連先テーブルのデータもフィールドとして含めて返してくれます。
例えば、Bookモデルだったら、BOOKSテーブルと関連するREVIEWSテーブルのデータは、Book.reviews としてモデルを返してくれます。
しかし、今回はBookモデルを自分でnewして、Book.reviewsを持つモデルをつくろうとしてます。
ところが、Bookモデルのクラス定義は以下。
class Book < ActiveRecord::Base has_many :reviews end
自分で普通にnewしたBookモデルでは、reviewsフィールドに値をセットできません。
だからといて、わざわざそのためにクラス定義するのも面倒だし。
そこで特異クラスを使ってみました。 attr_Accessor :reviews を追加定義することで、モデルを再現しています。
本当にこういう使い方でいいのかな?
Active Record で N+1 問題
最近会社でHibernateのN+1問題事例を調べてたんですが、ActiveRecordでも当然のように起こりますね。
BOOKSテーブルが、1対NでREVIEWSテーブルと関連を持っています。(BOOKSが1、REVEWSがN)
以下のコードでは、BOOKSテーブルを全件検索して、それに関連するREVIEWSテーブルのレコードを取得して、REVIEWSテーブルのBODYカラムを出力する。
Book.all.each { |book| book.reviews.each { |review| puts review.body } }
このコードではBOOKSテーブルに対して1回のSQLが発行され、REVIEWSテーブルに対してはBOOKSテーブルのレコード数分のSQLが発行されます。N+1問題です。BOOKSテーブルとREVIEWSテーブルの多重度に関係なく、親テーブルを複数検索して、子テーブルのレコードも取得しようとするとき、O-Rマッピングの通常のやり方だとこうなります。これはHibernateとかも同じ。
Review Load (0.3ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 1) Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 2) Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 3) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 4) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 5) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 6) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 7) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 8) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 9) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 10) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 12) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 13) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 14) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 15) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 16) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 17) Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE ("reviews".book_id = 18)
対策はどうするかというと、馴染みのあるJOIN SQLを呼べばいい。
books = Book.joins('LEFT JOIN reviews ON books.id = reviews.book_id'). select('books.id bid, reviews.id rid, reviews.book_id, reviews.body') books.each { |book| puts book.body if book.body != nil }
発行されるSQLは一発。
SELECT books.id bid, reviews.id rid, reviews.book_id, reviews.body FROM "books" LEFT JOIN reviews ON books.id = reviews.book_id
大手SIerなんかだと、こっちの方を好みます。性能問題が何よりも怖いから。
でも、このやり方だと、SQLで取得する結果がとても扱いにくい。
最初のN+1問題がでるやり方では取得したモデルbookはちゃんとreviewsをフィールドとして持った忠実なモデルになっている。
例えば、BOOKS.ID=1と関連するREVEIWSテーブルのレコードが2つあれば、一つのモデルbookの中に2つのreviewモデルを保持するreviewsコレクションが存在することになる。
一方下のJOIN SQLのやり方では、BOOKS.ID=1と関連するREVIEWSレコードが2つあってもあっても、階層構造ではなく、フラットな構造でデータを返してくる。例えば、BOOKS.ID=1,REVIEWS.ID=1 のレコードと、BOOKS.ID=1, REVIEWS.ID=2 という形。SQLの結果そのままで、オブジェクト指向のモデルにはなっていない。
このサンプルのようにただ子テーブル側データを表示するだけなら問題ないが、帳票のように階層的なデータを表示する場合に、こういうデータ構造は扱いにくい。
これを解消するために、JOIN SQLで取得したフラットなデータ構造を自分でモデル化してみる。
def build_book books = Book.joins('LEFT JOIN reviews ON books.id = reviews.book_id'). select('books.id bid, reviews.id rid, reviews.book_id, reviews.body') res = {} books.each { |rec| book = res[rec.bid] if book == nil book = Book.new class << book attr_accessor :reviews end res[rec.bid] = book book.id = rec.bid book.reviews = Array.new end review = Review.new next if rec.rid == nil review.id = rec.rid review.book_id = rec.book_id review.body = rec.body book.reviews.push(review) } res.values end
JavaのiBatisだとSQLがなんだろうが、こういうマッピングをしてくれるんだけどね。(groupbyの機能)
JPAだとできない気がする。Active Recordではできるのかな。
インターネットを流れるモノ
便利な時代です。
物理的にはインターネットというのは、世界中で瞬時に情報を流すプラットフォームです。
しかし、近頃では、そういう無機質な理解を超えたものだということを体感しつつあります。
パケットの中に隠れているのは単なる文字や画像ではありません。
人の思いや、様々な価値観、情熱が光の早さで流れます。
ロボットやプログラムには解釈できないものです。
それが人に与える影響の大きさは計り知れません。
明治維新を迎える直前の幕末、それは塾や書物の形をとって志士たちの間を伝搬していきました。
今と比べればとてもゆっくりしたものだったに違いありません。
黒船という衝撃は、大きなインパクトを持って、志士たちの間に伝わって行きました。
様々な価値観が共有され、大きなうねりとなりました。
インターネットを手に入れた現代、私たちは1日のうちに恐らく幕末数年分の知識を手に入れることができます。
遠くにいて、まだ会ったこともない知識人から教えを乞うこともできます。
誰から影響を受けるかも自由です。
その熱を自分で伝える力も幕末と比べれば百人力です。
ただ流れる情報の量が多いだけではないのです。その上に載せる大事なモノも、すごい速さで伝わるのです。
なんて素晴らしい時代なんだとワクワクせずにはいられません。
私たちは本当に恵まれています。
require
Rubyプロセス上で一度requireされたものは、そのあとはrequireしなくてもよいんですね。へえ。
require './hoge.rb' require './sage.rb' hoge = Hoge.new hoge.execute sage = Sage.new sage.execute
Sageの中で改めてHogeをrequireしなくても呼び出せます。
class Sage def execute Hoge.new.execute end end
便利だけど、コンテキストで既にrequireされているかわからないような時も結構あるような。そんなときはとりあえずrequireしとくんすかね。
requireが何回呼ばれても1回しかロードしないってのはそのためですかね。
Javaのimportとも違うし、省エネできてなかなか。
ActiveRecordの検索結果に対してコレクション操作を呼び出せるのは何故?
モデルBookから返された@booksで、eachとかsortとか呼べるのはなんでかなと。
@books = Book.find(1) @books = @books.sort { |x, y| x.price <=> y.price }
とか
@books = Book.select('publish, AVG(price) AS avg_price').group('publish') @books.each do |book| puts "publish=" + book.publish puts "avg="+book.avg_price end
とか。
調べてみると、@booksは ActiveRecord::Relation というクラス。
Relationの中で、
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
としている。delegate は ActiveSupportがRubyの元の委譲の仕組みを強化したものらしい。
オブジェクト指向で言われる「委譲」のこと。
ここでは、:each つまりeachメソッドを、 :to_a(このクラス内ではメソッド) に委譲するということ。
to_aはメソッドで、配列化するメソッド。
メソッドに委譲するということは、メソッドの返り値に委譲するということ?
Relation#eachはつまり、Relation#to_a#each になるってことですかね。
じゃあ、sortは? delegate の中に入ってません。
ここでまた、method_missing の出番。
def method_missing(method, *args, &block) if Array.method_defined?(method) to_a.send(method, *args, &block)
そのメソッドが、Arrayクラスで定義されているものだったら、to_aに委譲する。
それでsortが動けていた模様。
delegate を後で動かして確認してみよう。
(ActiveRecord 3.0.9 を確認)
GP02買った
GP02を購入、EMOBILEを契約しました。
最近、MacBookAirを持ち歩いてて、いつでもネット接続したかったためです。
とりあえず速いのは確実。(他のモバイルルータは体感してないけど、少なくともFOMA通信よりは速い)
GP02で手持ちのGalaxySの通信も全部賄おうと思ったけど、バッテリーの持ち時間がネック。4.5時間しか持たない。
Androidは常時通信してしまうので、GP02をそのままONにしておくとすぐにバッテリーが切れてしまう。あんまり考えないで買ってしまった。
とりあえずいくつか運用方法を試してみよう。
- GalaxySも外ではWifiオフにして普通のドコモ回線でパケット通信する。コストは安くならない。
- 通勤など移動中はGP02をON。仕事中はOFFにして、時々GalaxySのメールチェックしたいときだけONにする。コストは安くなるけど、めんどい。
- 大容量バッテリーを買う(でたらしい)。んで、常時ON。2.3倍だそうなので、10時間ぐらいは持つのかな。バッテリーが1万以上して結構高いのと、GP02の体積が倍になる。
大容量バッテリー購入はレビューが出始めてから様子見て考える。