Ruby on Rails - ActiveRecord -
Ruby on Rails ActiveRecordの勉強会資料です。
Ruby on Rails - ActiveRecord -
2008/3/19
方針
- 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
- http://localhost:3000/articles/ にアクセスすると CRUD はできます
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
- 同じく http://localhost:3000/articles/ にアクセスすると CRUD はできます
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)
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
関連とテーブル設計の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%')))
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)
その他の話題(次回のWeb周りと合わせて説明)
- validation
- callback methods
- acts_as