Personal tools
You are here: Home 原稿・資料 ワークス、アリエル共同勉強会 Ruby on Rails - ActiveRecord -
Document Actions

Ruby on Rails - ActiveRecord -

Ruby on Rails ActiveRecordの勉強会資料です。

方針

  • Rubyを知っている前提です(前回の勉強会の資料 http://dev.ariel-networks.com/articles/workshop/ruby/)
  • RDBの基礎知識が前提です
  • なるべく手を動かして目に見える形で説明を進めます
  • Ruby on Rails(以下、Rails)全体は巨大なので、ActiveRecord(ORM層)に話を限定します(Web層は次回)
  • Webから切り離してirb or コマンドラインでActiveRecordを使います

バージョン

$ ruby --version
ruby 1.8.5 (2006-08-25) [i486-linux]

$ rails --version
Rails 2.0.2

Rails2紹介を少しだけ

いくつかのコンポーネントの集合体です。

  • actionpack (WebのMVC)
  • activerecord (ORM)
  • activesupport (Utility)
  • activeresource (REST)
  • actionmailer (Mail)

Rails2紹介を少しだけ - 1分でblog作成(sqlite) -

$ rails myblog
$ cd myblog
$ script/generate scaffold Article title:string body:text
$ rake db:migrate
$ script/server

Rails2紹介を少しだけ - 1分でblog作成(mysql) -

$ rails myblog -d mysql
$ cd myblog
$ script/generate scaffold Article title:string body:text
$ emacs config/database.yml  (mysqlのuseridとpasswordを設定)
$ rake db:create
$ rake db:migrate
$ script/server

sqlite

  • Rails v2.0.2で、デフォルトのデータベースエンジンがsqliteになりました。
  • 実験するにはsqliteは楽なのでお薦めです(ユーザ管理や権限管理が不要なので)。
  • この資料の実験は、オンメモリ版のsqliteをデフォルトとしています。

ActiveRecordのPhilosophy

READMEより

Convention over Configuration:
* No XML-files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word 

Admit the Database:
* Lets you drop down to SQL for odd cases and performance
* Doesn't attempt to duplicate or replace data definitions

ActiveRecordのPhilosophy (超意訳)

  • XMLファイル最低
  • メタプログラミング最高
  • ブラックボックス(黒魔術)ですが何か?
  • SQLは必要悪
  • データ定義は一回書けば充分

Active Recordパターン

ORMパターンの中では、オブジェクト指向寄りな設計です。

  • オブジェクト(=レコードをメモリにロードしたインスタンス)に必要な操作(=インターフェース)を考え、その実装(=SQL)を隠蔽します。
  • 以下、ActiveRecordと書いた時は、固有名詞(RailsのActiveRecordフレームワーク)を指します。

Railsと切り離してActiveRecordだけ使うソースのskelton

ActiveRecordがどんなSQLを発行しているか調べるには、以下にコードを書き足して動かしてください。

#!/usr/bin/ruby
require '/usr/local/ruby2/gems/activerecord-2.0.2/lib/active_record'  # depends on install path
ActiveRecord::Base.default_timezone = :utc
logger = ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
  • Railsプログラムの場合、これらは設定ファイルに隠蔽されます
  • Railsプログラムの場合、timezoneは environment.rb 内で設定します(デフォルトが:utcで無いのは謎)

Railsと切り離してActiveRecordだけ使うソースの例

#!/usr/bin/ruby
require '/usr/local/ruby2/gems/activerecord-2.0.2/lib/active_record'  # depends on install path
ActiveRecord::Base.default_timezone = :utc
logger = ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')

ActiveRecord::Base.connection.execute 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)'

class User < ActiveRecord::Base
end

new_user = User.create(:name => 'master')     # INSERTが走る
p new_user

user = User.find(:first, :conditions => ["name = ?", 'master'])        # SELECTが走る
p user

このコードの出力

SQL (0.001296)   CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)
User Create (0.000270)   INSERT INTO users ("name") VALUES('master')
#<User id: 1, name: "master">
User Load (0.000479)   SELECT * FROM users WHERE (name = 'master') LIMIT 1
#<User id: 1, name: "master">

irbでActiveRecordを実験

一番手軽と思える方法を示します。

irb> require '/usr/local/ruby2/gems/activerecord-2.0.2/lib/active_record'
  • このパスはインストールパス依存です
  • Railsアプリの場合、気にする必要はありません(勝手に面倒を見てくれます)

ActiveRecord::Base を調べる

irb> ActiveRecord::Base.methods
irb> ActiveRecord::Base.instance_methods

どちらもたくさんの出力があります(とりあえず気にしない)

データベースの接続

irb> ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
  • Railsアプリの場合、config/database.yml の設定ファイルで行うので、上の記述は不要です

データベースの接続(YAML版)

次のdatabase.yml(ファイル名は任意)を用意します。

development:
  adapter: sqlite3
  database: ':memory:'
irb> ActiveRecord::Base.configurations = YAML.load_file('database.yml')
irb> ActiveRecord::Base.establish_connection('development')
  • 'development'の部分はYAMLファイル内のキーに対応します
  • Railsアプリの場合、YAMLファイルのロードは気にする必要はありません(勝手に面倒を見てくれます)

ActiveRecordの一番低レベルのところを見てみる(1)

irb> ActiveRecord::Base.connection.class
#=> ActiveRecord::ConnectionAdapters::SQLite3Adapter

irb> ActiveRecord::Base.connection.raw_connection
#=> #<SQLite3::Database:0xb78803d0 @driver=#<SQLite3::Driver::Native::Driver:0xb786b584 @busy_handler={}, @authorizer={}, @callback_data={}, @trace={}>, @statement_factory=SQLite3::Statement, @translator=nil, @handle=#<SWIG::TYPE_p_sqlite3:0xb786b4bc>, @type_translation=false, @closed=false, @results_as_hash=true>
  • データベースエンジンごとのドライバが ActiveRecord::Base.connection の下に隠蔽されています

ActiveRecordの一番低レベルのところを見てみる(2)

irb> ActiveRecord::Base.connection.execute 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)'
irb> ActiveRecord::Base.connection.execute "INSERT INTO users VALUES (NULL,'master')"

irb> ActiveRecord::Base.connection.execute 'SELECT * FROM users'
[{"name"=>"master", 0=>"1", 1=>"master", "id"=>"1"}]
irb> ActiveRecord::Base.connection.execute "SELECT name FROM users"
[{"name"=>"master", 0=>"master"}]
  • このSQLはデータベースエンジン依存です
  • executeメソッドは、エスケープ処理の面倒を見たりはしません
  • なので、当然、Rails的には上記は非推奨です
  • しかし、現実は現実で、必要な場面もあります(特にDDL系)

ActiveRecordの一番低レベルのところを見てみる(3)

それっぽいメソッドを探します(リファレンスかソース見る方が速いですが)

irb> ActiveRecord::Base.connection.methods.select { |e| e =~ /table/ }
["drop_table", "move_table", "rename_table", "copy_table", "tables", "table_alias_for", "table_structure", "copy_table_indexes", "create_table", "table_alias_length", "copy_table_contents", "quote_table_name", "alter_table"]

irb>  ActiveRecord::Base.connection.methods.select { |e| e =~ /select/ }
["select_all_without_query_cache", "select_value", "select", "select_one", "select_values", "select_rows", "select_all", "select_all_with_query_cache"]

irb>  ActiveRecord::Base.connection.methods.select { |e| e =~ /insert/ }
["insert_sql", "insert_fixture", "empty_insert_statement", "insert", "insert_with_query_dirty", "insert_without_query_dirty"]

ActiveRecordの一番低レベルのところを見てみる(4)

irb> ActiveRecord::Base.connection.drop_table :users
irb> ActiveRecord::Base.connection.create_table :users do |t|
       t.string :name
     end

irb> ActiveRecord::Base.connection.insert "INSERT INTO users VALUES (NULL,'master')"

irb> ActiveRecord::Base.connection.select_value 'SELECT name FROM users'
"master"            

irb> ActiveRecord::Base.connection.tables
["users"]

irb> ActiveRecord::Base.connection.table_structure :users
[{5=>"1", "name"=>"id", 0=>"0", 1=>"id", "type"=>"INTEGER", 2=>"INTEGER", "pk"=>"1", 3=>"99", "notnull"=>"99", "cid"=>"0", 4=>nil, "dflt_value"=>nil}, {5=>"0", "name"=>"name", 0=>"1", 1=>"name", "type"=>"varchar(255)", 2=>"varchar(255)", "pk"=>"0", 3=>"0", "notnull"=>"0", "cid"=>"1", 4=>"NULL", "dflt_value"=>"NULL"}]
  • 少しレイヤが上がりました
  • DDL(Data Definition Language)系メソッドは、Migrationで使います(後述)
  • DML(Data Manipulation Language)系メソッドは、この呼び出しは(基本的に)しません(モデルオブジェクトが隠蔽します)

fyi; データベースの接続(MySQL版)

  • rootユーザで作業するのは嫌いなので、新規ユーザを作成します(以下の例は、権限が強いですが)
  • パスワードおよび権限管理は自己責任です(下記の設定が推奨ではありません)
$ mysql -u root -p
[(mysqlの)rootのパスワードをタイプ]

mysql> GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' IDENTIFIED BY 'password';
$ mysql -u mysql -p
mysql> CREATE DATABASE my_development;
irb> ActiveRecord::Base.establish_connection(:adapter => 'mysql', :host => 'localhost', :database => 'my_development', :username => 'mysql', :password => 'password')

RailsのORMの基本方針

  • DDL系はMigrationに隠蔽
  • DML系は(ActiveRecord::Baseを継承したモデルクラスの)オブジェクトアクセスに隠蔽

DDL系

  • テーブル操作(作成、削除、リネーム)
  • カラム操作
  • インデックス操作
  • 制約操作

irbでActiveRecord::Migrationを実験

irb> class MyMigration < ActiveRecord::Migration
       def self.up
         create_table :users do |t|
           t.string :name
         end
       end
     
       def self.down
         drop_table :users
       end
     end

irb> MyMigration.up

これでusersテーブルが作成できます

Migrationとは

  • DDL系操作の履歴管理ができます
  • 個人的感想は...微妙 (DDL系はSQL直書きの方が楽なので、見合う効果があるかどうか不明です)

Railsアプリで使うMigrationの具体例(1)

$ script/generate migration CreateTables
$ script/generate migration AddFooColumns
$ script/generate migration AddFooIndex

とすると、次の雛型ファイルができます。

db/migrate/001_create_tables.rb
db/migrate/002_add_foo_column.rb
db/migrate/003_add_foo_index.rb
  • それぞれのファイルの中身は ActiveRecord::Migration を継承したクラスの実装です
  • それぞれのクラスに self.upとself.down を実装します (これらを呼ぶのは後述するrakeから)
  • ファイル名の先頭についた数値がヴァージョン番号です
  • 上記ファイルを作れば、コマンドラインでは rake db:migrate を実行するだけです

Railsアプリで使うMigrationの具体例(2)

  • db/migrate/001_create_tables.rb の中身は例えば次のような感じです
  • クラス名とファイル名は、Rails流儀で一致させます
class CreateTables < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name
    end
  end
   
  def self.down
    drop_table :users
  end
end
  • upとdownで対称的なDDL操作を記述できれば、正しく履歴管理できます(これが簡単に書ければ苦労しない...)

tips; migrationの強制(やり直す場合など)

色々やっているうちに元に戻したくなった場合は、次のように一度ヴァージョン0に戻してから、改めて rake db:migrate します。

$ rake db:migrate VERSION=0
$ rake db:migrate

fyi; Migrationの内部動作

ヴァージョン番号を保持する1行だけのテーブル(schema_info)を作成しています

irb> ActiveRecord::Schema.define(:version => 1) do
       create_table :users do |t|
         t.string :name
       end
     end

irb> ActiveRecord::Migrator.schema_info_table_name
"schema_info"
irb> ActiveRecord::Migrator.current_version
1

Migrationの継承クラスのself.up、self.downで使う主なメソッド

  • リファレンスかソースを読む方が楽かもしれませんが、irbで当たりをつける方法
irb> ActiveRecord::Base.connection.methods.select { |e| e =~ /add/ }
["add_column_options!", "add_limit!", "add_order_by_for_association_limiting!", "add_index", "add_column", "add_limit_offset!", "add_lock!"]

irb> ActiveRecord::Base.connection.methods.select { |e| e =~ /remove/ }
["remove_index", "remove_column", "remove_subclasses_of"]

irb> ActiveRecord::Base.connection.methods.select { |e| e =~ /rename/ }
["rename_column", "rename_table"]

create_table

create_table :users do |t|
  t.string :name, :null => false
  t.integer :age, :null => false, :default => 20
  t.integer :lock_version, :null => false, :default => 0
end
  • ブロックパラメータ t の型は ActiveRecord::TableDefinition です

上のコードは、より汎用的には、次のように書けます

create_table :users do |t|
  t.column :name, :string, :null => false
  t.column :age, :integer, :null => false, :default => 20
  t.column :lock_version, :integer, :null => false, :default => 0
end

カラムに使えるデータベース型の一覧

irb> ActiveRecord::Base.connection.native_database_types
{:integer=>{:name=>"integer"}, :primary_key=>"INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", :string=>{:limit=>255, :name=>"varchar"}, :date=>{:name=>"date"}, :float=>{:name=>"float"}, :binary=>{:name=>"blob"}, :datetime=>{:name=>"datetime"}, :decimal=>{:name=>"decimal"}, :boolean=>{:name=>"boolean"}, :text=>{:name=>"text"}, :timestamp=>{:name=>"datetime"}, :time=>{:name=>"datetime"}}

プライマリキー

ActiveRecordでは、テーブルには暗黙に id というカラム(型はinteger)ができて、autoincrementなプライマリキーになります。

このお世話が不要な場合は次のように、明示的に禁止します。

create_table(:users, :id => false) do |t|
  t.column :name, :string, :null => false
end

create_table tips

  • railsでサポートしていない bigint なども使えます(ただし、データベースエンジン依存になります)
t.column 'facebookid', :bigint
  • created_at と updated_at カラムは t.timestamps で作れます
  • このふたつのカラムは特別なカラムで、ActiveRecordが勝手に更新の面倒を見てくれます
class CreateTables < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name
      t.timestamps
    end
  end
  def self.down
    drop_table :users
  end
end

外部キー制約(1)

usersテーブルを参照する外部キー制約を持つ articles テーブルがある場合、次のようにします。

create_table :articles do |t|
   t.references :user, :null => false
end
execute "alter table articles add constraint fk_articles_users foreign key (user_id) references users(id)"

外部キー制約(2)

Rails本からの転用です(executeの直接呼び出しを隠蔽する手法)。

次のファイルを lib/migration_helpers.rb として置きます。

module MigrationHelpers
  def foreign_key(from_table, from_column, to_table)
    constraint_name = "fk_#{from_table}_#{to_table}"
    
    execute "alter table #{from_table} add constraint #{constraint_name} foreign key (#{from_column}) references #{to_table}(id)"
  end
end

外部キー制約(3)

001_create_tables.rbファイルを次のようにします。

require 'migration_helpers'
class CreateTables < ActiveRecord::Migration
  create_table :articles do |t|
     t.references :user, :null => false
  end
  foreign_key :articles, :user_id, :users
end

fyi; データベースのカラム型とRubyの型の関係

/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb から引用

def klass
  case type
    when :integer       then Fixnum
    when :float         then Float
    when :decimal       then BigDecimal
    when :datetime      then Time
    when :date          then Date
    when :timestamp     then Time
    when :time          then Time
    when :text, :string then String
    when :binary        then String
    when :boolean       then Object
  end
end

DML系

  • ORMの華はこちら

ActiveRecord::Base を継承したクラス

ActiveRecord::Schema.define do
  create_table :users do |t|
    t.string :name, :null => false
    t.integer :age, :null => false, :default => 20
    t.integer :lock_version, :null => false, :default => 0
  end
end
  • しばらくこのテーブルを前提にします(ageをテーブルに持つ問題は後ほど)。
class User < ActiveRecord::Base
end
  • これだけで usersテーブルを操作できます

テーブル定義情報の参照

irb> User.columns.size  #=>4

irb> User.columns.map { |e| e.name }
["id", "name", "age", "lock_version"]

irb> User.columns.map { |e| e.sql_type }
["INTEGER", "varchar(255)", "integer", "integer"]

レコード作成(1) - INSERT -

user.save で INSERT が走ります

irb> user = User.new
#=> #<User id: nil, name: nil, age: 20, lock_version: 0>
irb> user.name = 'master'
irb> user.age = 31
irb> user.save   #=> true

# SQLで確認 (:memory:ではなく、ストレージを使っている場合は直接確認も可能です)
irb> ActiveRecord::Base.connection.execute 'SELECT * FROM users'
#=> [{"name"=>"master", 0=>"1", 1=>"master", "lock_version"=>"0", 2=>"31", "id"=>"1", 3=>"0", "age"=>"31"}]

レコード作成(2) - INSERT -

もう少し簡単に書けます

irb> user = User.new(:name => 'master', :age => 31)
irb> user.save

更に簡単に書けます(戻り値は受ける必要なし)

irb> user = User.create(:name => 'master', :age => 31)

レコード更新(1) - UPDATE -

オブジェクトに id があれば、saveの呼び出しはSQLのUPDATEになります。

irb> user = User.create(:name => 'master', :age => 31)
irb> user
#=> #<User id: 1, name: "master", age: 31, lock_version: 0>
  
irb> user.age = 32
irb> user.save

irb> ActiveRecord::Base.connection.execute 'SELECT * FROM users'
#=> [{"name"=>"master", 0=>"1", 1=>"master", "lock_version"=>"1", 2=>"32", "id"=>"1", 3=>"1", "age"=>"32"}]
  • user.save で UPDATE が走ります

レコード更新(2) - UPDATE -

updateメソッドを使うと、オブジェクトを介さずにレコードをUPDATEできます

irb> User.update(1, :age => 33)
irb> ActiveRecord::Base.connection.execute 'SELECT * FROM users'
[{"name"=>"master", 0=>"1", 1=>"master", "lock_version"=>"3", 2=>"33", "id"=>"1", 3=>"3", "age"=>"33"}]
  • cf. update_all

レコード更新(3) - UPDATE -

update_attributesはハッシュテーブルを受けるので便利。

irb> user.update_attribute(:age, 33)

irb> user.update_attributes(:name => 'hama', :age => 32)

レコード作成や更新でのDBエラー(1)

NOT NULL制約違反を起こしてみます

irb> user = User.create(:age => 32)
例外発生(saveでも同じ)

条件(不明)によっては、レコード登録が失敗しても、createやsaveは例外を発生させずに単にidが空のオブジェクトを返すことがあります(はまりポイント)

レコード作成や更新でのDBエラー(2)

レコード登録失敗を確実にとらえるには、save! を使います。

user = User.new(:age => 32)
begin
  user.save!
rescue Exception => e
  logger.debug e
end

fyi; 発行されたSQLを見るには

  • irbより、コマンドラインツール(要logger設定)の方が便利です。
  • 「Railsと切り離してActiveRecordだけ使うソースのskelton」を参照

fyi; 少し内部動作の話

  • ActiveRecord::BaseにincludeされたActiveRecord::AttributeMethodsモジュールの method_missing でメソッドを動的に追加しています(テーブルのスキーマを読むのは、クラスごとに一回のみ)。
  • ActiveRecord::Baseの動作は、Migrationのコードとは無関係です(テーブル定義はRDBに問い合わせます)
irb> class User < ActiveRecord::Base; end
irb> User.instance_methods.select { |e| e =~ /name/ }   # まだ name メソッドは無い
irb> user = User.new
irb> User.instance_methods.select { |e| e =~ /name/ }   # まだ name メソッドは無い
irb> user.name = 'master'                               # method_missing
irb> User.instance_methods.select { |e| e =~ /name/ }   # nameメソッド等あり
#=> ["name", "name=", "table_name_prefix", "table_name_suffix", "name?", "attribute_names", "pluralize_table_names"]
irb> User.instance_methods.select { |e| e =~ /age/ }    # 他のカラム用のメソッドもあり
#=> ["age", "age=", "age?"]

# ここで例えカラムが増えても
irb> ActiveRecord::Base.connection.execute 'ALTER TABLE users ADD COLUMN age2 INTEGER'
irb> User.instance_methods.select { |e| e =~ /age2/ }   # 新しいテーブル情報を読むことはしません
#=> []

みんな大好きSELECT(1)

  • id(primary key)で引っ張ってみます。
  • findメソッドを使います。
  • 名前がselectメソッドでないのは...まあ仕方ありません
irb> User.create(:name => 'mao', :age => 23)  # レコード1件だと寂しいので追加

irb> user = User.find(1)
#=> #<User id: 1, name: "master", age: 33, lock_version: 3>

irb> user.class
#=> User(id: integer, name: string, age: integer, lock_version: integer)

みんな大好きSELECT(2)

  • idを複数(複数の引数 or 配列)を渡すと、オブジェクトの配列が返ります
# 以下のふたつは同じ結果です
irb> users = User.find(1,2)
irb> users = User.find([1,2])
#=> [#<User id: 1, name: "master", age: 33, lock_version: 3>, #<User id: 2, name: "mao", age: 23, lock_version: 2>]

irb> users.class
#=> Array

irb> users[0].class
#=> User(id: integer, name: string, age: integer, lock_version: integer)

みんな大好きSELECT(3)

  • where句を書きたい場合、次のようにします
  • :where ではなく :conditions です。理由は不明です
  • :allはArrayを返します。:firstは一件のみ返します(複数候補のうち何が返るかは、:orderの指定が無ければ不定)
irb> User.find(:all, :conditions => "name = 'master'")
irb> User.find(:first, :conditions => "name = 'master'")
irb> User.find(:all)   # これもOK(全レコード)

みんな大好きSELECT(4)

  • 多くのWebアプリでは、ユーザ入力を使ってSELECTします。
  • エスケープ処理が抜けると、SQLインジェクションされます。
  • where句を次の形にすればActiveRecordがエスケープ処理を面倒みてくれます。
irb> iname = 'master'   # 与えられたパラメータ
irb> iage = 33   # 与えられたパラメータ

irb> User.find(:all, :conditions => ['name = ? and age = ?', iname, iage])
#=> [#<User id: 1, name: "master", age: 33, lock_version: 3>]

irb> User.find(:first, :conditions => ['name = ? and age = ?', iname, iage])
#=> #<User id: 1, name: "master", age: 33, lock_version: 3>

みんな大好きSELECT(5)

  • Prepared statement内のプレースホルダーを名前付きにすれば、ハッシュテーブルでパラメータを渡せます
irb> User.find(:first, :conditions => ['name = :name and age = :age', {:name => 'master', :age => 33}])
#=> #<User id: 1, name: "master", age: 33, lock_version: 3>

みんな大好きSELECT(6)

  • findメソッドは引数が色々あるので、リファレンスを参照してください
  • findメソッドの先頭の引数は、数値(id)もしくは、:all、:first のふたつのシンボルのいずれかです

みんな大好きSELECT(7)

  • :conditions で指定していた文字列を次のようにメソッド名で代用できます
  • _and_ はいくつでも続けられます
  • andを含むカラム名では動かないので、カラム名にandを使うのはやめた方が良いです
  • 内部的には method_missing を利用しています
irb> User.find_all_by_name('master')   # :all
irb> User.find_by_name('master')       # :first

irb> User.find_all_by_name_and_age('master', 33)

  • find_by_foo
  • find_by_foo_and_bar
  • find_by_foo_and_bar_and_baz
  • find_or_create_by_foo_and_bar

みんな大好きSELECT(8)

NOT、OR、IS NULL、LIKE、不等号はそのまま書きます。

irb> User.find(:all, :conditions => ['not name = ? and age = ?', 'master', 33])
irb> User.find(:all, :conditions => ['not (name = ? and age = ?)', 'master', 33])
irb> User.find(:all, :conditions => ['name = ? or age = ?', 'master', 33])
irb> User.find(:all, :conditions => ['name = ? and age is null', 'master'])
irb> User.find(:all, :conditions => ['name like ?', 'ma%'])
irb> User.find(:all, :conditions => ['name <> ?', 'daimao'])
irb> User.find(:all, :conditions => ['age > ?', 20])

SELECTの失敗(1)

  • 存在しないidで探すと、例外が発生します
irb> user = User.find(4)
##=> ActiveRecord::RecordNotFound: Couldn't find User with ID=4

irb> users = User.find([1,2,10])
##=> ActiveRecord::RecordNotFound: Couldn't find all Users with IDs (1,2,10) (found 2 results, but was looking for 3)

SELECTの失敗(2)

存在しない where 句で探すと、nilが返ります

irb> User.find_by_name('ham')
#=> nil

Userオブジェクトの属性列挙

irb> user = User.find(1)
irb> user.attributes.each { |k,v| p k.to_s + ', ' + v.to_s; }
"name, master"
"lock_version, 0"
"id, 1"
"age, 31"

Userクラスにメソッド追加(1)

class User < ActiveRecord::Base
end

これだけでも色々できることが分かったと思いますが、ActiveRecordの名前の元となったActive Recordパターン的には、このクラスのオブジェクトへの操作をメソッド定義すると、「らしく」なります。

Userクラスにメソッド追加(2)

create_table :users do |t|
  t.string :name, :null => false
  t.integer :age, :null => false, :default => 20
  t.integer :lock_version, :null => false, :default => 0
end

現在のテーブルはこうですが、ageをテーブルに持つのは変なので、

create_table :users do |t|
  t.string :name, null => false
  t.date :birthday, :null => false
  t.integer :lock_version, :null => false, :default => 0
end

以下、テーブルはこれにします。

Userクラスにメソッド追加(3)

テーブルにageカラムが無くても、今までどおりageを参照したい場合、Userクラスにメソッドを追加します(誤差は無視してください)。

class User
  def age
    (Date.today - birthday).to_i / 365
  end
end
user = User.new(:name => 'master', :birthday => Date.new(1976,6,1))
user.age  #=>31

リレーション

joinはRDBの華

  • ActiveRecordで、典型的なjoinはオブジェクト参照に隠蔽できます

関連とテーブル設計のcheat sheet

1対多 or (1|0)対多

  • 多の側に外部キーを持たせるのが定石
  • 結合テーブルを使う手段もある(1も多のひとつと見なせば理屈上は一貫性がある。パフォーマンスとのトレードオフ)

1対1 or 1対(0|1)

  • 1対多の一部。どちらに外部キーを持たせるかは状況次第
  • 1対(0|1)なら、(0|1)の側に外部キーを持たせるのが定石
  • 結合テーブルを使う手段もある(1も多のひとつと見なせば理屈上は一貫性がある。パフォーマンスとのトレードオフ)

多対多

  • 結合テーブル(aka 交差テーブル、関連テーブル)で、ふたつのテーブルへの外部キーを持たせるのが定石

1対1の関連の例(1)

user  1 _____ 0..1  blog

ひとりの人が複数のblogを持たない世界を仮定します。

ActiveRecord::Schema.define do
  create_table :blogs do |t|
    t.string :title
    t.string :url
    t.references :user, :null => false
    t.timestamps
  end
end

1対1の関連の例(2)

class User < ActiveRecord::Base
  has_one :blog
end

class Blog < ActiveRecord::Base
  belongs_to :user
end

この宣言で次のことが可能になります。

  • (RDBの)関連を、(オブジェクトの)関連として扱える(joinの隠蔽)
  • 外部キー制約の隠蔽
  • 関連テーブルの更新のトランザクションの隠蔽

1対1の関連の例(3)

所有者のいないblogは存在できない世界にしているので(RDB的に外部キー制約)、次はエラー(例外)になります。

blog = Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
blog.save   
##=> ActiveRecord::StatementInvalid

1対1の関連の例(4)

owner = User.new(:name => 'master', :birthday => Date.new(1976,6,1))
owner.blog = Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
owner.save

saveのタイミングで、次のSQLが走ります(トランザクションをサポートしていれば、同じトランザクションに入ります)。

INSERT INTO users ("name", "lock_version", "birthday") VALUES('master', 0, '1976-06-01')
INSERT INTO blogs ("title", "url", "user_id") VALUES('ariel area', 'http://dev.ariel-networks.com', 1)

1対1の関連の例(5)

User.create(:name => 'master', :birthday => Date.new(1976,6,1))
owner = User.find(:first, :conditions => ["name = 'master'"])
owner.blog = Blog.new(:title => 'mixi nikki', :url => 'http://mixi.jp/secret')

owner.blog にオブジェクトを代入したタイミングで、次のSQLが走ります。

INSERT INTO blogs ("title", "url", "user_id") VALUES('mixi nikki', 'http://mixi.jp/secret', 1)

1対1の関連の例(6)

  • ここまでの話は User クラスの has_one :blog の働きです
  • belongs_to の働きとは独立です
  • has_oneとbelongs_toはセットで指定する必要はありません(どちらか片方だけも可)

1対1の関連の例(7)

Blogクラスの belongs_to :user の働きは?

owner = User.new(:name => 'master', :birthday => Date.new(1976,6,1))
blog = Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
owner.blog = blog
p blog.user   #=>nil
owner.save
p blog.user   #=>#<User id: 1, name: "master", birthday: "1976-06-01", lock_version: 0>
  • このようにblogオブジェクトからuserを参照できます。

1対1の関連の例(8)

area = Blog.find(:first, :conditions => ["title = 'ariel area'"])
p area.user

area(型はBlogクラス)のuserを参照したタイミングで、次のSQLが走ります(belongs_toの働き)。

SELECT * FROM users WHERE (users."id" = 1)

1対1の関連の例(9)

典型例

blog = Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
blog.user = User.new(:name => 'master', :birthday => Date.new(1976,6,1))
blog.save     #[1]

new_blog = Blog.new(:title => 'mixi nikki', :url => 'http://mixi.jp/secret')
new_blog.user = blog.user
new_blog.save #[2]

saveのタイミングで、次のSQLが走ります(belongs_toの働き)。 [1]のふたつのINSERTは、トランザクションをサポートしていれば、同じトランザクションに入ります。

[1]
INSERT INTO users ("name", "lock_version", "birthday") VALUES('master', 0, '1976-06-01')
INSERT INTO blogs ("title", "url", "user_id") VALUES('ariel area', 'http://dev.ariel-networks.com', 1)

[2]
INSERT INTO blogs ("title", "url", "user_id") VALUES('mixi nikki', 'http://mixi.jp/secret', 1)

1対多の関連の例(1)

1対1 とそんなに違いはありません。

試しに、ひとりが複数のblogを持てる世界に変えます。

user  1 _____ *  blog
  • has_one :blog を has_many :blogs にします(複数形に注意)
  • Userオブジェクトから参照できるBlogオブジェクトが、コレクション(型はArray)になります

1対多の関連の例(2)

class User < ActiveRecord::Base
  has_many :blogs
end

class Blog < ActiveRecord::Base
  belongs_to :user
end
  • Blogクラスは変わっていません。相手がhas_oneだろうとhas_manyだろうと、belongs_toの働きは同じです。
  • has_one 同様、has_manyとbelongs_toもセットで使う必要はありません(どちらか片方だけも可)

1対多の関連の例(3)

典型例

owner = User.create(:name => 'master', :birthday => Date.new(1976,6,1))
owner.blogs << Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
owner.blogs << Blog.new(:title => 'mixi nikki', :url => 'http://mixi.jp/secret')
user = User.find(:first, :conditions => ["name = 'master'"])
user.blogs.each { |blog| p blog }

user.blogsを参照したタイミングで、次のSQLが走ります。

SELECT * FROM blogs WHERE (blogs.user_id = 1)

has_oneとhas_manyの別名

クラス名と一致させないことも可能です。

class User < ActiveRecord::Base
  has_many :sites, :class_name => 'Blog'
end

class Blog < ActiveRecord::Base
  belongs_to :author, :class_name => 'User'
end
user.sites
blog.author

のように参照できます

条件付きhas_oneとhas_many(1)

  • has_oneやhas_manyは条件付きでいくらでも指定可能です。
  • 結局、(SQLの)結果セットをオブジェクトの参照でどう見せるか、の指定にすぎないからです。
  • パフォーマンス(何も考えないhas_manyはメモリをバカ食いする可能性)のためにも、適切な条件付きhas_manyは重要です。

条件付きhas_oneとhas_many(2)

class User < ActiveRecord::Base
  has_many :blogs
  has_many :ariel_blogs, :class_name => 'Blog', :conditions => "url like '%ariel%'"
  has_one :first_blog, :class_name => 'Blog', :order => 'created_at DESC'
end
  • ariel_blogsやfirst_blogで関連を参照できます

条件付きhas_oneとhas_many(3)

user = User.find(1)
user.blogs << Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
user.blogs << Blog.new(:title => 'mixi nikki', :url => 'http://mixi.jp/secret')
user.blogs << Blog.new(:title => 'facebook', :url => 'http://www.facebook.com')

user.blogs.size   #=>3
user.ariel_blogs.size  #=>1
user.first_blog
#=> #<Blog id: 1, title: "ariel area", url: "http://dev.ariel-networks.com", user_id: 1>

多対多の関連の例(1)

member  * _____ *  group

以下のような結合テーブルを使います。

ActiveRecord::Schema.define do
  create_table :members do |t|
    t.string :name, :null => false
  end
  create_table :groups do |t|
    t.string :name, :null => false
  end
  create_table(:group_and_members, :id => false) do |t|
    t.references :group, :null => false
    t.references :member, :null => false
  end
  add_index :group_and_members, [:group_id, :member_id]
  add_index :group_and_members, :member_id
end

多対多の関連の例(2)

結合テーブル

  • group_and_members のようにふたつのテーブル名を and でつなぐのがRails流です
  • プライマリキーのid列は、目的が結合のみであれば不要です
  • 結合テーブルにカラムを追加して、対応クラスに属性を追加してオブジェクトとして扱う場合はid列が必要です
  • ふたつの外部キーにインデックスを張るのが定石です(前ページのadd_index参照)

多対多の関連の例(3)

class Member < ActiveRecord::Base
  has_many :group_and_members
  has_many :groups, :through => :group_and_members
end

class Group < ActiveRecord::Base
  has_many :group_and_members
  has_many :members, :through => :group_and_members
end

class GroupAndMember < ActiveRecord::Base
  belongs_to :member
  belongs_to :group
end

多対多の関連の例(4)

member = Member.new(:name => 'master')
member.save   # need to save
group = Group.new(:name => 'ariel')
group.save    # need to save

member.groups << group  # [1]

[1]の時に次のSQLが走ります。

INSERT INTO group_and_members ("member_id", "group_id") VALUES(1, 1)

多対多の関連の例(5)

master = Member.find(:first, :conditions => "name = 'master'")
master.groups.each { |group| p group }

master.groupsを参照した時、次のSQLが走ります。

SELECT groups.* FROM groups INNER JOIN group_and_members ON groups.id = group_and_members.group_id WHERE ((group_and_members.member_id = 1))

多対多の関連の例(6)

has_many 自体の動作は同じなので、条件付きの has_many も可能です。

class Member < ActiveRecord::Base
  has_many :group_and_members
  has_many :groups, :through => :group_and_members
  has_many :a_groups, :source => :group, :conditions => "name like 'a%'", :through => :group_and_members
end

多対多の関連の例(7)

member = Member.new(:name => 'master')
member.save   # need to save

member.groups << Group.create(:name => 'ariel')
member.groups << Group.create(:name => 'google')

master = Member.find(:first, :conditions => "name = 'master'")
master.groups.each { |group| p group }   #[1]
master.a_groups.each { |group| p group } #[2]

[1]と[2]で走るSQLはそれぞれ次のようになります。

[1]
SELECT groups.* FROM groups INNER JOIN group_and_members ON groups.id = group_and_members.group_id WHERE ((group_and_members.member_id = 1))

[2]
SELECT groups.* FROM groups INNER JOIN group_and_members ON groups.id = group_and_members.group_id WHERE ((group_and_members.member_id = 1) AND ((name like 'a%')))

多対多の関連の例(8)

  • has_and_belongs_to_many (HABTM)

:through 登場以降の存在がいまいち不明なので無視します。

N+1問題(1)

N+1問題は、ORMの典型的なパフォーマンス問題です。

次のコードは N+1 (Nは最初のSELECTの結果セットのレコード数)のSELECTが発行されます(以下の例では3+1回)。

user = User.create(:name => 'master', :birthday => Date.new(1976,6,1))
user.blogs << Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
user.blogs << Blog.new(:title => 'mixi nikki', :url => 'http://mix.jp/secret')
user.blogs << Blog.new(:title => 'facebook', :url => 'http://www.facebook.com')

Blog.find(:all).each do |blog|
  p blog.title
  p blog.user.name
end

N+1問題(2)

前ページのコードをコマンドラインで実行した時の出力(eachループ)

Blog Load (0.001043)   SELECT * FROM blogs
"ariel area"
User Load (0.000633)   SELECT * FROM users WHERE (users."id" = 1)
"master"
"mixi nikki"
User Load (0.000604)   SELECT * FROM users WHERE (users."id" = 1)
"master"
"facebook"
User Load (0.000586)   SELECT * FROM users WHERE (users."id" = 1)
"master"

ベタに4回SELECTが発行されています

N+1問題(3)

eachループを次のように書き換えると、SELECTが1回になります。

Blog.find(:all, :include => :user).each do |blog|
  p blog.title
  p blog.user.name
end

N+1問題(4)

前ページのコードをコマンドラインで実行した時の出力(eachループ)

SELECT blogs."id" AS t0_r0, blogs."title" AS t0_r1, blogs."url" AS t0_r2, blogs."user_id" AS t0_r3, blogs."created_at" AS t0_r4, blogs."updated_at" AS t0_r5, users."id" AS t1_r0, users."name" AS t1_r1, users."birthday" AS t1_r2, users."lock_version" AS t1_r3 FROM blogs LEFT OUTER JOIN users ON users.id = blogs.user_id
"ariel area"
"master"
"mixi nikki"
"master"
"facebook"
"master"

SQLの結果セットのキャッシュ(1)

user = User.create(:name => 'master', :birthday => Date.new(1976,6,1))
user.blogs << Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
user.blogs << Blog.new(:title => 'mixi nikki', :url => 'http://mix.jp/secret')

master = User.find(:first, :conditions => "name = 'master'")
master.blogs.each { |b| p b.title }   #[1]
master.blogs.each { |b| p b.title }   #[2]

[1]でSELECTが走りますが、[2]ではキャッシュを見るため、SQLが抑制されます。

SQLの結果セットのキャッシュ(2)

user = User.create(:name => 'master', :birthday => Date.new(1976,6,1))
user.blogs << Blog.new(:title => 'ariel area', :url => 'http://dev.ariel-networks.com')
user.blogs << Blog.new(:title => 'mixi nikki', :url => 'http://mix.jp/secret')

master = User.find(:first, :conditions => "name = 'master'")
master.blogs.each { |b| p b.title }
user.blogs << Blog.new(:title => 'facebook', :url => 'http://www.facebook.com')  # INSERT
master.blogs.each { |b| p b.title }   # 'facebook'は出ない

SQLが抑制されるため、RDBの最新状態を見ていません

SQLの結果セットのキャッシュ(3)

reloadすると強制的にSELECTできます

master = User.find(:first, :conditions => "name = 'master'")
master.blogs.each { |b| p b.title }
user.blogs << Blog.new(:title => 'facebook', :url => 'http://www.facebook.com')  # INSERT
master.reload
master.blogs.each { |b| p b.title }   # SELECTが走り、'facebook'が出る

トランザクション(1)

  • ここまで見たように、うまくActiveRecordを使うと、トランザクションを隠蔽できます(プログラマが意識しなくて良い)
  • 明示的な宣言が必要な時も多々あります

トランザクション(2)

ここだけ再び、birthdayカラムではなく、ageカラムがあるという前提にします。

ユーザは同時に年を取る世界だと仮定します。 次のようにtransactionメソッドで囲みます。

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

class User < ActiveRecord::Base
  def age_up
    self.age = self.age + 1
    self.save
  end
end

user1 = User.create(:name => 'mao', :age => 23)
user2 = User.create(:name => 'master', :age => 31)

User.transaction(user1, user2) do     # BEGINとCOMMITでINSERTが囲まれます
  user1.age_up
  user2.age_up
end

トランザクション(3)

例外が起きてトランザクションを失敗した時、ロールバックされる様子を示します。

class User < ActiveRecord::Base
  class OldException < StandardError; end
  def age_up
    self.age = self.age + 1
    if self.age > 30
      raise OldException, "he is ojisan!"
    end
    self.save
  end
end

user1 = User.create(:name => 'mao', :age => 23)
user2 = User.create(:name => 'master', :age => 31)

begin
  User.transaction(user1, user2) do
    user1.age_up
    user2.age_up      # raise Exception
  end
rescue Exception => e
end

user1.age   #=>23
user2.age   #=>31

単純な排他制御(楽観的lock)(1)

  • lock_versionという名前のカラムがあると、次のコードで例外が起きます
user = User.create(:name => 'master', :birthday => Date.new(1976,6,1))
user0 = User.find(:first, :conditions => "name = 'master'")

user.birthday = user.birthday.succ
user.save   #=> true

user0.birthday = user0.birthday.next_month
user0.save  ##=>例外

単純な排他制御(楽観的lock)(2)

前ページのコードの出力例

UPDATE users
SET "name" = 'master', "birthday" = '1976-06-02', "lock_version" = 1
WHERE id = 1
AND "lock_version" = 0

UPDATE users
SET "name" = 'master', "birthday" = '1976-07-01', "lock_version" = 1
WHERE id = 1
AND "lock_version" = 0

/usr/local/ruby2/gems/activerecord-2.0.2/lib/active_record/locking/optimistic.rb:85:in `update_without_callbacks': Attempted to update a stale object (ActiveRecord::StaleObjectError)

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.