Just do IT

思うは招く

Ruby on Rails でモデルを作成する基本

前回の続き。

k-koh.hatenablog.com

今回はRailsで簡単なモデルを作ったり、モデルクラスから情報を取得、編集、削除したりする。

手順

データベースを作成する

ターミナル上で直接データベースを作成してもよいのだが、Railsではデータベースを作成するコマンドが用意されている。

rails db:create

結果

Database 'db/development.sqlite3' already exists
Created database 'db/test.sqlite3'

本来ならdb/development.sqlite3もcreatedとなるのだが、私の環境ではすでに作成されていたようす。

ORMでプログラムからDBへアクセスする

  • ORM(Object Relation Mapping)とは、オブジェクト指向言語からRDBにアクセスするための仕組み
  • ORMの概念に基づいたライブラリを「O/Rマッパー」と呼ぶ
  • RailsSinatraのO/RマッパーはActiveRecord
  • テーブルの情報をモデルクラスとして取得する

ActiveRecordのルール

  • モデルクラスは先頭が大文字の単数形、テーブル名は小文字で複数形
    • 例:モデルクラス「Memo」、テーブル「memos」
  • 2つ以上の単語を組み合わせた場合、テーブル名は小文字でアンダースコアでつなげて最後の単語が複数形
    • モデルクラス「UserItem」、テーブル「user_items」
  • レコードを識別するためのカラム名はデフォルトで id となる
  • ActiveRecordでテーブルを新規作成すると、idカラムは指定する必要がなく、自動で追加される
  • created_at, updated_atも自動で追加される

モデルクラスを作成

モデルクラスを作るには、次のコマンドを使う。

rails g model モデル名 カラム名1:データ型 カラム名2:データ型…

これを実行すると、モデルクラスとマイグレーションファイルが作られる。

マイグレーションファイルとは、テーブルの新規作成や構造変更を簡単に管理してくれるファイルのこと。マイグレーションを使うことで、DBMSによって異なるテーブル定義のSQLを、DBMSの種類に左右されず、同じ定義方法で記述できる。

作成例

rails g model Diary title:string body:text

意味

  • Diaryというモデルクラスを作成する
  • titleカラムを用意し、データ型はstringにする
  • bodyカラムを用意し、データ型はtextにする

結果

Running via Spring preloader in process 9912
      invoke  active_record
      create    db/migrate/20200116062630_create_diaries.rb
      create    app/models/diary.rb
      invoke    test_unit
      create      test/models/diary_test.rb
      create      test/fixtures/diaries.yml

最初の

db/migrate/20200116062630_create_diaries.rb

マイグレーションファイル。 db/migrate下に作成され、ファイル名は作成された日時が接頭辞(prefix)として付与される。Diaryモデルクラスに対応し、自動でdiariesというテーブル名になっている。

app/models/diary.rb

これはモデルクラスのファイル。app/models配下に作られる。

test/models/diary_test.rbはモデルクラスのテストコードの雛形。 test/fixtures/diaries.ymlはフィクスチャと呼ばれる、テストデータを作成するための雛形。

マイグレーションファイルの中身

マイグレーションファイルである、db/migrate/20200116062630_create_diaries.rbを見てみる。

class CreateDiaries < ActiveRecord::Migration[6.0]
  def change
    create_table :diaries do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

このマイグレーションファイルを実行すると、テーブル作成の処理が記述されているchangeメソッドが実行される。

  def change
    create_table :diaries do |t|
      t.string :title
      t.text :body

      t.timestamps
    end

t.timestampsという記述は、作成するテーブルにcreated_atとupdated_atというカラムをつくる。

モデルクラス

app/models/diary.rbを見てみる。

class Diary < ApplicationRecord
end
  • ApplicationRecordクラスを継承している
  • このモデルクラスがあるだけでRailsアプリ上からdiariesテーブルにアクセスできるようになる

同一ディレクトリにあるapp/models/application_record.rbも見てみる。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
  • ApplicationRecordクラスは、モデルクラス間でアプリ共通の設定を記述するためのクラス
  • ActiveRecord::Baseクラスという、ActiveRecordの基本機能を提供するクラスを継承している
  • ApplicationRecordクラスを継承することで、ActiveRecordの基本機能がすべて使える
  • self.abstract_class = trueは、あるモデルクラスを継承して別のモデルクラスを定義した場合に、元のモデルクラスが存在しない場合でもエラーが発生しないようにするための設定

マイグレーションを実行してファイルを作成

rails g model Diary title:string body:text

これをした時点では、モデルクラスとマイグレーションファイルが作成されるだけで、データベース上に必要なテーブルはまだ作成されていない。マイグレーションコマンドを実行する必要がある。

rails db:migrate

これでデータベースにテーブルが新規作成される。

Railsのデータベース用コンソールを起動して確かめる。

rails dbconsole

こんな画面が出てsqliteにアクセスできる。

SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.

ここでは開発環境用のデータベースであるdevelopment.sqlite3に接続される。

テーブル確認。

.tables

diariesテーブルが作られていることがわかる。

ar_internal_metadata  diaries               schema_migrations   
  • ar_internal_metadata: データベースを誤って削除してしまわないようにするために参照されるテーブル
  • schema_migrations: マイグレーションのバージョン管理のためのテーブル

これらはRailsが自動生成している。

テーブル構造を確認するにはこちら。

.schema diaries

CREATE TABLE IF NOT EXISTS "diaries" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar, "body" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

idが自動連番で生成されてる点に注目。

テストデータを準備

  • Railsアプリを開発中に、動作確認をするために必要最低限なデータを準備する
  • そんなデータを定義する仕組みとしてフィクスチャがある

test/fixtures/diaries.yml

# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  id: 1
  title: '今日は筋トレをした'
  body: 'たくさんベンチプレスをした'

two:
  id: 2
  title: '今日食べたもの'
  body: '今日は鶏肉を食べた'

デフォルトファイルにはMystringなどと書かれているが、オリジナルに変更した。idカラムは自動連番で勝手にデータが入力されるが、その場合、振られる番号がわからないため、自分で付与しておいたほうが動作確認時には便利。

テストデータを取り込む

上記で作ったテストデータを取り込む。

rails db:fixtures:load

テーブルを見てみる。

select * from diaries;

1|今日は終日休暇|今日は仕事を休んで、公園で読書を楽しんだ。|2020-01-16 06:37:16.275065|2020-01-16 06:37:16.275065
2|今日の天気|今日は鶏肉を食べた|2020-01-16 06:37:16.275065|2020-01-16 06:37:16.275065

ちなみにこれでテーブルの出力結果が変わる。

.headers on
.mode column
select * from diaries;

id          title       body                   created_at                  updated_at                
----------  ----------  ---------------------  --------------------------  --------------------------
1           今日は終日休暇     今日は仕事を休んで、公園で読書を楽しんだ。  2020-01-16 06:37:16.275065  2020-01-16 06:37:16.275065
2           今日の天気       今日は鶏肉を食べた              2020-01-16 06:37:16.275065  2020-01-16 06:37:16.275065

.headers onでヘッダー情報が表示。 .mode columnでレコードの長さに対応した横幅になる。

モデルクラスにアクセス

Railsで使えるirbを立ち上げる。

rails c

Running via Spring preloader in process 10814
Loading development environment (Rails 6.0.2.1)
irb(main):001:0> 

レコードを参照する。

Diary.all
   (1.0ms)  SELECT sqlite_version(*)
  Diary Load (0.6ms)  SELECT "diaries".* FROM "diaries" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Diary id: 1, title: "今日は筋トレをした", body: "たくさんベンチプレスをした", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">, #<Diary id: 2, title: "今日食べたもの", body: "今日は鶏肉を食べた", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">]>

id指定する。

Diary.find(2)
  Diary Load (0.7ms)  SELECT "diaries".* FROM "diaries" WHERE "diaries"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
=> #<Diary id: 2, title: "今日食べたもの", body: "今日は鶏肉を食べた", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">

内部では自動でSQLが実行されているのがわかる。すげぇ。

変数に入れる。

d = Diary.find(2)
d
=> #<Diary id: 2, title: "今日食べたもの", body: "今日は鶏肉を食べた", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">

titleを取得。

d.title
=> "今日食べたもの"

bodyを取得。

d.body
=> "今日は鶏肉を食べた"

モデルクラスからレコードを作成する

irb(main):007:0> d1 = Diary.new
=> #<Diary id: nil, title: nil, body: nil, created_at: nil, updated_at: nil>

代入する。

d1.id = 3
=> 3

d1.title = 'title3'
=> "title3"

d1.body = 'body3'
=> "body3"

save

d1.save

   (0.1ms)  begin transaction
  Diary Create (5.0ms)  INSERT INTO "diaries" ("id", "title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["id", 3], ["title", "title3"], ["body", "body3"], ["created_at", "2020-01-16 11:47:18.696422"], ["updated_at", "2020-01-16 11:47:18.696422"]]
   (2.9ms)  commit transaction
=> true

INSERT INTOが実行されている。

確認すると、

Diary.all

  Diary Load (1.6ms)  SELECT "diaries".* FROM "diaries" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Diary id: 1, title: "今日は筋トレをした", body: "たくさんベンチプレスをした", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">, #<Diary id: 2, title: "今日食べたもの", body: "今日は鶏肉を食べた", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">, #<Diary id: 3, title: "title3", body: "body3", created_at: "2020-01-16 11:47:18", updated_at: "2020-01-16 11:47:18">]>

ちゃんと入ってる。

削除するには。

d1.destroy

確認。

 Diary.all
  Diary Load (0.9ms)  SELECT "diaries".* FROM "diaries" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Diary id: 1, title: "今日は筋トレをした", body: "たくさんベンチプレスをした", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">, #<Diary id: 2, title: "今日食べたもの", body: "今日は鶏肉を食べた", created_at: "2020-01-16 11:41:40", updated_at: "2020-01-16 11:41:40">]>

ないことがわかる。

レコード作成の別の書き方

createメソッドだとレコード挿入とsaveが同時にできる。

Diary.create(id: 3, title: 'title3', body: 'body3')
Diary.find(3)
  Diary Load (1.4ms)  SELECT "diaries".* FROM "diaries" WHERE "diaries"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
=> #<Diary id: 3, title: "title3", body: "body3", created_at: "2020-01-16 11:51:18", updated_at: "2020-01-16 11:51:18">

newメソッドの結果をブロックに渡して値をセット。

irb(main):021:0> d4 = Diary.new do |d|
irb(main):022:1* d.id = 4
irb(main):023:1> d.title = 'title4'
irb(main):024:1> d.body = 'body4'
irb(main):025:1> end
Diary.find(4)

  Diary Load (1.2ms)  SELECT "diaries".* FROM "diaries" WHERE "diaries"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
=> #<Diary id: 4, title: "title4", body: "body4", created_at: "2020-01-16 11:53:14", updated_at: "2020-01-16 11:53:14">