RubyOnRails を使ってみる 【第 3 回】 ActiveRecord

もりきゅうです。

今回は Ruby on Rails (以下 RoR) を構成するライブラリ群のうち ActiveRecord について掘り下げていきます。

ActiveRecord (以下 AR) は RLR 第 4 回 に取り上げられたように、Ruby での O/R マッピングライブラリのひとつです。 AR を使えば、簡単かつ効率的にリレーショナルデータベース上の情報を Ruby オブジェクトとして扱うことができます。その理論的なところは P of EAA: Active Record を見ていただくことにして、ここではその実際的な使い方を見ていきます。

さて、RoR は非常に早く進化してきましたが、コアはそれほど変わっていません。 AR も使い方はそうそう変わらないだろうと思いますので、一通りその機能を見ていきたいと思います。

今回は、チュートリアルではなく主にリファレンスとして、AR に関する情報を日本語で提供できるように書いてみました。

はじめに、RoR と切り離して AR を使うための簡単なサンプルを紹介します。 次に、AR のソース構造を概観し、AR の test を読んでいきます。

後半はマニュアルの翻訳を試みました。 関連のうち、特に has_many と belongs_to について、生成されるメソッドを概観します。 そして callback, validation といったよく使う機能を見ていきます。 最後に、AR クラス・オブジェクトのリファレンスを載せました。

なお、今回は activerecord-1.10.1 を対象としています。

おさらい

AR オブジェクトの基本的な扱い方を簡単におさらいしておきましょう。

RoR は全て含めると非常に大きいので、なかなか把握しづらいかもしれません。 そこで、今回は AR を単体で使ってみることにします。 AR を RoR から独立したひとつのライブラリとして扱います。

database, table の準備

ここでは RDBMS として MySQL を使うことにします。

rubima という database に rails という user で接続するとします。

 CREATE DATABASE rubima;
 grant all on rubima.* to rails@localhost;

users table を作ります。

 use rubima;
 CREATE TABLE users (
   id int unsigned not null auto_increment,
   name varchar(50),
   occupation varchar(50),
   primary key (id)
 );

AR で接続

DB の準備ができました。AR で接続してみましょう。

  • ar.rb:
 # gem を使わない場合
 # require 'active_record'

 # gem を使う場合
 require 'rubygems'
 require_gem 'activerecord'

 ActiveRecord::Base.establish_connection(
   :adapter => 'mysql',
   :host => 'localhost',
   :username => 'rails',
   :password => '',
   :database => 'rubima'
 )

 class User < ActiveRecord::Base
 end

new - 作成

AR オブジェクトを作るには new メソッドを使います。

create メソッドを使うこともできます。create は new したあと save (DB に格納) します。

引数として、カラム名と値をハッシュにして与えます。

 user = User.new(:name => "David", :occupation => "Code Artist")
 p user.name # => "David"

また、ブロックを使うこともできます。

 user = User.new do |u|
   u.name = "David"
   u.occupation = "Code Artist"
 end

もちろん、作ってから値を設定してもいいです。

 user = User.new
 user.name = "David"
 user.occupation = "Code Artist"

なお、new で作った AR オブジェクトは

 user.save

のように save するまでは DB に格納されません。

find - 検索・抽出

検索・抽出を行うには find メソッドを使います。

find のほか find_first, find_all, find_by_sql といったメソッドがあります。

base.rb から例を引用しておきます。 オプションの意味を理解するには若干 SQL の知識が必要です。 詳細はマニュアルを読んでください。

id で検索する例:

 Person.find(1)       # returns the object for ID = 1
 Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
 Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
 Person.find([1])     # returns an array for objects the object with ID = 1
 Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")

find first の例:

 Person.find(:first) # returns the first object fetched by SELECT * FROM people
 Person.find(:first, :conditions => [ "user_name = ?", user_name])
 Person.find(:first, :order => "created_on DESC", :offset => 5)

find all の例:

 Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
 Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
 Person.find(:all, :offset => 10, :limit => 10)
 Person.find(:all, :include => [ :account, :friends ])

パラメータ (プレースフォルダ)

クエリに変数を適用するときには、直接 #{} で文字列の中に埋め込むのではなく、パラメータとして渡したほうが安全です。 例えば (base.rb 参照) user_name と password 変数を埋め込むとします。

 User < ActiveRecord::Base
   def self.authenticate_unsafely(user_name, password)
     find_first("user_name = '#{user_name}' AND password = '#{password}'")
   end

   def self.authenticate_safely(user_name, password)
     find_first([ "user_name = ? AND password = ?", user_name, password ])
   end
 end

authenticate_unsafely のように書いたとき、user_name や password にエスケープされていない引用符を含めることができれば、任意のSQLを実行させることができてしまいます。 このようなときは authenticate_safely のように ? を用いて値を渡しましょう。 ? に渡った値はサニタイズされます (文字列ならエスケープされて引用符で囲まれます)。

名前付きパラメータ

? がたくさん付くようになると、パラメータの順序を覚えるのがうっとおしくなります。 このようなときは ? の代わりに :名前 を使ってパラメータに名前を付けると良いでしょう。 このときは対応する値をハッシュで与えます。

 Company.find(:first, [
   "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
   { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
 ])

AR の構成

AR を理解する第一歩として、まず AR の構成を知るところから始めましょう。

ライブラリの構成

まず、ディレクトリ構造を見てみましょう。

ソースツリーは次のようになっています。

 activerecord-1.10.1/
   examples/
   lib/
   test/

lib/ を見てください。

 lib/
   active_record/
     acts/
     associations/
     connection_adapters/
     vendor/
     wrappers/

acts/ は list, tree といったデータ構造を AR で実現するための拡張です。 詳細はマニュアルをご覧ください。

associations/ は lib/associations.rb が読み込みます。 ここでは外部キーに基づく関連 (relationship) を実装しています。 belongs_to, has_one, has_many, has_and_belongs_to_many といった関連の定義はここにあります。

connection_adapters/ では DB アダプタを実装しています。

vendor/ には RoR の外部から提供されているライブラリが置かれています。

wrappers/ にはカラム値の変換プラグインが置かれています。今のところ YAML だけです。

さて、require ‘active_record’ あるいは require_gem ‘activerecord’ したときに読み込まれるのは lib/active_record.rb です。このファイルを見てみましょう。 すると、fixtures.rb 以外のファイルはすべてここで require され、各 module が ActiveRecord::Base.class_eval の中で include されていることが分かります。

そして、各 module には次のような仕掛けがあります。 例えば validations.rb:

 module Validations
 ...
   def self.append_features(base) # :nodoc:
     super
     base.extend ClassMethods
     base.class_eval do
       alias_method :save_without_validation, :save
       alias_method :save, :save_with_validation

       alias_method :update_attribute_without_validation_skipping, :update_attribute
       alias_method :update_attribute, :update_attribute_with_validation_skipping
     end
   end
 ...
   module ClassMethods

self.append_features(base) は include 時に呼ばれるのでしたね。 そして module ClassMethods は base.extend されているので、ここで定義されたメソッドは base (= ActiveRecord::Base) のクラスメソッドとして扱われます。ライブラリを分割する手法として非常に参考になります。

また、validations.rb では alias_method を使ってメソッドを置き換えています。

結局、module の多くは ActiveRecord::Base クラスに対する定義になっています。

AR test を読む

AR を単体として見るために、AR 自身の test を取り上げます。

AR には RDoc で生成された良くできたマニュアルが付いていますが、それでもソースを読む価値は常にあります。 AR はテスト駆動で作られているため、test に AR の仕様が具体的にコードの形で表われます。 test を読むと、仕様の微妙な箇所や実装の妥協点がよく分かります。

test を読むとっかかりとして、本稿では connection.rb と Topic, Reply クラスだけ解説します。

  • activerecord-1.10.1/RUNNING_UNIT_TESTS:
  cd test; ruby -I "connections/native_mysql" base_test.rb

-I “connections/native_mysql” は $: の設定であり、require ‘connection’ が読み込む connection.rb のパスを指定しています。

connections

ここでは MySQL アダプタを用いる例を見ていきますが、どのアダプタでも基本は同じです。

  • connections/native_mysql/connection.rb:
 print "Using native MySQL\n"
 require 'fixtures/course'
 require 'logger'

 ActiveRecord::Base.logger = Logger.new("debug.log")

 db1 = 'activerecord_unittest'
 db2 = 'activerecord_unittest2'

 ActiveRecord::Base.establish_connection(
   :adapter  => "mysql",
   :host     => "localhost",
   :username => "rails",
   :password => "",
   :database => db1
 )

 Course.establish_connection(
   :adapter  => "mysql",
   :host     => "localhost",
   :username => "rails",
   :password => "",
   :database => db2
 )

ここで require ‘fixtures/course’; db2 = ‘activerecord_unittest2’; Course.establish_connection といった部分は、複数のDBを関連付けて扱う multiple_db_test.rb で使われます。この機能については今回取り上げません。詳細は multiple_db_test.rb を読んでください。

なので、今回扱う範囲では、connection.rb は次のもので十分です。

 print "Using native MySQL\n"
 require 'logger'

 ActiveRecord::Base.logger = Logger.new("debug.log")

 db1 = 'activerecord_unittest'

 ActiveRecord::Base.establish_connection(
   :adapter  => "mysql",
   :host     => "localhost",
   :username => "rails",
   :password => "",
   :database => db1
 )

ここで logger を除いてみると、ここで行っているのは ActiveRecord::Base.establish_connection の呼び出しのみということになります。 establish_connection は引数を見て分かるとおり、DBに接続して、その接続を維持します。これ以降、この接続は ActiveRecord::Base.connection で参照できます。

abstract_unit.rb

次に base_test.rb を見ると、頭に require ‘abstract_unit’ があります。

  • abstract_unit.rb:
 $:.unshift(File.dirname(__FILE__) + '/../lib')
 $:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib')

 require 'test/unit'
 require 'active_record'
 require 'active_record/fixtures'
 require 'active_support/binding_of_caller'
 require 'active_support/breakpoint'
 require 'connection'

 class Test::Unit::TestCase #:nodoc:
   def create_fixtures(*table_names)
     if block_given?
       Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names) { yield }
     else
       Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names)
     end
   end
 end

 Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"

ここではライブラリの require を行っています。require ‘connection’ もここにあります。

 require 'test/unit'

これはみなさんお馴染みの Test::Unit ライブラリです。

 require 'active_record'
 require 'active_record/fixtures'

ここで AR ライブラリを読み込んでいます。 fixtures だけ別になっています。fixtures は test のみで使われるからです。

 require 'active_support/binding_of_caller'
 require 'active_support/breakpoint'

これはデバッグ用のライブラリです。 binding_of_caller, breakpoint の詳細は複雑かつ興味深いものですが、今回は避けます。

 require 'connection'

ここで先の connection.rb を読み込みます。

 class Test::Unit::TestCase #:nodoc:
   def create_fixtures(*table_names)

create_fixtures を定義していますが、現在は create_fixtures よりも fixtures を使うのが普通です。

 Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"

fixtures のパスはこのように指定します。

ここまで特に難しくはないですね。

base_test.rb に戻ります。

 require 'fixtures/topic'
 require 'fixtures/reply'

topic.rb

  • fixtures/topic.rb:
 class Topic < ActiveRecord::Base
   has_many :replies, :dependent => true, :foreign_key => "parent_id"
   serialize :content

   before_create  :default_written_on
   before_destroy :destroy_children

   def parent
     self.class.find(parent_id)
   end

   protected
     def default_written_on
       self.written_on = Time.now unless attribute_present?("written_on")
     end

     def destroy_children
       self.class.delete_all "parent_id = #{id}"
     end
 end

これも、もうみなさんは見慣れたであろう ActiveRecord::Base から継承した AR クラスの定義です。 普段は、script/generator model で作っておくファイルですね。

この Topic クラスでは、いくつかARの機能が見て取れます。

 has_many :replies, :dependent => true, :foreign_key => "parent_id"

has_many は1対多の関連付けに用います。 普通はふたつのテーブルを使いますが、Topic は Reply と共に Single table inheritance を構成します。 topics.png

  • :dependent => true

親 (Topic) を削除するときは子 (Reply) も削除するという指定です。

  • :foreign_key => “parent_id”

外部キーを parent_id にします。:foreign_key を省略したときは、外部キーの名前は相手のテーブル名から作られます。 逆に言うと、キー名を自由に決めていいときは、AR の仕様に合わせて名前を決めたほうがいいでしょう。

   serialize :content

content を YAML 形式で格納します。

   before_create  :default_written_on
   before_destroy :destroy_children

コールバックです。create, destroy 時に実行するメソッドを指定しています。

   def parent

これは Topic から継承する Reply クラスで使うことを想定しているものと考えられます。 Reply ではない Topic では parent_id は nil のはずですから、find は常に失敗します。

DBスキーマ

ここで Topic に対応するDBスキーマを見ておきましょう。 MySQL のものは以下にあります。

  • fixtures/db_definitions/mysql.sql:
 CREATE TABLE `topics` (
   `id` int(11) NOT NULL auto_increment,
   `title` varchar(255) default NULL,
   `author_name` varchar(255) default NULL,
   `author_email_address` varchar(255) default NULL,
   `written_on` datetime default NULL,
   `bonus_time` time default NULL,
   `last_read` date default NULL,
   `content` text,
   `approved` tinyint(1) default 1,
   `replies_count` int(11) default 0,
   `parent_id` int(11) default NULL,
   `type` varchar(50) default NULL,
   PRIMARY KEY  (`id`)
 ) TYPE=InnoDB;

注目したいのは replies_count, parent_id, type カラムです。これらは Reply との関係で使われます。 type カラムはクラス名を格納します。これは Single table inheritance の肝です。 id カラムは auto_increment な primary key です。 そのほかのカラムは単純な値の入れ物です。

reply.rb

topic.rb で何度も Reply について言及しましたので、reply.rb も合わせて見ておきましょう。

  • fixtures/reply.rb:
 class Reply < Topic
   belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
   has_many :silly_replies, :dependent => true, :foreign_key => "parent_id"

   validate :errors_on_empty_content
   validate_on_create :title_is_wrong_create

   attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read

   def validate
     errors.add("title", "Empty")   unless attribute_present? "title"
   end

   def errors_on_empty_content
     errors.add("content", "Empty") unless attribute_present? "content"
   end

   def validate_on_create
     if attribute_present?("title") && attribute_present?("content") && content == "Mismatch"
       errors.add("title", "is Content Mismatch")
     end
   end

   def title_is_wrong_create
     errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create"
   end

   def validate_on_update
     errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update"
   end
 end

 class SillyReply < Reply
 end
   belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true

belongs_to は関連するふたつのテーブルのうち association id を持つテーブル側に指定する関連付けでしたね。

  • :foreign_key => “parent_id”

topics table は parent_id を association id として、ひとつのテーブルで継承関係を作ります。

  • :counter_cache => true

この指定により replies_count カラムを自動的に更新します。

   validate :errors_on_empty_content
   validate_on_create :title_is_wrong_create
   def validate
   def validate_on_create
   def validate_on_update

これらは validation と呼ばれる機能です。値のチェックを行います。

ActiveRecord::Base#attribute_present?(カラム名) は、カラムが存在してかつ nil? でも empty? でもない、ようするに、値を持っていることを確認します。

   attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read

attr_accessible は、アクセスできるカラム名を (緩やかに) 制限します。attr_protected と対になります。

   has_many :silly_replies, :dependent => true, :foreign_key => "parent_id"

 class SillyReply < Reply
 end

Reply からさらに継承しています。Topic から見ると二段階の has_many になります。

参考: 特に一般化したリスト構造やツリー構造を扱う際には acts mixin が使えます。

base_test.rb ではほかにも多くのテーブルを扱っていますが、とりあえず Topic と Reply で留めておきます。 base_test.rb の test を読むと、AR の仕様が見えてくると思います。

以降は、関連、コールバック、validation について、マニュアルを読みつつ見ていきます。

associations

Topic, Reply を元に、AR の関連 (relationship) についてまとめてみます。

Topic, Reply は AR クラスです。よって new, find など AR クラスのメソッドは全て使えます。

さらに has_many, belongs_to の指定によってメソッドが追加されます。

has_many

Topic クラスで has_many :replies を宣言すると、次のメソッドが追加されます。

Topic#replies(force_reload = false)

関連付けられた reply の配列を返します。 もし reply がなければ空配列を返します。

Reply を使えば

 Reply.find :all, :conditions => "parent_id = #{topic.id}"

と書けます。

Topic#replies の結果はキャッシュされます。 同じ topic で再度 topic.replies を呼び出したときはキャッシュされた値が返ります。 force_reload = true にすると、リロードします (キャッシュを削除して DB から読み直します)。

Topic#replies<<(reply, …)

指定された reply を追加します。

DB 上は外部キーに topic.id を設定します。

Topic#replies.delete(reply, …)

指定された reply を取り除きます。

DB 上は外部キーに NULL を設定します。

もし :dependent => true であれば reply は destroy されます。

Topic#replies.clear

全ての reply を取り除きます。ただし reply は destroy されません。

Topic#replies.empty?

reply がなければ真を返します。

これは

 Topic#replies.size == 0

と同等です。

Topic#replies.size

reply の個数を返します。

Reply を使えば

 Reply.count("parent_id = #{topic.id}")

と書けます。

counter_cache が有効であればキャッシュの値が使われます。

Topic#replies.find(…)

reply を検索・抽出します。 引数は Base.find と同じ規則です。

Reply を使えば

 Reply.find(id, :conditions => "parent_id = #{topic.id}")

と書けます。

Topic#replies.build(attributes = {})

新たな reply オブジェクトを生成します。 引数は Base.new と同じ規則です。

new と同様、すぐに save はされません。

Reply を使えば

 Reply.new("parent_id" => topic.id)

と書けます。

Topic#replies.create(attributes = {})

新たな reply オブジェクトを生成し、save します (validation チェックは行います)。

Reply を使えば

 reply = Reply.new("parent_id" => topic.id)
 reply.save
 reply

あるいは

 Reply.create("parent_id" => topic.id)

と書けます。

オプション

has_many はオプションのハッシュをとることができます。

   has_many :replies, :dependent => true, :foreign_key => "parent_id"

全て書く余白がないので ;)、オプションについては割愛します。

belongs_to

Reply クラスで belongs_to :topic を宣言すると、次のメソッドが追加されます。

Reply#topic(force_reload = false)

関連付けられた topic を返します。なければ nil を返します。

Reply#topic=(topic)

topic を関連付けます。DB 上は外部キーに topic.id を設定します。

Reply#build_topic(attributes = {})

新たな topic を生成し、そして、この reply を関連付けます。 save しません。

Reply#create_topic(attributes = {})

新たな topic を生成し、そして、この reply を関連付けます。 save します。

オプション

belongs_to はオプションのハッシュをとることができます。

   belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true

詳しくはマニュアルをご覧ください。

callbacks

(callbacks.rb の部分的な翻訳です)

(new_record な) AR オブジェクトを save するときに呼ばれるコールバックは次の通りです。

  • (-) save
  • (-) valid?
  • (1) before_validation
  • (2) before_validation_on_create
  • (-) validate
  • (-) validate_on_create
  • (4) after_validation
  • (5) after_validation_on_create
  • (6) before_save
  • (7) before_create
  • (-) create
  • (8) after_create
  • (9) after_save

9 つのコールバックがありますね。 コールバックは、Active Record ライフサイクルのそれぞれの状態に反応し準備するための、とても大きな力となります。

コールバックの例:

 class CreditCard < ActiveRecord::Base
   # Strip everything but digits, so the user can specify "555 234 34" or
   # "5552-3434" or both will mean "55523434"
   def before_validation_on_create
     self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
   end
 end

 class Subscription < ActiveRecord::Base
   before_create :record_signup

   private
     def record_signup
       self.signed_up_on = Date.today
     end
 end

 class Firm < ActiveRecord::Base
   # Destroys the associated clients and people when the firm is destroyed
   before_destroy { |record| Person.destroy_all "firm_id = #{record.id}"   }
   before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
 end

継承可能なコールバックキュー

上書き可能なコールバックメソッドに加えて、コールバックマクロでコールバックを登録できます。 その主な利点は、マクロは継承階層の影響を受けずにコールバックキューに振る舞いを追加できることです。

例えば:

 class Topic < ActiveRecord::Base
   before_destroy :destroy_author
 end

 class Reply < Topic
   before_destroy :destroy_readers
 end

さて、 Topic#destroy を実行すると、destroy_author だけ呼ばれます。 Reply#destroy を実行すると、destroy_author と destroy_readers が呼ばれます。

同じ振る舞いを、上書き可能なメソッドで実装した場合と比較してみましょう。

 class Topic < ActiveRecord::Base
   def before_destroy() destroy_author end
 end

 class Reply < Topic
   def before_destroy() destroy_readers end
 end

こうすると、Reply#destroy は destroy_readers だけ実行し、destroy_author は実行しません。

なので、 あるコールバックが全階層で呼ばれることを保証したいときには、コールバックマクロを使い、 それぞれの派生先で留めたいときは、上書きメソッドを使えばいいでしょう。 上書きメソッドは、super を呼んで派生元のコールバックを行うか決めることができます。

重要: 継承関係がコールバックキューに対して正しく働くためには、 関連を指定する前にコールバックを指定する必要があります。 そうしないと、コールバックを登録している親の前に子を load したときに、継承されません。

コールバックのタイプ

コールバックには 4 つのタイプがあります。

  • メソッドリファレンス (symbol)
  • コールバックオブジェクト
  • インラインメソッド (proc を使います)
  • インライン eval メソッド (string を使います)

メソッドリファレンスとコールバックオブジェクトは、推奨されるアプローチです。 proc を使ったインラインメソッドは、ときには適しています (例えば mix-in するとき)。 インライン eval メソッドは推奨されません。

after_find と after_initialize の扱い

after_find と after_initialize は、パフォーマンスの制約から、(def after_find のように) 明確に定義されたときだけ実行されます。

コールバックのキャンセル

bafore_* の戻り値を false にすると、それ以降のコールバック全てと、そのコールバックに関連付けられたアクションがキャンセルされます。 after_* の戻り値を false にすると、それ以降のコールバック全てがキャンセルされます。

コールバックは定義された順に呼ばれますが、モデル上にメソッドとして定義されたコールバックは最後に呼ばれます。

validations

(validations.rb の部分的な翻訳です)

AR オブジェクトは Base#validate (あるいは validate_on_create validate_on_update) を上書きすることで validation を実装できます。 これらのメソッドは、オブジェクトの状態を検査でき、カラムがある値を持つこと (empty でない、範囲内にある、ある正規表現にマッチするといったようなこと) を保証できます。

validation の例:

 class Person < ActiveRecord::Base
   protected
     def validate
       errors.add_on_empty %w( first_name last_name )
       errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
     end

     def validate_on_create # is only run the first time a new object is saved
       unless valid_discount?(membership_discount)
         errors.add("membership_discount", "has expired")
       end
     end

     def validate_on_update
       errors.add_to_base("No changes have occurred") if unchanged_attributes?
     end
 end

 person = Person.new("first_name" => "David", "phone_number" => "what?")
 person.save                         # => false (and doesn't do the save)
 person.errors.empty?                # => false
 person.count                        # => 2
 person.errors.on "last_name"        # => "can't be empty"
 person.errors.on "phone_number"     # => "has invalid format"
 person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
                                          "Phone number has invalid format"

 person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
 person.save # => true (and person is now saved in the database)

errors オブジェクトは、それぞれの AR オブジェクトに対して自動的に生成されます。 より高度な validation については ActiveRecord::Validations::ClassMethods をご覧ください。

ActionPack と絡めた話

ActiveRecord は ActionPack から独立していますが、連携させて用いることが多いです。 ここでは ActionPack の ActiveRecord 対応機能をいくつか取り上げます。

validation と template

ActionController の create, update action で AR オブジェクトを save するときに、値をチェックしたいことがあります。

値をチェックするには、モデルクラスに validation メソッドを定義して、 望ましくない値を持つ場合に errors.add(カラム名, メッセージ) でエラーを追加します。

   def validate
     errors.add("code", "重複しています") if ...
   end

errors が存在すると (errors.empty? でないと) save に失敗することになります (ActiveRecord::Base#save が false を返します)。 ActionController 側では、普通、save に成功すれば redirect_to し、失敗すれば render で元のページを表示します。

    if @customer.save
      redirect_to :action => "show", :id => @customer.id
    else
      render_action "edit"
    end

errors.on(カラム名) は対応するメッセージを返します。そのカラム名に対応するエラーがないときは nil を返します。 これを編集用テンプレートに埋め込むと、save に失敗した理由を表示することができます。

   <tr>
     <th>会員NO</th>
     <td><%= text_field "customer", "code" %><div><%=h @customer.errors.on("code") %></div></td>
   </tr>

また、text_field といった helper メソッドは、カラム名に対応するエラーが存在するときにはタグを <div class=”fieldWithErrors”>…</div> で囲みます。 これを利用すると、スタイルシートを使ってエラーの原因となった入力フォーム要素を目立たせることができます。

 div.fieldWithErrors {
   background-color: red;
 }

ActiveRecordStore

RoR の session 機能は CGI::Session を用いていて、標準では PStore を用いてファイルとして格納しています。 この格納方法は取り替えることができ、そのひとつに ActiveRecordStore があります。 これは session を AR オブジェクトとして扱い、DB に格納します。

date, time, datetime

date_helper を使うとひとつのカラムに対して複数の値を渡すことになります。 この複数の値をひとつにまとめているのは ActionPack でしょうか。それとも ActiveRecord でしょうか。 これは ActiveRecord です。 ActiveRecord::Base#attributes= がそれを実現しています。

ActiveRecord リファレンス

AR クラス共通設定 (名前)

RoR は cattr_accessor という accessor 宣言を用意しています。 これはクラス変数用の attr_accessor です。 例えば、

 module ActiveRecord
   class Base
     cattr_accessor :primary_key_prefix_type

とすれば、@@primary_key_prefix_type は

 ActiveRecord::Base.primary_key_prefix_type
 ActiveRecord::Base.primary_key_prefix_type=

で読み書きできるようになります。

ここでは特に名前の設定に関するクラス変数をまとめておきます。

@@primary_key_prefix_type = nil

全てのプライマリキーカラム名の頭に付加する prefix のタイプを指定します。

:table_name を指定すると、Product クラスはプライマリキーカラムとして “id” の代わりに “productid” を探します。

:table_name_with_underscore を指定すると、Product クラスはプライマリキーカラムとして “id” の代わりに “product_id” を探します。

これは全 AR クラスに共通の設定になります。

@@table_name_prefix = “”

全てのテーブル名の頭に付加する prefix 文字列を指定します。

“basecamp_” を設定すると、テーブル名は “basecamp_projects”, “basecamp_people” などとなります。 これは共有される DB の名前空間を作るのに便利です。

デフォルトは空文字列です。

@@table_name_suffix = “”

table_name_prefix と同様に働きますが、頭ではなく後ろに付加されます (“_basecamp” を設定すると “projects_basecamp”, “people_basecamp” となります)。

デフォルトは空文字列です。

@@pluralize_table_names = true

テーブル名をクラス名の複数形にするかを指定します。

true なら、Product クラスのデフォルトテーブル名は products になります。false なら、product になります。

table/class 名前付けの規則の詳細については、table_name を見てください。

デフォルトは true です。

AR クラスメソッド (削除, 更新, カウント)

AR のクラスメソッドをまとめてみます。 ただし、ここでは base.rb で定義されているものだけ扱います。 すでに書いた new, create, find, find_* は除きます。

データ操作を行うメソッドは、おおまかにみて、SQL を直接扱うものと AR オブジェクトを通すものに区別できます。

destroy(id)

delete(id)

destroy と delete は、DB から該当行を削除します。 削除という意味ではどちらも同じですが、実行の仕方に違いがあります。

destroy は find で得た AR オブジェクトを destroy します。このとき、AR のコールバックは全て働きます。 例えば、:dependent による子の削除は、コールバック (before_destroy) によって実現されているので、destroy したときに行われます。

delete は AR オブジェクトを生成せずに直接 SQL (delete from …) を発行します。 そのため、コールバックは働きません。:dependent による削除は行われません。

destroy_all(conditions)

delete_all(conditions)

destroy_all, delete_all は、条件に合う行を削除します。 条件の書き方は find と同じです。

destroy_all, delete_all は destroy, delete と同じ関係にあります。

update(id, attributes)

update は、複数のカラムを更新し、save します。

update は find(id) して得られる AR オブジェクトに対して update_attributes(attributes) を行います。 update_attributes(attributes) は self.attributes = attributes して save します。戻り値は save の戻り値です。

update_all(updates, conditions = nil)

update_all は、直接 SQL を使った更新を行います。updates は SQL 文です。 delete_all と同様、直接 SQL (update …) を発行します。

count(conditions = nil, joins = nil)

count_by_sql(sql)

count は、条件に合う行数を返します。 直接 SQL (select count(*) from …) を発行します。joins は追加 SQL 文です。

count_by_sql は直接 SQL を発行します。sql は SQL 文で、select count(*) from … で書き始めます。 戻り値は、最初のカラムを to_i した値です。

increment_counter(counter_name, id)

decrement_counter(counter_name, id)

increment_counter, decrement_counter は、カウンタとして扱うカラムの値を +1, -1 します。

update_all を用いて実現されています。直接 SQL を発行します。

AR クラスメソッド (属性)

上に書いたような SQL を用いたデータ操作のためのメソッドとは別に、Ruby レベルでの動作を設定するためのクラスメソッドがあります。 これらは AR のクラス定義で関数的に使います。

なお、Ruby 側の属性 (attribute) と DB 側のカラム (column) は区別されますが、ここでは全てカラムと書いています。

attr_protected(*attributes)

attr_protected は、カラムへの代入を制限します。制限するのは new や attributes= のようにオブジェクトのカラムを一括して扱うメソッドに対してであって、カラムに対応するメソッドを使って代入することは制限しません。

protected_attributes

attr_protected で指定されたカラム名の配列を返します。

attr_accessible(*attributes)

attr_accessible は、attr_protected とは逆に、(一括した) 代入を許すカラム名を指定します。

accessible_attributes

attr_accessible で指定されたカラム名の配列を返します。

serialize(attr_name, class_name = Object)

serialize は、シリアライズするカラムを指定します。 カラムへの格納する値を YAML で変換します。これにより、Ruby の配列やハッシュをそのまま読み書きできるようになります。

class_name を指定すると、カラムに格納するオブジェクトのクラスを制限できます。

serialized_attributes

serialize で指定されたカラム名をキーとし、クラスを値とするハッシュを返します。

AR クラスメソッド (テーブル, キー)

DB テーブルやキーの名前を扱うクラスメソッドです。

table_name

AR クラスに対応する DB テーブル名を返します。 これは AR クラスの継承に対応しています。 例えば、Reply < Topic < ActiveRecord ならば、Reply 上でも Topic の table_name が返ります。

primary_key

プライマリキーを返します。標準では “id” ですが、オプションによってはテーブル名が付加されたりします。

inheritance_column

AR クラスの継承を行う際、DB 側にクラス名を保存します。 inheritance_column はクラス名の保存先となるカラム名です。

set_table_name( value=nil, &block )

set_primary_key( value=nil, &block )

set_inheritance_column( value=nil, &block )

これらは、対応する getter メソッドを再定義します。 以前の getter メソッドは original_* に alias されるので (table_name なら original_table_name)、block の中で original_* メソッドを使うと、以前の規則を元に名前を作ることができます。

class_name(table_name = table_name)

DB テーブル名に対応する AR クラス名を返します。 table_name メソッドの逆です。

AR クラスメソッド (カラム)

カラムオブジェクト (ActiveRecord::Base::Column) を扱うクラスメソッドです。

columns

カラムオブジェクトの配列を返します。

columns_hash

カラム名をキーとし、カラムオブジェクトを値とするハッシュを返します。

column_names

カラム名の配列を返します。

content_columns

カラムオブジェクトのうち、プライマリキーや継承クラス名キーそして _count, _id で終わる名前のカラムに対応するものを除いた配列を返します。

column_methods_hash

カラム名をキーとし、カラムに対応するメソッド名 (attr, attr=, attr?, attr_before_type_cast) のシンボル値を値とするハッシュの配列を返します。

AR クラスメソッド (そのほか)

quote(object)

sanitize(object)

サニタイズした値を返します。connection.quote の委譲です。

benchmark(title) {}

Benchmark.measure を用いたベンチマークを行います。

silence {}

logger によるログをとらないブロックを提供します。

AR インスタンスメソッド

AR のインスタンスメソッドをまとめます。

id

id の値を返します。 id は DB のプライマリキーに対応します。

id_before_type_cast

キャストする前の id の値を返します。

quoted_id

quote した id の値を返します。

id=(value)

id の値を設定します。

new_record?

save されていない (まだ DB 上に格納されていない) オブジェクトであるか。

save

DB に格納します。true を返します。

new_record であれば insert し、そうでなければ update します。

ただし、validation チェックに引っかかった場合は格納しません。このときは false を返します。

destroy

save されていれば (new_record? でなければ) DB から行を削除します (SQL 文 (delete from …) を発行します)。

また、どちらにしても freeze します。

clone

AR オブジェクトのクローンを作成します。この際 id を未設定にするので、new_record として扱います。

update_attribute(name, value)

ひとつのカラムを更新し、save します。 これは特に、既存 record の boolean フラグの更新に役立ちます。

戻り値は save の戻り値です。 ただし、validation チェックは回避されます。

update_attributes(attributes)

複数のカラムを更新し、save します。

self.attributes = attributes して save します。戻り値は save の戻り値です。

increment(attribute)

decrement(attribute)

0 で初期化したあと、+1, -1 します。数値を扱うカラムでのみ動作します。

increment!(attribute)

decrement!(attribute)

increment, decrement したあと、save します。

toggle(attribute)

真偽を入れ替えます。self を返します。

toggle!(attribute)

toggle したあと、save します。

reload

リロードします (キャッシュを破棄し、DB から値を取り込みます)。self を返します。

カラム値を取得します。

得られるのはキャスト後の値です。例えば DATE カラムの “2004-12-12” は Date.new(2004, 12, 12) として得られます。

read_attribute メソッド (protected) の alias です。

[]=(attr_name, value)

カラム値を設定します。

write_attribute メソッド (protected) の alias です。

attributes=(attributes)

ハッシュで一度に複数のカラム値を設定します。 attr_protected, attr_accessible の制限を受けます。

attributes

カラム名をキーとし、カラム値 (キャスト後) を値とするハッシュを返します。

attribute_present?(attribute)

attribute はカラム名です。 その名前のカラムが存在してかつ値が nil? でも empty? でもなければ true を、そうでなければ false を返します。

attribute_names

ソートしたカラム名の配列を返します。

column_for_attribute(name)

name に対応するカラムオブジェクトを返します。

おわりに

ごめんなさい。力尽きました。orz 今回もさまざまな形でご意見をいただきました。感謝いたします。

著者について

もりきゅうは ミッタシステム のプログラマです。

著者の連絡先は moriq@moriq.com です。

RubyOnRails を使ってみる 連載一覧