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_sqlSQLを直発行。
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><