LoginSignup
0
1

More than 3 years have passed since last update.

Railsチュートリアル 第6章 ユーザーのモデルを作成する - Userモデル

Posted at

モデルに関する基礎知識

  • モデル…データモデルとして扱うデフォルトのデータ構造
    • 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というメソッドが使用できるクラスでしたね。

4章で使用したUserクラス
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_mockup.png

以下はUserのデータモデルのスケッチ2です。

User_sketch.png

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が済んでいます。

マイグレーションのコード解説

以下は自動生成されたマイグレーションです。

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

このコードで重要な事柄は以下です。

  • マイグレーションファイル名の先頭には、マイグレーション生成時点のタイムスタンプが追加されている
    • マイグレーションのコンフリクトはまず発生しない
  • データベースに与える変更は、changeメソッドによって定義される
  • Railsではcreate_tableというイテレータメソッドが定義されている
    • RDB上にテーブルを作成するためのメソッドである
    • tというオブジェクトをブロック変数として用いる
    • 繰り返すのは「生成するカラムを定義する」という動作である
      • stringというメソッドであれば、生成対象としてstring型のカラムを定義する
    • t.timestampsという特別なメソッドが存在する
      • created_atupdated_atという2つのマジックカラムを生成するメソッドである
        • created_atは、レコードの作成時刻を自動で記録するカラムである
        • updated_atは、レコードの最終更新時刻を自動で記録するカラムである
  • テーブルに対するシンボル名が:usersという複数形であることに注意
    • 「テーブルは複数のレコードから成る」という構造を反映した命名規則
    • 一方で、モデル名はUserという単数形である
      • 単一レコードを対象として処理を行うオブジェクトであるため

コードの解説を踏まえた、Userの完全なデータモデル

マジックカラムを含めた、Userの完全なデータモデルは以下のようになります。

User_full.png

マイグレーションの適用

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データベースの実体です。

スクリーンショット 2019-09-29 11.03.35.png

上述スクリーンショットは、Visual Studio CodeのSQLite拡張(alexcvzz.vscode-sqlite
)の機能によってusersテーブルの構成を確認したものです。

idというカラムは、Railsにより自動で生成されたカラムで、レコードを一意に識別するために使うものです。主キーであることを示す鍵アイコンが頭についていますね。

idが主キーであることを踏まえた、より厳密なUserの完全なデータモデルは以下のようになります。

User_full.png

演習 - データベースの移行

1. あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。

db/schema.rbは、データベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われるRubyコードです。

まずは、db/migrate/[timestamp]_create_users.rbのコードを見てみましょう。

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のコードを見てみます。

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.timestampt.datetimeの違い
  • db/schema.rbcreate_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の内容は以下のようになりました。

`db/schema.rb`
ActiveRecord::Schema.define(version: 0) do

end

ポイントは以下でしょうか。

  • ActiveRecord::Schema.defineの第1引数のキー:versionに対する値が0になっている
    • ロールバック前は、マイグレーションのタイムスタンプに対応する値であった
  • create_tableメソッド以下がきれいさっぱり削除された

rails db:migraterails db:rollbackの関係について追加の解説

  • Railsのchangeメソッドには、create_tabledrop_tableがそれぞれ対応関係であるという情報が格納されている
    • create_tabledrop_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の内容は以下のようになりました。

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ファイル

マイグレーションふりかえり

  1. usersテーブルを作成し、development.sqlite3の内容を更新する
  2. usersテーブルに、以下5つのカラムを作成する
    • id: 主キー、integer型
    • name: varchar型
    • email: varchar型
    • created_at: マジックカラム、datetime型
    • updated_at: マジックカラム、datetime型

modelファイルの内容

rails generate model Userコマンドで生成された直後のapp/models/user.rbの内容は、以下のようになっています。

app/models/user.rb
class User < ApplicationRecord
end
  • Userというクラスが定義されている
    • UserクラスはApplicationRecordクラスを継承している
  • 他には特に何も定義されていない

ApplicationRecordクラスのコードは、app/models/application_record.rbにて定義されています。

app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ここでは、「ApplicationRecordクラスは、ActiveRecord::Baseクラスを継承している」という事実が重要です。「Railsにおけるモデルという概念を知るにあたり、まず学習する必要があるのは、ActiveRecord::Baseクラスについての知識である」ということですね。

演習 - modelファイル

1. Railsコンソールを開き、User.newUserクラスのオブジェクトが生成されること、そしてそのオブジェクトが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. 同様にして、ApplicationRecordActiveRecord::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というインスタンスが存在する状態で、usernameemailupdated_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メソッド

destroycreateの逆です。

>> 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_RelationActiveRecord::Relationを継承しており、ActiveRecord::RelationObjectを直接継承しています。

>> User.all.class.superclass
=> ActiveRecord::Relation
>> User.all.class.superclass.superclass
=> Object

User  ActiveRecord_Relation.png

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日)。


  1. Markdownで表を記述→VSCodeのMarkdown PDF拡張でPNGにエクスポート→適当な大きさにトリミング、という方法をとりました。 

  2. PlantUMLでデータモデルのスケッチを記述→VSCodeのPlantUML拡張でPNGにエクスポート、という方法をとりました。 

  3. 手でマイグレーションを定義する方法もあります。Railsチュートリアルを進めていく中で、そちらも学習していくことになっています。 

  4. この点については疑問があります。確かにインスタンススコープの変数なのでしょうが、「変数名なくハッシュだけを返している」というのは、果たして本当にインスタンス変数なのでしょうか。 

  5. saveメソッドの戻り値は、trueまたはfalseでしたね。 

  6. 「RDB上に存在するカラムにおいて、RDB上に存在しない値」を検索したときにエラーを返したい場合は、感嘆符つきのfind_by!メソッドを使います。 

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1