Active Recordの設計
概要
この記事は、「メタプログラミングRuby 第2版」第9章「Active Recordの設計」を読み学習した内容を個人学習用にまとめ直したものです。
Active RecordはRailsの中核をなすORMであり、その内部はメタプログラミングの技法で満ちています。本記事では、オートローディング、ActiveRecord::Baseのモジュール構成、ゴーストメソッドによるアトリビュートアクセスなど、メタプログラミングの観点からActive Recordの内部設計を読み解きます。
Active Recordとは
Active Recordは、Railsの中核をなすORM(Object-Relational Mapping)ライブラリ。
Railsを経由せずに、Active Record単体で利用するためには以下のようにrequireしてデータベースとのコネクションを開く必要がある。
require 'active_record'
# sqlite3のgemを先にインストールしておくこと
ActiveRecord::Base.establish_connection :adapter => "sqlite3",
:database => "dbfile"
# railsアプリケーションでは自動的に設定される
そして、Active Recordの規約に従っていれば、自動的に以下のようなDuckクラスをducksテーブルにマッピングしてくれる。
さらに、DBスキーマでducksテーブルにnameが含まれることを発見し、そのアトリビュートにアクセスするゴーストメソッドを定義してくれる。
# 全てのマッピングクラスはActiveRecord::Baseを継承する
class Duck < ActiveRecord::Base
validate do # ブロックを受け取るクラスマクロ
errors.add(:base, "Illegal duck name.") unless name[0] == 'D'
end
end
my_duck = Duck.new
my_duck.name = "Donald" # nameはアトリビュートにアクセスするゴーストメソッド
my_duck.valid? # => true
my_duck.save!
Active Recordの仕組み
Active Recordの実装とオートローディング
前項のコード例における、require 'active_record'によって読み込まれるActiveRecordの内容は以下のようになっている。
require 'active_support'
require 'active_model'
# ...
module ActiveRecord
extend ActiveSupport::Autoload # extendしてクラスメソッドを利用可能なように拡張している
# autoloadはActiveSupport::Autoloadのクラスマクロ
# モジュール名を最初に使った際に、自動的にモジュール・クラスのソースを探索しrequireする
autoload :Base
autoload :NoTouching
autoload :Persistence
autoload :QueryCache
autoload :Querying
autoload :Validations
# ...
ActiveRecord::Base
ActiveRecordにおいてautoload :Baseによって読み込まれるActiveRecord::Baseの内容は以下のようになっている。
Baseクラスは、多数のモジュールをinclude/extendすることで機能を組み立てている。各モジュールが永続化・バリデーション・コールバック・アソシエーションといった個別の責務を担い、それらを動的に合成することでBaseクラスの豊富な機能が実現されている。
ActiveRecordで使用されているautoloadにより、include Persistenceやextend Queryingで定数が参照された時点で対応するソースファイルが自動的にrequireされるため、Baseクラスはinclude/extendの宣言だけで簡潔に構成できている。
module ActiveRecord
class Base
include ActiveModel::API
extend ActiveSupport::Benchmarkable
extend ActiveSupport::DescendantsTracker
extend ConnectionHandling
extend QueryCache::ClassMethods
extend Querying
extend Translation
extend DynamicMatchers
extend DelegatedType
extend Explain
extend Enum
extend Delegation::DelegateCache
extend Aggregations::ClassMethods
include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
include Inheritance
include Scoping
include Sanitization
include AttributeAssignment
include Integration
include Validations
include CounterCache
include Attributes
include Locking::Optimistic
include Locking::Pessimistic
include Encryption::EncryptableRecord
include AttributeMethods
include Callbacks
include Timestamp
include Associations
include SecurePassword
include AutosaveAssociation
include NestedAttributes
include Transactions
include TouchLater
include NoTouching
include Reflection
include Serialization
include Store
include SecureToken
include TokenFor
include SignedId
include Suppressor
include Marshalling::Methods
self.param_delimiter = "_"
end
# Baseの読み込み完了を通知し、外部のgemやinitializerがBaseを拡張できるようにする
# これにより、上記でincludeされた各モジュールの振る舞いも拡張・上書きが可能になる
ActiveSupport.run_load_hooks(:active_record, Base)
end
このように、ActiveRecord::Baseはextend、includeで多数のモジュールを取り込んでいる。
前項のコード例でDuckクラスがActiveRecord::Baseを継承した瞬間に、これらすべてのモジュールの機能が利用可能になる。
例えば、save!メソッドはPersistenceモジュールから、nameのようなゴーストメソッドはAttributeMethodsモジュールから提供されている。
Validationsモジュール
バリデーション機能はActiveModel::ValidationsとActiveRecord::Validationsの2つのモジュールから責務を分けてincludeされている。
-
ActiveModel::Validations(include ActiveModel::API経由):validateクラスマクロやvalid?の基本実装など、オブジェクトモデルとしてのバリデーション基盤を提供。DBに依存しない汎用的なバリデーションの仕組みを担当 -
ActiveRecord::Validations(include Validations):valid?をオーバーライドして:create/:updateのコンテキスト判定を追加し、save/save!時に自動でバリデーションを実行するなど、保存や読み込みなどのDB操作に関連するバリデーション拡張を担当
ActiveRecordの各モジュールの再利用性
ActiveRecord::Baseは何百ものメソッドをまとめた巨大なクラスだが、疎結合、テスト容易性、モジュールの再利用性を組み合わせて、実行時にクラスが構成されるような設計となっている。
例えば、妥当性確認の機能を利用する必要があれば、ActiveRecord::Baseや他のモジュールを無視して、以下のようにActiveModel::Validationsをインクルードすれば良い。
require 'active_model'
class User
include ActiveModel::Validations
attr_accesor :password
validate do
errors.add(:base, "Don't let choose the password") if password == '1234'
end
end
user = User.new
user.password = "12345"
user.valid? # => true
user.password = "1234"
user.valid? # => false
まとめ
- Active RecordはRailsの中核をなすORMで、規約に従えばクラスとテーブルを自動マッピングする
-
ActiveRecord::Baseはextend/includeで多数のモジュールを合成し、永続化・バリデーション・コールバック等の機能を実現している。オートローディングによりrequire不要で宣言だけで構成できている - DBスキーマからカラム情報を読み取り、ゴーストメソッドで動的にアトリビュートアクセサを提供する
- 各モジュールは疎結合に設計されており、
ActiveRecord::Baseを経由せず個別にincludeして再利用できる
参考文献
この記事は以下の情報を参考にして執筆しました。