SQL直発行を断念
ARではSQLが自動生成されるわけですが、SQLを直発行してDBのI/Oを減らしてみようという試みをしていましたが断念。
簡単に言うとページネイトの対応でつまずきました。
AR.findを置き換える
既存
controller @topic_list = Topic.find(:all, :include => :user, :conditions => ['id = ?', 1] ) erb <% @topic_list.each do |t| %> <%= h(t.title) %> <%= h(t.user.name) %> <% end %>
find_by_sqlでSQLを直発行。
ERBで「t.title」を取得するためには、カラム名を「title」にする。
「t.user.name」の場合は「user.name」というカラム名にすればOK。そのままだとMySQLでエラーになってしまうので``で括る。
sql = <<-SQL SELECT tp.id id, tp.title title, tp.body body, ・・ us.id `user.id`, us.name `user.name`, FROM topics as tp left outer join users as us on tp.user_id = us.id WHERE tp.id = :topic_id SQL @topic_list = Topic.find_by_sql([sql, {:topic_id => 1}])
PagingEnumerator
今回は、ページネートにはpagenating_findプラグインを使用している。これを使うとAR.findで:pageオプションを指定した際に、モデルの配列じゃなくて、PagingEnumeratorってクラスになる。(昨日のエントリ参照)
で、このPagingEnumerator、まずページ情報を取得するために、内部的にSELECT COUNT(*)で件数を取得して、それからモデルを取りに行くという動きになっている模様。そこはSQLを2つ発行することでクリアできそうだけど、最大の問題点は初期化する際にコールバックを指定する必要があるみたい。
class PagingEnumerator include Enumerable attr_accessor :results, :page, :first_page, :last_page, :stop_page, :page_size, :page_count, :size, :auto def initialize(page_size, size, auto = false, page = 1, first_page=page, &callback) self.page = page.to_i self.page_size = page_size.to_i self.size = size.to_i self.auto = auto self.first_page = first_page.to_i self.last_page = page_count.to_i self.stop_page = auto ? last_page : self.page @callback = callback end
既存コードのfind(:page付き)で見てみると、コールバックは、Procでpageinating_find.rbの83行目になってる。
これはfind_with_pagination(*args)ってメソッドの一部。
pagenating_find.rb PagingEnumerator.new(page_size, total_size, auto, current, first) do |page| args.pop if args.last.is_a?(Hash) # Set appropriate :offset and :limit options for this page options[:offset] = (page - 1) * page_size options[:limit] = (page_size) < total_size ? page_size : total_size if cached_scoped_methods # :with_scope options were specified, so # the with_scope method must be invoked self.with_scope(cached_scoped_methods) do find_without_pagination(*(args << options)) end else find_without_pagination(*(args << options)) end end
で、どこでコールバックが呼ばれているかというと、PagingEnumeratorのload_pageの中のよう。(コールバックを設定してないと、ここで例外が発生する)
処理の流れは以下のようになるかと。
controllerでfind
→find_with_pagination。その中で件数を取得(collect_count_options)
→find_with_paginationの中でPagingEnumeratorをnew。コールバックをセット
→controllerからviewに処理が移る
→load_pageでコールバック
→コールバックの処理の中でSELECTを実行(find_without_pagination)
つまり、ページネートをしている処理はほとんどプラグイン側で処理されていて、修正するためにはプラグイン側に手を入れないとできないということになります。
で、プラグインでの処理は、勿論いろんなモデルのfindに対応するため、汎用的なつくりになっているため、ここにSQLを直接書くというのは非現実的(っていうか無理?)。
SQLを直接発行する場合は、ページネートも自前で考えて作りこむ必要がありそうです。あと、ここに書いてない(やってない)けど、1:Nの関連データを取ってくる場合は1回で取得しようとすると1の方が重複しちゃうので、ruby側でそこの制御(判定)を毎回するか、別途取得するようにしないといけない。
流石にそこまでのコストをかけるだけの効果があるかというと。。。ということでSTOP><