【コードリーディング】Rails 1.0 ActiveRecordのソースコードを読んでみた

#はじめに
Railsポートフォリオ作成でActiveRecordを使っており、作成当時は正直内部処理がどうなっているのか分かっていませんでし、気にもしていませんでした笑

そういった中、実務案件で直接SQLを書く機会があり、ActiveRecordって実際どういう風にSQLに置換して実行しているのか気になりはじめました。

そこで、ActiveRecordのソースコードでSQLに置き換えている仕組みの部分のコードを読んで理解した部分を書き留めたいと思います。

#今回読んだソースコード
今回はコードの流れをシンプルに見たかったので最新のソースコードではなくRails v1.0.0のソースコードを読みました。とは言ってもコードは約1800行もあり、読むのになかなか骨が折れました笑

その中でもController部分でよく使用するfindメソッドに絞って読んだものを記載します。
ソースコード元は[こちら](https://github.com/rails/rails/blob/v1.0.0/activerecord/lib/active_record/base.rb#L378)

#そもそも[ActiveRecord](https://railsguides.jp/active_record_basics.html)って？
ActiveRecordはRailsにおいてデータベースからデータを取り出したり、データベースにデータを保存したりする役割(MVCでいうMのModelの部分)を担っています。

ActiveRecordのメリットは主に以下の２点かなと感じております。
・簡易的な記述でSQL文を発行できる
・MySQL、PostgreSQLなど様々なデータベースに対し共通の記述で対応することが可能

O/Rマッパーと呼ばれるデータベースとプログラムを橋渡しする役目を担うものがActiveRecordに含まれているため、多様なデータベースからデータを取り出したり、データベースにデータを保存したりといった処理がスムーズに行うことができます。

#早速コードを読んでいく

```ruby:active_record/base.rb/find
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内に記載した引数に対し条件分岐するコードが記載されていました。

はじめに<code>extract_options!</code>で可変長引数(柔軟な引数指定)を受け取れるメソッドを使い引数を受け取り<code>optionsへ代入</code>

<code>:all</code>、<code>:first</code>を引数としており、<code>:all</code>の中でさらに分岐が行われている模様。

・<code>:first</code>の場合
inclued(モジュールを呼び出す)されなければ、optionsに<code>{ :limit => 1 }</code>を代入し、<code>:all</code>へ
id指定の場合はconditionsに<code>"#{table_name}.#{primary_key} = #{id}"</code>を追加し<code>:all</code>へ

・<code>:all</code>でやっていること
<code>:include</code>オプションありの場合<code>find_with_associations(options)</code>へ
<code>:include</code>オプションなしの場合<code>find_by_sql(construct_finder_sql(options))</code>へ

今回は<code>find_by_sql(construct_finder_sql(options))</code>を読み進めていきたいと思います。

##find_by_sqlメソッド

まずは<code>find_by_sql(construct_finder_sql(options))</code>のfind_by_sqlメソッドから

```ruby:active_record/base.rb/find_by_sql(sql)
def find_by_sql(sql)
  connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
```

find_byは今でも残る重要なメソッドですよね！なんだか知ってるメソッドがあるだけで嬉しい気持ちになりました笑

find_byメソッドはid以外のカラムを検索条件としたい場合に使用しますが、まさにカラムをとってきて返している処理が書かれています。

ここで気になったのは<code>sanitize_sql</code>と<code>instantiate</code>

###sanitize_sqlメソッド
公式ドキュメントは[こちら](https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html)

これによるとサニタイズした値を入れることによって意図しないSQLの挙動を防ぐことができるみたいですね。このメソッドはSQLインジェクション攻撃対策のために使用されています。

**サニタイズってなに？**

特別な意味を持つ文字の特別さを無効化する処理のこと

**SQLインジェクションとは？**
> SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。

> [Rails セキュリティガイド - Railsガイド](https://railsguides.jp/security.html#sql%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)

不正な「SQL」の命令を注入し、意図しないSQL文を発行し攻撃すること。
例えばDELETE文を発行され、データを全て消されてしまうなど対策をしないとセキュリティ的に非常に危険。

###instantiate

instantiateは読み進めているとメソッドとして定義している箇所がありました。

```ruby:active_record/base.rb/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
```
ほぼ読む必要はないが<code>allocate</code>によって取得したものが新しいレコードかを判断するメソッドと理解しました。レコードに保存されていないカラムや値の場合はエラーを吐き出す仕様になっている模様。

###find_by_sqlメソッド総括
総括してfind_by_sqlメソッドはサニタイズした値を入れ込む事によりSQLインジェクション攻撃を防ぎつつ、取ってきたレコードを返す。レコードはinstantiateにより既存のレコードかNewレコード化を判断し、場合によりエラーを吐き出す仕様になっていると言う感じですかね。

##construct_finder_sqlメソッド
続いて<code>find_by_sql(construct_finder_sql(options))</code>のconstruct_finder_sqlメソッドについて読み進めました。

```ruby:active_record/base.rb/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メソッド

```ruby:active_record/base.rb/save
def save
  raise ActiveRecord::ReadOnlyRecord if readonly?
  create_or_update
end
```
saveメソッドによりcreateやupdateを呼び出しています。

###createメソッド
```ruby:active_record/base.rb/save
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メソッド
```ruby:active_record/base.rb/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については[こちら](https://www.sejuku.net/blog/64345)が参考になりました。

##あとがき
今回は初めてOSS(オープンソースソフトウェア)を読んでみました。
ソースコードを辿ってみた結果愚直にSQLを発行していたと言うことがわかりました。

自分で作成したポートフォリオの比じゃないコード量を読み進めていくのは大変でしたが一つ一つメソッドを辿ってく内に理解していく感覚がすごく楽しかったです。

企業の自社サービスのコードはこれの比じゃないくらい膨大な記述量になっているとは思いますが、早くコードを読んでみたいと言う願望が高まっています。

これからも挙動の根幹をコードを読み進めて理解していきたいと思います。
最後までご覧いただきありがとうございました！！