@kyanny's blog

Remember Everything

ActiveRecord::Base#find_by_sql のコードを読んだ

プレースホルダの展開を String#gsub でやってるところがキレイだなーと思っただけなんだけど、せっかく読んだので少しだけどメモしとく。全部 rails/activerecord/lib/active_record/base.rb のコードでコピペしてあるのは v2.3.8 のやつ。

      def find_by_sql(sql)
        connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
      end

sanitize_sql を読む

        def sanitize_sql_for_conditions(condition, table_name = quoted_table_name)
          return nil if condition.blank?

          case condition
            when Array; sanitize_sql_array(condition)
            when Hash;  sanitize_sql_hash_for_conditions(condition, table_name)
            else        condition
          end
        end
        alias_method :sanitize_sql, :sanitize_sql_for_conditions

alias_method :new_method_name :old_method_name らしい。参考 Rubyのaliasとalias_method - walf443's blog

Model.find_by_sql(['select * from table where id = ?', id]) とかした場合 condition は Array なので sanitize_sql_array を読む

        def sanitize_sql_array(ary)
          statement, *values = ary
          if values.first.is_a?(Hash) and statement =~ /:\w+/
            replace_named_bind_variables(statement, values.first)
          elsif statement.include?('?')
            replace_bind_variables(statement, values)
          else
            statement % values.collect { |value| connection.quote_string(value.to_s) }
          end
        end

statement, *values = ary とかカッコイイと思う。プレースホルダの有無は String#include? で '?' を探してる。で replace_bind_variables を読む

        def replace_bind_variables(statement, values) #:nodoc:
          raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
          bound = values.dup
          statement.gsub('?') { quote_bound_value(bound.shift) }
        end

String#gsub('?') { } でブロック内で置換と。その前で raise するコードも String#count と values.size を比べるとか、見やすいなーと思う。

上の行を Perl で書くとするとこんな感じかな?

$statement =~ s/\?/quote_bound_value(shift @bound)/ge;

俺は正規表現に苦手意識があって、特に /e は読むのも書くのもてんでダメなので、余計に gsub が気に入ったのかもしれない。ただ、 pattern が文字列でも正規表現リテラルでもいいというのは結構戸惑う。 Array#reject! と Array#delete_if のように違いがあるのかと思って少し不安になる。