JQ Blog

ActiveRecordの設計

ActiveRecordの設計

ActiveRecordはRailsの一部であり、Rubyのオブジェクトをデータベースのテーブルにマッピングするものである。この機能をオブジェクトリレーショナルマッピングと呼ぶ。
ActiveRecordはRailsアプリケーションでは、モデル(MVCの「M」)として使われる。通常のRubyのオブジェクトでビジネスロジックを管理し、ActiveRecordでオブジェクトをデータベースに永続化することになる。

ActiveRecordの簡単な例

1
2
3
class Duck < ActiveRecord:Base
  validates_length_of :name, maximum: 6
end

上記の例では、6文字より長いnameをデータベースに保存しようとすると、例外を発生するか(save!の場合)、何も言わずに失敗する(saveの場合)。
ActiveRecordは規約に基づいて、自動的にDuckクラスをducksテーブルにマッピングしてくれる。データベーススキーマからducksテーブルにnameが含まれることを認識し、属性にアクセスするゴーストメソッドを定義する。

1
2
3
4
5
6
7
8
9
10
my_duck = Duck.new
my_duck.name = "Donald"
my_duck.valid? # => true
my_duck.save!


some_duck = Duck.first
some_duck.id   # => 1
some_duck.name # => "Donald"
some_duck.delete

のように使われる。

ActiveRecord::Base

1
2
3
4
5
6
7
8
9
10
# gems/activerecord-x.x.x/lib/active_record.rb
module ActiveRecord
  autoload :Base, 'active_record/base'
  autoload :Batches, 'active_record/batches'
  autoload :Calculations, 'active_record/calculations'
  autoload :Callbacks, 'active_record/callbacks'

  ・・・

end

ActiveRecordautoload()を使って各モジュールを読み込んでいる。Kernel#autoload()はモジュール名とファイル名を受け取り、モジュールを最初に参照した時にファイルを自動的に読み込む。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ActiveRecord
  class Base
    class << self
      def find(*args) # ...
      def first(*args) # ...
      def last(*args) # ...
      # ...
    end

    public
      def id # ...
      def save # ...
      def save! # ...
      def delete # ...
      # ...
  end
end

ActiveRecord::Baseには上記のコードのようにクラスメソッド、インスタンスメソッドを定義している。

ActiveRecord::Validations

先ほど使ってたvalid?()validates_length_of()などのメソッドはActiveRecord::Validationsモジュールで定義されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
      end
      base.send :include, ActiveSupport::Callbacks
      # ...
    end

    module ClassMethods
      def validate_each(*attrs) # ...
      def validates_confirmation_of(*attr_names) # ...
      def validates_length_of(*attrs) # ...
      # ...
    end

    def save_with_validation(perform_validation = true) # ...
    def save_with_validation! # ...
    def valid? # ...
    # ...
  end
end

ActiveRecord::BaseActiveRecord::Validationsをインクルードしていて、さらにフックメソッドを通してクラスメソッドも手に入れている。これはクラス拡張ミックスインである。ActiveRecord::Baseはモジュールのインクルードを繰り返して、メソッドを積み上げていく。

alias_method_chain()

1
2
3
4
5
6
7
class MyClass
  def greet
    puts "Hello!"
  end
end

MyClass.new.greet # => Hello!

例えば、このgreetメソッドの周りにロギング機能をつけたいとする場合、アラウンドエイリアスを使えば良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass
  def greet_with_log
    puts "メソッド呼び出し開始.."
    greet_without_log
    puts "...メソッド呼び出し完了"
  end

  alias_method :greet_without_log, :greet
  alias_method :greet, :greet_with_log
end

MyClass.new.greet
# =>
"メソッド呼び出し開始.."
"Hello!"
"...メソッド呼び出し完了""

このような仕組みをRailsではModule#alias_method_chain()を使って使用できる。これはActiveSupportライブラリで提供されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
      end

  ・・・

  end
end

上記のコードではActiveRecord::Baseクラスを再オープンして、save()save!()に検証機能をつけている。これでオブジェクトをデータベースに保存した時に自動的に検証を行ってくれる。

ActiveRecordのまとめ

ActiveRecord::BaseActiveRecordのメインとなるクラスだ。インスタンスメソッドとクラスメソッドが定義されているが、それ以外にもActiveRecord::Validationsのような追加モジュールをインクルードしている。モジュールをインクルードする際、クラス拡張ミックスインをして、さらに別のモジュールをインクルードする。こうしたモジュールはalias_method_chain()を使ってさらにいろんなメソッドを生み出す。このように次々にメソッドが定義されて、ActiveRecord::Baseは巨大なクラスになっていく。