はじめに
Qiita初投稿です。
2021年3月から実際の開発案件に参加させて頂き、案件を通じて学んだ内容を投稿したいと思います。
modelやmigrateを作成する情報を多く目にしますが、実際にコードを書いて実装した際、作成手順で迷ったり、migrateの作成し直しを何度も行ったりした点を備忘録的にまとめました。
RubyonRails初学者の為、間違っている情報を記載している場合はご指摘いただけますと幸いです。
開発環境
- macOS Catalina バージョン 10.15.7
- Ruby 2.7.2
- Rails 6.1.1
- mysql Ver 8.0.23
modelとmigrateを作成するまでの流れ
開発において、要件定義やER図・テーブル設計は、より高度な知識や理解を必要とする作業だと思いますが、私の場合は基本設計が既にできあがった状態から参加させて頂きました。
そのため、ここではER図とテーブル設計が完成した状態から、素早く(約半日くらいで)modelとmigrateを作成し、seedsで動作確認を行うまでの流れをまとめたいと思います。
下記のER図とテーブル設計は簡略化したものですが、基本的な項目は網羅するよう記載したつもりです。
作業の流れ
- ER図
- Userテーブル
テーブル情報整理
migrateとmodelを作成 - Productテーブル
テーブル情報整理
migrateとmodelを作成 - Contractテーブル
テーブル情報整理
migrateとmodelを作成 - seedsでDBテストデータ作成
- バリデーションが正しく機能するかテストする
- 正しく関連付け(アソシエーション)ができているかをテストする
- 補足
1. ER図
ER図から、モデルの関係性とKeyの指定を把握
- UserモデルとContractモデルは、1対0以上(1対多)の関係
- ProductモデルとContractモデルは、1対0以上(1対多)の関係
- PKやFKについては、テーブル設計と比較して一致確認
- PK(PRIMARY KEY/主キー)
- UK(UNIQUE KEY/ユニーク制約キー)
- FK(FOREIGN KEY/外部キー)
2. Userテーブル
カラム | カラムの説明 | キー | index | not null | カラムの型 | default | 備考 |
---|---|---|---|---|---|---|---|
id | id | PK | ● | ● | bigint | ||
name | ユーザー名 | ⚪ | ⚪ | string | |||
Eメール | UK | ● | ⚪ | string | 文字数を255文字までに制限する Emailアドレスのフォーマットを検証 |
||
password | パスワード | ⚪ | string | ||||
※ PK = Primary Key UK = Unique Key | |||||||
※ ● 暗黙的に設定 ⚪ 明示的に設定する |
上記Userテーブルの情報整理(テーブル情報をコードに置き換えて整理)
| |migrate|model|
|:- |:- |:- |:-:|:-:|:-: |:-:|:-:|
|キー【PK】|暗黙的にindex,not null設定が入る|||
|キー【UK】 |index: { unique: true }
または
add_index :users, :email, unique: true を書く |uniqueness: true |
|index | index: true を設定 または add_index を書く (PKは自動設定)||
|not null | null: false (PKとUKは自動設定) | presence: true ||
|カラムの型 | name:string email:string password:string
(PKはbigint型が自動で入る) | ||
|備考 | | length: { maximum: 255 }
format: { with: VALID_EMAIL_REGEX }
VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d-]+(.[a-z\d-]+)*.[a-z]+\z/i.freeze |
一意制約は、アプリ側(model)だけでよいのか、DB側(migrate)もかけるのか迷いましたが、両方にかけるのが正解
参考:Railsチュートリアル6章:「Active Recordはデータベースのレベルでは一意性を保証していないという問題」を参照
参考:Rails 一意性制約のかけ方
Userテーブルのmodelとmigrate作成
上記で整理した内容を実際にターミナルに入力
$ rails g model モデル名 カラム名:型
各カラムが作成されるところまではターミナルに入力し、追加設定はmodel、migrateに直接入力、と分けて作業するのがわかりやすいと思います
$ rails g model User name:string email:string password:string
Running via Spring preloader in process 78395
invoke active_record
create db/migrate/2021xxxxxxxxxx_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
modelとmigrateが作成される(テストはRSpecで書くのであれば、削除)
class User < ApplicationRecord
end
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password
t.timestamps
end
end
end
テーブル条件をDB側(migrate)に追加する
null: false
add_index :users, :email, unique: true
※一意制約はindex: { unique: true }
でもOK
あとでschema.rb
と見比べたりするのであれば、add_index:
にunique: true
としたほうが見やすい
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.string :password, null: false
t.timestamps
end
add_index :users, :name
add_index :users, :email, unique: true
end
end
設定し忘れがないかschema.rb
を確認
$ rails db:migrate
ActiveRecord::Schema.define(version: 2021_05_xx_xxxxxx) do
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.string "password", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["name"], name: "index_users_on_name"
end
end
テーブル条件をmodelに追加する
presence: true
uniqueness: true
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true
validates :password, presence: true
end
- Emailの255文字制限を追加
length: { maximum: 255 }
- 正規表現を定数に代入
VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d-]+(.[a-z\d-]+)*.[a-z]+\z/i.freeze
- フォーマット検証を追加
format: { with: VALID_EMAIL_REGEX }
class User < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.freeze
validates :name, presence: true
validates :email, presence: true, uniqueness: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
validates :password, presence: true
end
関連付け(アソシエーション)を追加
UserとContractは1:0以上(1:多)なので、has_many :contract, dependent: :destroy
を追加しておく
※dependent: :destroy
はUserを削除した時、関連付けしている契約(Contract)も同時に削除する
class User < ApplicationRecord
has_many :contracts, dependent: :destroy
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.freeze
validates :name, presence: true
validates :email, presence: true, uniqueness: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
validates :password, presence: true
end
ここで、Userのmigrate、modelをすぐにテストする場合は、5. seedsでDBテストデータ作成を参考にテストデータを作成してください
(この状態ではアソシエイトは動きません)
3. Productテーブル
カラム | カラムの説明 | キー | index | not null | カラムの型 | default | 備考 |
---|---|---|---|---|---|---|---|
id | id | PK | ● | ● | bigint | ||
product_name | 商品名 | ⚪ | ⚪ | string | |||
product_price | 商品の価格 | ⚪ | integer | 0 |
※ PK = Primary Key
※ ● 暗黙的に設定 ⚪ 明示的に設定する
###上記Productテーブルの情報整理(テーブル情報をコードに置き換えて整理)
| |migrate|model|
|:- |:- |:- |:-:|:-:|:-: |:-:|:-:|
|キー【PK】|暗黙的にindex,not null設定が入る|||
|index | add_index を設定 または index: true を書く (PKは自動設定)||
|not null | null: false (PKは自動設定) | presence: true ||
|カラムの型 | product_name:string product_price:integer
(PKはbigint型が自動で入る) | ||
|default| default: 0 ||
###Productテーブルのmodelとmigrate作成
上記で整理した内容を実際にターミナルに入力
Productテーブル情報を$ rails g model モデル名 カラム名:型
にあてはめる
$ rails g model Product product_name:string product_price:integer
Running via Spring preloader in process 85812
invoke active_record
create db/migrate/2021xxxxxxxxxx_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
class Product < ApplicationRecord
end
class CreateProducts < ActiveRecord::Migration[6.1]
def change
create_table :products do |t|
t.string :product_name
t.integer :product_price
t.timestamps
end
end
end
テーブル条件をDB側(migrate)に追加する
null: false
default: 0
add_index :products, :product_name
class CreateProducts < ActiveRecord::Migration[6.1]
def change
create_table :products do |t|
t.string :product_name, null: false
t.integer :product_price, null: false, default: 0
t.timestamps
end
add_index :products, :product_name
end
end
設定し忘れがないかschema.rb
を確認
$ rails db:migrate
ActiveRecord::Schema.define(version: 2021_05_xx_xxxxxx) do
create_table "products", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "product_name", null: false
t.integer "product_price", default: 0, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["product_name"], name: "index_products_on_product_name"
end
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.string "password", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["name"], name: "index_users_on_name"
end
end
テーブル条件をmodelに追加する
presence: true
class Product < ApplicationRecord
validates :product_name, presence: true
validates :product_price, presence: true
end
関連付け(アソシエーション)を追加
ProductとContractは1:0以上(1:多)なので、has_many :contract
を追加しておく
商品(Product)を削除した時に契約(Contract)を同時に削除する必要はないので、dependent: :destroy
は不要
class Product < ApplicationRecord
has_many :contracts
validates :product_name, presence: true
validates :product_price, presence: true
end
ここで、Productのmigrate、modelをすぐにテストする場合は、5. seedsでDBテストデータ作成を参考にテストデータを作成してください
(この状態ではアソシエイトは動きません)
4. Contractテーブル
カラム | カラムの説明 | キー | index | not null | カラムの型 | default | 備考 |
---|---|---|---|---|---|---|---|
id | id | PK | ● | ● | bigint | ||
user_id | ユーザーID | FK | ● | ● | bigint | 外部キー(Userテーブルのidと紐付ける) | |
product_id | 商品ID | FK | ● | ● | bigint | 外部キー(Productテーブルのidと紐付ける) | |
product_name | 商品名 | ⚪ | ⚪ | string | *Contract作成時、product_nameから転記 | ||
contract_money | 契約金 | ⚪ | integer | 0 | |||
contract_status | 契約ステータス | integer | enum(reservation:0, confirm:1) | ||||
※ PK = Primary Key FK = Foreign Key | |||||||
※ ● 暗黙的に設定 ⚪ 明示的に設定する |
###上記Contractテーブルの情報整理(テーブル情報をコードに置き換えて整理)
| |migrate|model|
|:- |:- |:- |:-:|:-:|:-: |:-:|:-:|
|キー【PK】|暗黙的にindex,not null設定が入る|||
|キー【FK】|user:references product:references (暗黙的にindex,not null設定が入る)
詳細には、references とすると自動的に null: false と foreign_key: true が migrate に追記される
index: true は記載されないが、index設定が自動的に db/schema.rb に入る||
|index | add_index :contracts, :product_name を設定 または index: true を書く (PK,FKは自動設定)||
|not null | references で null: false 設定 | presence: true を設定 ||
|カラムの型 | product_name:string contract_money:integer contract_status:integer
(PKはbigint型が自動で入る) | ||
|default| default: 0 | |
|備考| |enum contract_status: { reservation: 0, confirm: 1, }|
###Contractテーブルのmodelとmigrate作成
上記で整理した内容を実際にターミナルに入力
Contractテーブル情報を$ rails g model モデル名 カラム名:型
にあてはめる
$ rails g model Contract user:references product:references product_name:string contract_money:integer contract_status:integer
Running via Spring preloader in process 79699
invoke active_record
create db/migrate/2021xxxxxxxxxx_create_contracts.rb
create app/models/contract.rb
invoke test_unit
create test/models/contract_test.rb
create test/fixtures/contracts.yml
modelとmigrateが作成される(テストはRSpecで書くなら、削除)
class Contract < ApplicationRecord
belongs_to :user
belongs_to :product
end
class CreateContracts < ActiveRecord::Migration[6.1]
def change
create_table :contracts do |t|
t.references :user, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.string :product_name
t.integer :contract_money
t.integer :contract_status
t.timestamps
end
end
end
テーブル条件をDB側(migrate)に追加
- index
add_index :contracts, :product_name
- not null設定
null: false
- default設定
default: 0
ついでに行も揃えると見やすい
class CreateContracts < ActiveRecord::Migration[6.1]
def change
create_table :contracts do |t|
t.references :user, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.string :product_name, null: false
t.integer :contract_money, null: false, default: 0
t.integer :contract_status
t.timestamps
end
add_index :contracts, :product_name
end
end
設定漏れがないかschema.rb
を確認
$ rails db:migrate
ActiveRecord::Schema.define(version: 2021_05_26_081531) do
create_table "contracts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "product_id", null: false
t.string "product_name", null: false
t.integer "contract_money", default: 0, null: false
t.integer "contract_status"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["product_id"], name: "index_contracts_on_product_id"
t.index ["product_name"], name: "index_contracts_on_product_name"
t.index ["user_id"], name: "index_contracts_on_user_id"
end
create_table "products", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "product_name", null: false
t.integer "product_price", default: 0, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["product_name"], name: "index_products_on_product_name"
end
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.string "password", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["name"], name: "index_users_on_name"
end
add_foreign_key "contracts", "products"
add_foreign_key "contracts", "users"
end
今回のテーブル設計にはありませんが、以下はよくある?設定
t.references :user
の参照先テーブル名がカラム名とは違う場合 foreign_key: { to_table: :customers }
と参照先を指定する
t.references :user, null: false, foreign_key: { to_table: :customers }
テーブル条件をmodelに追加
-
presence: true
追加 -
enum
追加
class Contract < ApplicationRecord
belongs_to :user
belongs_to :product
validates :user, presence: true
validates :product, presence: true
validates :product_name, presence: true
validates :contract_money, presence: true
validates :contract_status, presence: true
enum contract_status: {
reservation: 0,
confirm: 1,
}
end
関連付け(アソシエーション)を追加
UserモデルとContractモデルの「1対0以上(1対多)」の関係と、ProductモデルとContractモデルの、「1対0以上(1対多)」の関係をmodelに反映
class Contract < ApplicationRecord
belongs_to :user
belongs_to :product
validates :user, presence: true
validates :product, presence: true
validates :product_name, presence: true
validates :contract_money, presence: true
validates :contract_status, presence: true
enum contract_status: {
reservation: 0,
confirm: 1,
}
end
class User < ApplicationRecord
has_many :contracts, dependent: :destroy
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.freeze
validates :name, presence: true
validates :email, presence: true, uniqueness: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
validates :password, presence: true
end
class Product < ApplicationRecord
has_many :contracts
validates :product_name, presence: true
validates :product_price, presence: true
end
これで、関連付けされた別テーブルのデータが使用できるようになるので、次項以下で確認します
migrateとmodelの作成作業はひとまず完了です
5. seedsでDBテストデータ作成
db/seeds.rb
でテストデータを作成する目的は、以下二点です
①DBが正常に保存できるかを確認する
②テーブル設計どおりにモデルのバリデーション等が機能するか確認する
user1 = User.create!(name: "akagi", email: "akagi@example.com", password: "password")
user2 = User.create!(name: "uozumi", email: "uozumi@example.com", password: "password")
user3 = User.create!(name: "anzai", email: "anzai@example.com", password: "password")
puts "userデータの投入に成功しました!"
product1 = Product.create!(product_name: "サッカーボール", product_price: 10000 )
product2 = Product.create!(product_name: "バスケットボール", product_price: 15000)
puts "productデータの投入に成功しました!"
Contract.create!(user_id: user1.id, product_id: product1.id, product_name: product1.product_name, contract_money: 500000, contract_status: 0)
Contract.create!(user_id: user2.id, product_id: product2.id, product_name: product2.product_name, contract_money: 1000000, contract_status: 1)
Contract.create!(user_id: user1.id, product_id: product2.id, product_name: product2.product_name, contract_money: 500000, contract_status: 0)
puts "Contractデータの投入に成功しました!"
テストデータを作成するコマンドを入力
$ rails db:seed
※一度、seedデータを作成し終えている場合は、$ rails db:reset
を実行
うまく保存できない(エラーが出る)場合は、binding.pry
をseeds.rb
に入れてどこがうまく行かないのか確認しながら修正する
6. バリデーションが正しく機能するかテストする
全てのバリデーションテストはRSpecのモデルスペックで実装することを想定し、ここでは、主な項目だけをテストしていきます。
$ rails console
# user4を正常なデータとして作成することができる(seeds.rbが動作する時点で確認済ですが…)
> user4 = User.create(name: "mitui", email: "mitui@example.com", password: "password")
# Emailの正規表現が機能しているか確認
> user4.update(email: "mitui@..example.com")
=> false
# エラーメッセージも確認する
> user4.errors.full_messages
=> ["Email is invalid"]
# 文字制限が機能している確認
> user4.update(email:" #{"m" * 255}@example.com")
=> false
> user4.errors.full_messages
=> ["Email is too long (maximum is 255 characters)", "Email is invalid"]
# uniqueが機能しているか確認
> user4.update(email: "akagi@example.com")
=> false
> user4.errors.full_messages
=> ["Email has already been taken"]
7. 正しく関連付け(association)ができているかをテストする
$ rails console
# Userが正常に保存されているか確認
> user1, user2, user3 = User.all
> user1.name
=> "akagi"
# Userモデルの has_many :contracts, dependent: :destroy が関連付けできているか確認
> user1.contracts
# user1のcontractsを全て取得する
> user1.contracts.all
# user1のcontractsの1つ目のproduct_nameを取得する
> user1.contracts[0].product_name
# productモデルの has_many :contracts が関連付けできているか確認
> product2 = Product.second
# productオブジェクトから、contractと関連付けされたuserを取得する
> product2.contracts[1].user
# user3.contracts.create!でuser3とproduct2のContractのインスタンスを作成して保存
> user3.contracts.create!(product_id: product2.id, product_name: product2.product_name, contract_money: 750000, contract_status: 0)
# 作成保存できているか、Contractを確認
> Contract.last
これで、DB周辺のmodelとmigrateの確認も完了
あとは必要な機能要件に合わせて、viewsとcontrollersの作成に入る
8. 補足
途中でカラムを追加したくなった時の手順で迷ったので補足
例えば、db/migrate/2021xxxxxxxxxx_create_contracts.rbに contract_title
をうっかり作成し忘れていた事に途中で気づいたような時、AddColumnを多用するのはよくない(自論)と思いますので、githubのmasterにマージする前後で作業を分けました
githubにブランチをpushしてmasterにマージする前
# どこまで戻すか確認する
$ rails db:migrate:status
database: qiita_posting
Status Migration ID Migration Name
--------------------------------------------------
up 20210526055004 Create products
up 20210526065824 Create users
up 20210526081531 Create contracts
# 必要なところまでdbを戻す(以下は一つだけ戻す書き方)
$ rails db:rollback
# もし、2つ戻すなら、rails db:rollback STEP=2 と書く
db/migrateにカラム(contract_title)を追加する
class CreateContracts < ActiveRecord::Migration[6.1]
def change
create_table :contracts do |t|
t.references :user, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.string :product_name, null: false
t.string :contract_title, null: false
t.integer :contract_money, null: false, default: 0
t.integer :contract_status
t.timestamps
end
add_index :contracts, :product_name
end
end
rails db:migrate
を再度実行する
githubにブランチをpushしてmasterにマージした後のカラム追加
$ rails g migration AddColumnToContracts contract_title:string
# 変更内容
def change
add_column :contracts, :contract_title, :string, null: false
end
end
数ある追加・修正パターンの一部ではありますが、備忘録的にまとめてみました。
私にとっては貴重な初案件であり、参加の機会をくださった方々、また技術不足の私にspatial.chatやzoom、Slackでサポート頂いた開発メンバーの方々に、この場を借りて厚く御礼申し上げます。
また、記載している内容の間違いやよりよい手順等ありましたら、ご指摘頂けますと幸いです。