モデルに関する基礎知識
- モデル…データモデルとして扱うデフォルトのデータ構造
- MVCアーキテクチャの「Model」のことである
- Active Record…データベースとやり取りするデフォルトのRailsライブラリ
- ORMフレームワークとしての機能を含む
- デザインパターンとしてのActive Recordに由来する名称
- ただ、RailsライブラリのActive Recordは固有名詞である
- LaravelのEloquentライブラリに相当する
- マイグレーション(Migration)
- Acgtive Recordの機能の一つ
- データの定義をRubyで記述することができる
- LaravelにもDatabase:Migrationsという機能が存在する
永続化の必要性
4章でUser
というクラスを定義して使用してみました。name
属性とemail
属性を持ち、formatted_email
というメソッドが使用できるクラスでしたね。
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email
"#{@name} <#{@email}>"
end
end
しかし、Ruby上でクラスを定義してオブジェクトを生成しただけだと、コンソールからexitした時点でクラスもオブジェクトも消えてしまいます。Webフレームワークで扱う動的データに重要な永続性がない状態ですね。この章では、「永続性を実現し、簡単に消えることのないユーザーのモデル」を構築していきます。
データベースの移行
そもそもRDBについて
RDBの主たる構成要素は、1個以上のテーブルです。テーブルの構成は、大雑把に言えば以下の2つです。
- データ行(レコード)
- カラム(列)
- データの属性
例えば、nameとemailを持つユーザーオブジェクトを保存するのであれば、name
カラムとemail
カラムを持つusers
テーブルを生成するわけです。
Userのデータモデルのスケッチは、PlantUMLのentity
を使って表現すると、以下のような感じになります。
Userのデータモデルを仮に定義してみる
以下はusers
テーブルに含まれるデータのサンプル1です。
以下はUserのデータモデルのスケッチ2です。
rails generate model
コマンドによるモデルの生成
コントローラを生成する際に、rails generate controller
というコマンドを使いました。とくれば、モデルを生成する際に使うコマンドはrails generate model
というコマンドになります。
今回生成するモデルの条件は以下です。
- モデル名…User
- コントローラ名に複数形を用いるのに対し、モデル名には単数形を用いる
-
string
型のname
属性を持つ -
string
型のemail
属性を持つ
上述前提に基づき、早速モデルを生成してみましょう。
# rails generate model User name:string email:string
Running via Spring preloader in process 2236
invoke active_record
create db/migrate/20190928080951_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
無事モデルを生成できたようです。
マイグレーション - 生成したモデルを実際にRDBに反映する
rails generate model
したのみの現状だと、「Railsフレームワーク側でデータモデルは定義されているものの、RDB上にデータモデルに対応するテーブルは存在しない」という状態です。データを永続化するためには、RDB上にもデータモデルに対応するテーブルを生成する必要があります。
Railsフレームワーク側をRDB側に反映させるための仕組みとして、Railsでは、マイグレーションと呼ばれる仕組みが存在します。マイグレーションの実体はRubyコードです。今回はrails generate model
によって自動生成3が済んでいます。
マイグレーションのコード解説
以下は自動生成されたマイグレーションです。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
このコードで重要な事柄は以下です。
- マイグレーションファイル名の先頭には、マイグレーション生成時点のタイムスタンプが追加されている
- マイグレーションのコンフリクトはまず発生しない
- データベースに与える変更は、
change
メソッドによって定義される - Railsでは
create_table
というイテレータメソッドが定義されている- RDB上にテーブルを作成するためのメソッドである
-
t
というオブジェクトをブロック変数として用いる - 繰り返すのは「生成するカラムを定義する」という動作である
-
string
というメソッドであれば、生成対象としてstring
型のカラムを定義する
-
-
t.timestamps
という特別なメソッドが存在する-
created_at
とupdated_at
という2つのマジックカラムを生成するメソッドである-
created_at
は、レコードの作成時刻を自動で記録するカラムである -
updated_at
は、レコードの最終更新時刻を自動で記録するカラムである
-
-
- テーブルに対するシンボル名が
:users
という複数形であることに注意- 「テーブルは複数のレコードから成る」という構造を反映した命名規則
- 一方で、モデル名はUserという単数形である
- 単一レコードを対象として処理を行うオブジェクトであるため
コードの解説を踏まえた、Userの完全なデータモデル
マジックカラムを含めた、Userの完全なデータモデルは以下のようになります。
マイグレーションの適用
rails db:migrate
コマンドを実行することにより、マイグレーションのコードを実行することができます。これを「マイグレーションの適用(migrating up)と呼びます。
# rails db:migrate
== [timestamp] CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0177s
== [timestamp] CreateUsers: migrated (0.0179s) =============================
実際に生成されたデータベース・テーブルの実体
マイグレーションの実行により、db/development.sqlite3
という名前のファイルが生成されました。これはSQLiteデータベースの実体です。
上述スクリーンショットは、Visual Studio CodeのSQLite拡張(alexcvzz.vscode-sqlite
)の機能によってusers
テーブルの構成を確認したものです。
id
というカラムは、Railsにより自動で生成されたカラムで、レコードを一意に識別するために使うものです。主キーであることを示す鍵アイコンが頭についていますね。
id
が主キーであることを踏まえた、より厳密なUserの完全なデータモデルは以下のようになります。
演習 - データベースの移行
1. あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
db/schema.rb
は、データベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われるRubyコードです。
まずは、db/migrate/[timestamp]_create_users.rb
のコードを見てみましょう。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
次に、db/schema.rb
のコードを見てみます。
ActiveRecord::Schema.define(version: [timestamp]) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
db/migrate/[timestamp]_create_users.rb
に対し、db/schema.rb
には以下のような違いがあるのが目に付きます。
- 同じActiveRecordのメソッドが使われているが、使われているメソッドはそれぞれ違う
-
db/migrate/[timestamp]_create_users.rb
ではActiveRecord::Migration[5.1]
-
db/schema.rb
ではActiveRecord::Schema.define
-
-
[timestamp]_create_users.rb
のファイル名にある[timestamp]
が、db/schema.rb
では、ActiveRecord::Schema.define
の第1引数(ハッシュ)の:version
キーに対する値として渡されている -
t.datetime
を除けば、呼び出しているメソッドの順番・ブロックの構成に違いはない- ブロック変数の名前はいずれも
t
である
- ブロック変数の名前はいずれも
- 各メソッドの第1引数の型が異なる
-
db/migrate/[timestamp]_create_users.rb
ではシンボル -
db/schema.rb
では文字列
-
-
t.timestamp
とt.datetime
の違い -
db/schema.rb
のcreate_table
には、force: :cascade
という第2引数が渡されている -
db/schema.rb
で呼び出されているt.datetime
には、null: false
という第2引数が渡されている
2. rails db:rollback
コマンドを実行後、db/schema.rb
の内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。
演習1.が終了した時点で、rails db:rollback
コマンドを実行します。
# rails db:rollback
== [timestamp] CreateUsers: reverting ======================================
-- drop_table(:users)
-> 0.0176s
== [timestamp] CreateUsers: reverted (0.0283s) =============================
実行後のdb/schema.rb
の内容は以下のようになりました。
ActiveRecord::Schema.define(version: 0) do
end
ポイントは以下でしょうか。
-
ActiveRecord::Schema.define
の第1引数のキー:version
に対する値が0
になっている- ロールバック前は、マイグレーションのタイムスタンプに対応する値であった
-
create_table
メソッド以下がきれいさっぱり削除された
rails db:migrate
とrails db:rollback
の関係について追加の解説
- Railsの
change
メソッドには、create_table
とdrop_table
がそれぞれ対応関係であるという情報が格納されている-
create_table
とdrop_table
に限らず、RDBに対する可逆的な操作に対する逆方向のマイグレーションは、Railsが持つ機能によって簡単に行うことができる
-
- 「あるカラムを削除する」等の不可逆的なマイグレーションの場合、
up
メソッドとdown
メソッドを別々に定義する必要がある - 詳細は、RailsガイドのActive Record マイグレーションを参照のこと
3. もう一度rails db:migrate
コマンドを実行し、db/schema.rb
の内容が元に戻ったことを確認してください。
# rails db:migrate
== [timestamp] CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0187s
== [timestamp] CreateUsers: migrated (0.0203s) =============================
実行後のdb/schema.rb
の内容は以下のようになりました。
ActiveRecord::Schema.define(version: [timestamp]) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
db/schema.rb
の内容が元に戻りました。
ロールバック前の[timestamp]
の値と、再度マイグレーションを実行した後の[timestamp]
の値は変わりません。ここは大きなポイントではないかと思います。
modelファイル
マイグレーションふりかえり
-
users
テーブルを作成し、development.sqlite3
の内容を更新する -
users
テーブルに、以下5つのカラムを作成する-
id
: 主キー、integer型 -
name
: varchar型 -
email
: varchar型 -
created_at
: マジックカラム、datetime型 -
updated_at
: マジックカラム、datetime型
-
modelファイルの内容
rails generate model User
コマンドで生成された直後のapp/models/user.rb
の内容は、以下のようになっています。
class User < ApplicationRecord
end
-
User
というクラスが定義されている-
User
クラスはApplicationRecord
クラスを継承している
-
- 他には特に何も定義されていない
ApplicationRecord
クラスのコードは、app/models/application_record.rb
にて定義されています。
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
ここでは、「ApplicationRecord
クラスは、ActiveRecord::Base
クラスを継承している」という事実が重要です。「Railsにおけるモデルという概念を知るにあたり、まず学習する必要があるのは、ActiveRecord::Base
クラスについての知識である」ということですね。
演習 - modelファイル
1. Railsコンソールを開き、User.new
でUser
クラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください。
>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
2. 同様にして、ApplicationRecord
がActiveRecord::Base
を継承していることについて確認してみてください。
>> ApplicationRecord.superclass
=> ActiveRecord::Base
ユーザーオブジェクトを作成する
Railsコンソールのサンドボックスモード
Railsコンソールには「サンドボックスモード」という特別なモードが存在します。「コンソール中でデータベースに対して加えた変更は、コンソールから抜ける際に自動で全てロールバックされる」というモードです。
Railsコンソールをサンドボックスモードで実行するには、rails console
コマンドに--sandbox
オプションを付けて実行します。
# rails console --sandbox
...略
Loading development environment in sandbox (Rails 5.1.6)
Any modifications you make will be rolled back on exit
>>
確かに「Any modifications you make will be rolled back on exit(ここで行ったすべての変更は終了時にロールバックされます)」というメッセージが表示されています。
モデルを使うと、require
せずともユーザーオブジェクトにアクセスできる
4章の学習において、「ユーザーオブジェクトを一から定義し、実際に使ってみる」という学習を行いました。このときは、「Railsコンソールにおいては、ユーザーオブジェクトを定義したコードをrequire
しなければ、ユーザーオブジェクトにアクセスできない」という挙動でした。
しかしながら、モデルにてユーザーオブジェクトが定義されている場合、RailsコンソールがRailsの環境を自動で読み込む際にユーザーオブジェクトも読み込んでくれます。こうして、require
せずともユーザーオブジェクトにアクセスできる、というわけなのです。
実際にユーザーオブジェクトを作成し、色々といじってみる
Railsコンソールで新しいユーザーオブジェクトを実際に作成してみます。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
上述のとおり、User.new
を引数なしで呼ぶと、すべての属性がnil
のユーザーオブジェクトが生成されます。
User.new
は、引数としてハッシュを取ることができます。具体的には、「属性名をキーとし、属性値を値とするハッシュ」を取り、引数のハッシュの内容に基づき、ユーザーオブジェクトのインスタンス変数4を初期化するのです。
以下は、初期化ハッシュのname属性とemail属性を与えてUser.new
を呼んだ場合の例です。Userオブジェクトのname属性とemail属性が期待通り設定されていることがわかります。
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
valid?
メソッド
Active Recordには、「有効性(Validity)」という重要な概念が存在します。後で詳しく学習しますが、現時点では「modelクラスオブジェクトの有効性は、valid?
というメソッドによって確認することができる」というルールを覚えておきましょう。
>> user
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
>> user.valid?
=> true
「modelクラスオブジェクトの有効性は、RDBにデータが格納されているかどうかには関係ない」ことも重要なので覚えておきましょう。
RDBにUserオブジェクトを保存する
現時点では、まだRDBにデータは格納されていません。modelクラスオブジェクトにあるデータをRDBに格納するためには、modelクラスオブジェクトのsave
メソッドを実行する必要があります。例えば、現在言及しているUserオブジェクトをRDBに保存するためには、user
オブジェクトのsave
というメソッドを呼び出す必要があります。
>> user
#<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
>> user.save
(5.7ms) SAVEPOINT active_record_1
SQL (25.6ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", "2019-09-29 22:50:02.984378"], ["updated_at", "2019-09-29 22:50:02.984378"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
save
メソッドは、成功すればtrue
を、失敗すればfalse
を返します。
Railsコンソールにおけるsave
メソッドの実行の流れを見ると、実際にRDBMS(開発環境ではSQLite3ですね)で実行されているSQL文の内容も見ることができます。Railsチュートリアルの範囲内で生のSQLが必要になる場面はほぼないですが、Active Recordに対応するSQL文を眺めたり推測したりすることには意味があると思います。
save
メソッド実行後のユーザーオブジェクト
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
-
save
メソッド実行前-
id
:nil
-
created_at
:nil
-
updated_at
: `nil
-
-
save
メソッド実行後-
id
:1
-
created_at
: 現在の日時 -
updated_at
: 現在の日時
-
以上のように内容が変化しました。
Userモデルのインスタンスの各属性に、ドット記法を用いてアクセスする
Userモデルのインスタンスの各属性には、Rubyのドット記法を用いてアクセスすることが可能です。以下のRailsコンソール出力は、Userモデルにuser
というインスタンスが存在する状態で、user
のname
・email
・updated_at
各属性にアクセスしてみた結果です。
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Sun, 29 Sep 2019 22:50:02 UTC +00:00
User.create
メソッド
Railsのモデルオブジェクトには、create
というメソッドが定義されます。create
メソッドは、モデルの生成とRDBへの保存を同時に行うメソッドです。引数はハッシュで、当該ユーザーオブジェクトのインスタンスに値を設定するために用います。
>> User.create(name: "A Nother", email: "another@example.org")
(5.4ms) SAVEPOINT active_record_1
SQL (1.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "A Nother"], ["email", "another@example.org"], ["created_at", "2019-10-01 22:40:17.812044"], ["updated_at", "2019-10-01 22:40:17.812044"]]
(0.3ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
(0.2ms) SAVEPOINT active_record_1
SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Foo"], ["email", "foo@bar.com"], ["created_at", "2019-10-01 22:40:47.991537"], ["updated_at", "2019-10-01 22:40:47.991537"]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2019-10-01 22:40:47", updated_at: "2019-10-01 22:40:47">
確かにSQLのINSERT
文が実行過程で呼ばれていますね。
create
オブジェクトの戻り値は、モデルオブジェクト自身です5。上述実行例の2番目のコマンドのように、create
オブジェクトの戻り値を、何らかの変数に代入することもできます。
User.destroy
メソッド
destroy
はcreate
の逆です。
>> foo.destroy
(0.2ms) SAVEPOINT active_record_1
SQL (1.7ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
(0.4ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2019-10-01 22:40:47", updated_at: "2019-10-01 22:40:47">
確かにSQLのDELETE
文が実行過程で呼ばれていますね。
しかしながら、destroy
メソッドが実行されてなお、当該モデルオブジェクトは(RDB上には残っていませんが)メモリ内に残っています。
>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2019-10-01 22:40:47", updated_at: "2019-10-01 22:40:47">
この先学習すること
- RDB上からオブジェクトが削除されたことを知る方法
- 保存して削除されていないモデルオブジェクトを使って、いかにしてRDBから情報を取得するか
演習 - ユーザーオブジェクトの作成
1. user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
>> user.name.class
=> String
>> user.email.class
=> String
2. created_atとupdated_atは、どのクラスのインスタンスでしょうか?
>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class.superclass
=> Object
ActiveSupport::TimeWithZone
というクラスです。以下のような特徴があります。
- ActiveSupport名前空間に所在する
- RailsのActive Supportライブラリにより実装されたクラスである
-
Object
クラスを直接継承している
ユーザーオブジェクトを検索する
Active Recordにおいて、RDB上に存在するレコードを検索する方法は多数用意されています。Railsチュートリアルでは、以下のメソッドが紹介されています。これらはいずれもモデルオブジェクトのクラスメソッドです。
-
find
メソッド -
find_by
メソッド -
first
メソッド allメソッド
find
メソッド
レコードを検索するActive Recordのメソッドのうち、最も基本的なものです。引数にはNumeric型の値をとり、引数で指定したid
のオブジェクトを検索します。
まずはRDB上に存在するオブジェクトを検索した場合の例です。
>> User.find(1)
User Load (1.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
SQLのSELECT
文がRDBに渡され、その結果に対応するユーザーオブジェクトが返されているのがわかりますね。
今度はRDB上に存在しないオブジェクトを検索した場合の例です(先ほどの学習で、id=3
というユーザーは削除されました)。
>> User.find(3)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=3)
SQLのSELECT
文がRDBに渡されましたが、その結果となるレコードが返ってこなかったというエラーが返ってきました。
余談 - find
メソッドに複数の引数を与えてみる
余談ですが、find
メソッドは複数の引数を取ることができます。複数の引数を取った場合、引数として渡された整数に対応するid
を持つ全てのレコードが、引数の順序に対応する順番で格納された配列を返します。
>> User.find(1,2)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2)
=> [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">]
RDB上に存在しないid
を含むfind
メソッドを実行した場合、引数が複数であっても、引数1つの場合と同様、エラーが返ってきます。ただ、複数引数のfind
においては、エラーメッセージの内容が1引数の場合と異なります。
>> User.find(1,3)
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 3)
Traceback (most recent call last):
1: from (irb):30
ActiveRecord::RecordNotFound (Couldn't find all Users with 'id': (1, 3) (found 1 results, but was looking for 2))
find_by
メソッド
特定の属性を検索条件として、RDB上のレコードを検索するメソッドです。検索条件として与える属性と属性値はハッシュとして与え、当該ハッシュがfind_by
メソッドの引数となります。
>> User.find_by(email: "mhartl@example.com")
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
find_by
メソッドの引数として与える属性・属性値は、SQL文のWHERE
条件式に対応します。
RDB上にカラムとして存在する属性をキーとして与え、かつ引数として与えた属性値を持つレコードが存在しない場合、find_by
メソッドはnil
を返します。
>> User.find_by(email: "foobar@example.com")
User Load (4.1ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "foobar@example.com"], ["LIMIT", 1]]
=> nil
RDB上にカラムとして存在しない属性をキーとして与えた場合、find_by
メソッドはエラーとなります。
>> User.find_by(group: "")
User Load (49.5ms) SELECT "users".* FROM "users" WHERE "users"."group" = ? LIMIT ? [["group", ""], ["LIMIT", 1]]
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: users.group: SELECT "users".* FROM "users" WHERE "users"."group" = ? LIMIT ?)
first
メソッド・last
メソッド
first
メソッドは、データベースの最初のレコードを返します。
>> User.first
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
引数として整数値を与えた場合、データベースの最初から、引数として与えた個数のレコードを返します。
>> User.first(2)
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 2]]
=> [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">]
first
とは逆に、データベースの最後のレコードを返すlast
というメソッドもあります。
>> User.last
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">
all
メソッド
RDB上に存在する、当該モデルクラスに対応する全てのレコードの組を返します。
>> User.all
User Load (1.3ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">]>
戻り値がActiveRecord::Relation
という型である、というのは重要な事実です。
演習 - ユーザーオブジェクトを検索する
1.1. name
を使ってユーザーオブジェクトを検索してみてください。
>> User.find_by(name: "Michael Hartl")
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
name
の属性値としてRDB上に存在しない値を与えた場合、find_by
メソッドの戻り値はnil
となります6。
>> User.find_by(name: "Foo Bar")
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Foo Bar"], ["LIMIT", 1]]
=> nil
1.2. find_by_name
メソッドが使えることも確認してみてください。
>> User.find_by_name("Michael Hartl")
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
なお、Railsのモデルオブジェクトにおいて、find_by
と名のつくオブジェクトは、find_by
およびfind_by_name
以外にもいくつか存在します。
>> User.methods.grep(/find_by/)
=> [:find_by_name, :find_by, :find_by!, :initialize_find_by_cache, :find_by_sql]
2.実用的な目的のため、User.all
はまるで配列のように扱うことができますが、実際には配列ではありません。User.all
で生成されるオブジェクトを調べ、Array
クラスではなくUser::ActiveRecord_Relation
クラスであることを確認してみてください。
>> User.all.class
=> User::ActiveRecord_Relation
User::ActiveRecord_Relation
クラスの継承関係
User::ActiveRecord_Relation
はActiveRecord::Relation
を継承しており、ActiveRecord::Relation
はObject
を直接継承しています。
>> User.all.class.superclass
=> ActiveRecord::Relation
>> User.all.class.superclass.superclass
=> Object
User::ActiveRecord_Relation
クラスのオブジェクトからArray
クラスのオブジェクトを得るには
なお、User::ActiveRecord_Relation
クラスのオブジェクトではto_a
メソッドが利用可能であり、to_a
メソッドを実行することによって、本当にArray
クラスのオブジェクトを得ることができます。
>> User.all.to_a
User Load (0.8ms) SELECT "users".* FROM "users"
=> [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2019-10-01 22:40:17", updated_at: "2019-10-01 22:40:17">]
>> User.all.to_a.class
User Load (0.2ms) SELECT "users".* FROM "users"
=> Array
3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。
>> User.all.length
User Load (0.2ms) SELECT "users".* FROM "users"
=> 2
ダックタイピングとは
Railsチュートリアルの本文では、以下のように言及されています。
Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。
ユーザーオブジェクトを更新する
属性を個別に代入することによって更新する方法
>> user # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-09-29 22:50:02", updated_at: "2019-09-29 22:50:02">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
(0.7ms) SAVEPOINT active_record_1
SQL (0.5ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "mhartl@example.net"], ["updated_at", "2019-10-02 23:05:12.413976"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
このとき、更新日時を表すマジックカラムであるupdated_at
も更新されていることは重要です。
>> user.created_at
=> Sun, 29 Sep 2019 22:50:02 UTC +00:00
>> user.updated_at
=> Wed, 02 Oct 2019 23:05:12 UTC +00:00
save
しなかった場合
モデルオブジェクトの変更内容は、save
メソッドを実行して保存しなければRDBには反映されません。保存しないでreload
メソッドを実行した場合、RDBの内容を元にオブジェクトを再読み込みするので、次のように変更が取り消されます。
>> user.email
=> "mhartl@example.net"
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "mhartl@example.net"
update_attributes
メソッドを使って更新する方法
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
(0.2ms) SAVEPOINT active_record_1
SQL (0.3ms) UPDATE "users" SET "email" = ?, "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["email", "dude@abides.org"], ["updated_at", "2019-10-02 23:12:19.673874"], ["name", "The Dude"], ["id", 1]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
update_attributes
メソッドには、以下のような特徴があります。
- 引数は属性のハッシュである
- 必要な属性がすべて定義されていないと成功しない
- 成功時には更新と保存を続けて同時に行う
-
save
メソッドを別に実行する必要はない
-
- 保存まで成功した場合、戻り値は
true
となる
update_attributes
メソッドによってRDBに新しいレコードを生成することも可能です。
>> User.new.update_attributes(email:"foo@bar.com")
(0.2ms) SAVEPOINT active_record_1
SQL (0.3ms) INSERT INTO "users" ("email", "created_at", "updated_at") VALUES (?, ?, ?) [["email", "foo@bar.com"], ["created_at", "2019-10-03 11:06:14.875963"], ["updated_at", "2019-10-03 11:06:14.875963"]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> true
update_attribute
メソッドを使う方法
特定の属性のみを更新したい場合は、update_attribute
というメソッドを使います。
>> user.update_attribute(:name, "El Duderino")
(4.4ms) SAVEPOINT active_record_1
SQL (4.0ms) UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["updated_at", "2019-10-03 10:56:18.366072"], ["name", "El Duderino"], ["id", 1]]
(2.1ms) RELEASE SAVEPOINT active_record_1
=> true
かくして、Userモデルの1番目のレコードの中身は以下のようになりました。
>> User.find(1)
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "El Duderino", email: "dude@abides.org", created_at: "2019-09-29 22:50:02", updated_at: "2019-10-03 10:56:18">
演習 - ユーザーオブジェクトの更新
1. userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
>> user.name
=> "El Duderino"
>> user.name = "Reimu Hakurei"
=> "Reimu Hakurei"
>> user.save
(0.2ms) SAVEPOINT active_record_1
SQL (0.3ms) UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["updated_at", "2019-10-03 10:58:44.709664"], ["name", "Reimu Hakurei"], ["id", 1]]
(0.3ms) RELEASE SAVEPOINT active_record_1
=> true
>> User.find(1)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Reimu Hakurei", email: "dude@abides.org", created_at: "2019-09-29 22:50:02", updated_at: "2019-10-03 10:58:44">
2. 今度はupdate_attributesを使って、email属性を更新および保存してみてください。
>> user.update_attributes(email: "foo@foobar.net")
(0.1ms) SAVEPOINT active_record_1
SQL (1.4ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "foo@foobar.net"], ["updated_at", "2019-10-03 11:02:33.452008"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
update_attributes
メソッドを使った場合、検証が成功した時点で直ちにRDBに変更が保存されます。
3. 同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。
ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
>> user.update_attributes(created_at: 1.year.ago)
(0.3ms) SAVEPOINT active_record_1
SQL (0.2ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", "2018-10-03 11:12:02.215696"], ["updated_at", "2019-10-03 11:12:02.228937"], ["id", 1]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> true
created_at
が確かにUTC2018年10月3日になっていますね(この演習を行ったのはUTC2019年10月3日)。
-
Markdownで表を記述→VSCodeのMarkdown PDF拡張でPNGにエクスポート→適当な大きさにトリミング、という方法をとりました。 ↩
-
PlantUMLでデータモデルのスケッチを記述→VSCodeのPlantUML拡張でPNGにエクスポート、という方法をとりました。 ↩
-
手でマイグレーションを定義する方法もあります。Railsチュートリアルを進めていく中で、そちらも学習していくことになっています。 ↩
-
この点については疑問があります。確かにインスタンススコープの変数なのでしょうが、「変数名なくハッシュだけを返している」というのは、果たして本当にインスタンス変数なのでしょうか。 ↩
-
save
メソッドの戻り値は、true
またはfalse
でしたね。 ↩ -
「RDB上に存在するカラムにおいて、RDB上に存在しない値」を検索したときにエラーを返したい場合は、感嘆符つきの
find_by!
メソッドを使います。 ↩