#はじめに
Railsポートフォリオ作成でActiveRecordを使っており、作成当時は正直内部処理がどうなっているのか分かっていませんでし、気にもしていませんでした笑
そういった中、実務案件で直接SQLを書く機会があり、ActiveRecordって実際どういう風にSQLに置換して実行しているのか気になりはじめました。
そこで、ActiveRecordのソースコードでSQLに置き換えている仕組みの部分のコードを読んで理解した部分を書き留めたいと思います。
#今回読んだソースコード
今回はコードの流れをシンプルに見たかったので最新のソースコードではなくRails v1.0.0のソースコードを読みました。とは言ってもコードは約1800行もあり、読むのになかなか骨が折れました笑
その中でもController部分でよく使用するfindメソッドに絞って読んだものを記載します。
ソースコード元はこちら
#そもそもActiveRecordって?
ActiveRecordはRailsにおいてデータベースからデータを取り出したり、データベースにデータを保存したりする役割(MVCでいうMのModelの部分)を担っています。
ActiveRecordのメリットは主に以下の2点かなと感じております。
・簡易的な記述でSQL文を発行できる
・MySQL、PostgreSQLなど様々なデータベースに対し共通の記述で対応することが可能
O/Rマッパーと呼ばれるデータベースとプログラムを橋渡しする役目を担うものがActiveRecordに含まれているため、多様なデータベースからデータを取り出したり、データベースにデータを保存したりといった処理がスムーズに行うことができます。
#早速コードを読んでいく
def find(*args)
options = extract_options_from_args!(args)
#省略
case args.first
when :first
find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first
when :all
records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options))
records.each { |record| record.readonly! } if options[:readonly]
records
else
return args.first if args.first.kind_of?(Array) && args.first.empty?
expects_array = args.first.kind_of?(Array)
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
ids = args.flatten.compact.uniq
case ids.size
when 0
raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
when 1
if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" }))
return expects_array ? [ result ] : result
else
raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
end
else
ids_list = ids.map { |id| sanitize(id) }.join(',')
result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"}))
if result.size == ids.size
return result
else
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
end
end
end
end
最初の部分はallやfirstなど、find内に記載した引数に対し条件分岐するコードが記載されていました。
はじめにextract_options!
で可変長引数(柔軟な引数指定)を受け取れるメソッドを使い引数を受け取りoptionsへ代入
:all
、:first
を引数としており、:all
の中でさらに分岐が行われている模様。
・:first
の場合
inclued(モジュールを呼び出す)されなければ、optionsに{ :limit => 1 }
を代入し、:all
へ
id指定の場合はconditionsに"#{table_name}.#{primary_key} = #{id}"
を追加し:all
へ
・:all
でやっていること
:include
オプションありの場合find_with_associations(options)
へ
:include
オプションなしの場合find_by_sql(construct_finder_sql(options))
へ
今回はfind_by_sql(construct_finder_sql(options))
を読み進めていきたいと思います。
##find_by_sqlメソッド
まずはfind_by_sql(construct_finder_sql(options))
のfind_by_sqlメソッドから
def find_by_sql(sql)
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
find_byは今でも残る重要なメソッドですよね!なんだか知ってるメソッドがあるだけで嬉しい気持ちになりました笑
find_byメソッドはid以外のカラムを検索条件としたい場合に使用しますが、まさにカラムをとってきて返している処理が書かれています。
ここで気になったのはsanitize_sql
とinstantiate
###sanitize_sqlメソッド
公式ドキュメントはこちら
これによるとサニタイズした値を入れることによって意図しないSQLの挙動を防ぐことができるみたいですね。このメソッドはSQLインジェクション攻撃対策のために使用されています。
サニタイズってなに?
特別な意味を持つ文字の特別さを無効化する処理のこと
SQLインジェクションとは?
SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。
不正な「SQL」の命令を注入し、意図しないSQL文を発行し攻撃すること。
例えばDELETE文を発行され、データを全て消されてしまうなど対策をしないとセキュリティ的に非常に危険。
###instantiate
instantiateは読み進めているとメソッドとして定義している箇所がありました。
def instantiate(record)
object =
if subclass_name = record[inheritance_column]
if subclass_name.empty?
allocate
else
require_association_class(subclass_name)
begin
compute_type(subclass_name).allocate
rescue NameError
raise SubclassNotFound,
"The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
"or overwrite #{self.to_s}.inheritance_column to use another column for that information."
end
end
else
allocate
end
object.instance_variable_set("@attributes", record)
object
end
ほぼ読む必要はないがallocate
によって取得したものが新しいレコードかを判断するメソッドと理解しました。レコードに保存されていないカラムや値の場合はエラーを吐き出す仕様になっている模様。
###find_by_sqlメソッド総括
総括してfind_by_sqlメソッドはサニタイズした値を入れ込む事によりSQLインジェクション攻撃を防ぎつつ、取ってきたレコードを返す。レコードはinstantiateにより既存のレコードかNewレコード化を判断し、場合によりエラーを吐き出す仕様になっていると言う感じですかね。
##construct_finder_sqlメソッド
続いてfind_by_sql(construct_finder_sql(options))
のconstruct_finder_sqlメソッドについて読み進めました。
def construct_finder_sql(options)
sql = "SELECT #{options[:select] || '*'} FROM #{table_name} "
add_joins!(sql, options)
add_conditions!(sql, options[:conditions])
sql << " GROUP BY #{options[:group]} " if options[:group]
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options)
sql
end
ここは結構SQL文そのままって印象でした。用途によっていろんなSQL文を発行しテーブルからデータを参照している印象です。
##保存、更新
最後にfindとは関係なくなってしまいますがActiveRecordによる保存、更新処理部分を読み進めて終わりたいと思います。
###saveメソッド
def save
raise ActiveRecord::ReadOnlyRecord if readonly?
create_or_update
end
saveメソッドによりcreateやupdateを呼び出しています。
###createメソッド
def create
if self.id.nil? and connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
self.id = connection.insert(
"INSERT INTO #{self.class.table_name} " +
"(#{quoted_column_names.join(', ')}) " +
"VALUES(#{attributes_with_quotes.values.join(', ')})",
"#{self.class.name} Create",
self.class.primary_key, self.id, self.class.sequence_name
)
@new_record = false
end
愚直にテーブル名、カラム名、プライマリーキー、idなどをSQLで挿入していますね。
###updateメソッド
def update(id, attributes)
if id.is_a?(Array)
idx = -1
id.collect { |id| idx += 1; update(id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
object
end
end
ここでidから検索しidとattributes(Modelの属性全て)を渡して更新しています。
検証中に失敗した場合はオブジェクトを返しています。
attributesについてはこちらが参考になりました。
##あとがき
今回は初めてOSS(オープンソースソフトウェア)を読んでみました。
ソースコードを辿ってみた結果愚直にSQLを発行していたと言うことがわかりました。
自分で作成したポートフォリオの比じゃないコード量を読み進めていくのは大変でしたが一つ一つメソッドを辿ってく内に理解していく感覚がすごく楽しかったです。
企業の自社サービスのコードはこれの比じゃないくらい膨大な記述量になっているとは思いますが、早くコードを読んでみたいと言う願望が高まっています。
これからも挙動の根幹をコードを読み進めて理解していきたいと思います。
最後までご覧いただきありがとうございました!!