ここまでの記事はこちら
Ruby on Railsの習得のためタスクリスト的なの作ってみる①〜構成、設計編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる②〜Rails7.0+React18.0+ MySQL8.0 Docker開発環境構築編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる③〜API開発・呼び出し編〜
今回は、引き続きバックエンド開発です。DB周りをやっていきます。
RailsでDDDってどうなの?そもそもRailsの設計思想って?という疑問に関しては別記事で語ることにしたのでよかったら読んでください。
やはりRailsで開発するメリットを活かすなら Rail way に従おう、という結論に至ったのでRailsらしいごく普通の実装をしていきます。ちっちゃいアプリだしこの方が勢いよく開発できそう。
第一回、第二回で選定及び環境構築したとおり、DBにはMySQLを使っていきます。
MySQLを使用するための設定変更
RailsのデフォルトのDBはSQLiteになっています。
今回はMySQLを採用してDockerコンテナを立て、デフォルトの3306番ポートを開けてあるので、そこにアクセスできるようRails側の設定を変えたいです。
どうやらアプリケーション作成した時点でMySQLに指定できたらしいのですが、後からでも大丈夫とのこと。
以下のコマンドを実行すればOK。
$ rails db:system:change --to=mysql
/config/database.yml
を上書きしていいか聞かれるので、Y
を返します。
これだけで
- Gemfileの書き換え
- configをMySQL用のデフォルト状態に上書き
してくれます。
Gemを書き換えたので
$ bundle install
もやっておきます。
このままでは個別のDBには適合していないので、ドキュメントに従って/config/database.yml
の開発用の設定箇所を書き換えます。
development:
<<: *default
database: testdb
username: user
password: pass
Railsのマイグレーションを使う
コマンドでファイル生成可能です。
(注意)次のパートでモデル生成のコマンドでマイグレーションファイルも一緒に生成できることが判明しました。そっちのコマンドの方が二度手間にならなくていいかも。
$ rails generate migration Task
class CreateTasks < ActiveRecord::Migration[7.0]
def change
create_table :tasks do |t|
t.timestamps
end
end
end
こういう感じで生成されて、create_table
ブロック内に追加するカラムを書いていきます。
書き方は上記ドキュメント参照。
class CreateTasks < ActiveRecord::Migration[7.0]
def change
create_table :tasks do |t|
t.references :user, foreign_key: true
t.string :name, :null => false
t.datetime :limit, :null => false
t.string :priority, :null => false
end
end
end
主キーについては何も指定しなければid
カラムが生成され、主キーとなります。
※外部キー設定しているusersテーブルは先にマイグレーションファイル作成済みです。
マイグレーションの実行もコマンドで行います。
$ rails db:migrate
dry runはなさそうなので、一旦やってみてrails db:rollback
すればいいのだと思います、多分。
無事テーブルが作成され、/db/schema.rb
というファイルが生成されました。
複数の環境間でスキーマの整合性を保つために使われるらしい。へーっ。(参考記事)
人間が見てもわかりやすくていいですね。
Active Recordのモデル実装
LaravelでいうEloquentみたいな感じ。いわゆるORMというやつで、SQLを直接書かずにデータベース操作するアレです。
コマンドでファイル生成。と、思ったら
$ rails generate model task
invoke active_record
conflict db/migrate/20230306204236_create_tasks.rb
Another migration is already named create_tasks: /achievelist/db/migrate/20230306154320_create_tasks.rb. Use --force to replace this migration or --skip to ignore conflicted file.
あー、マイグレーションのファイルも一緒に生成できたんですかそうですか。
$ rails generate model task --skip
これでコンフリクトしたマイグレーションファイルの生成はスキップしてくれます。助かった。
ModelとControllerの役割分担どんな感じ?
早速Modelを書いていきたいのですが、Active Record便利すぎてほぼ書くことなくないですか?
whereとかfindとかの条件もControllerに書いている人が多い気がします。ていうかそんなんControllerに書いておk?ユースケースとか挟まなくていいんですか?
その割にバリデーションはActive Recordの仕事みたいなこと書いてあるし。
うーん。
まず、今回の方針的にRailsに従う以上ControllerがActiveRecordに依存するのはもう仕方ないものとして諦めます。
とはいえ、全部が全部Controllerに書くのも本来MVCフレームワークとして存在するRailsの方針に合わないはずです。
なので、今回の開発では以下のような折衷案で進めることにしましょう。
- ユースケース的に使い回すものは、シンプルな操作であってもModelにメソッドを書く
- Modelにメソッドを置いたとしても単にActiveRecordを一度呼ぶだけになる場合はControllerに書く
利便性と保守性で妥協するとこんな感じかと思います。
Model実装
使い方その1 バリデーション
テーブルに保存する値のバリデーションをModelの機能として行ってくれます。
各バリデーションはドキュメント参照。
class Task < ApplicationRecord
validates :user_id, numericality: true
validates :name, length: { minimum: 1, maximum: 200 }
validates :priority, inclusion: {
in: %w[HIGH MIDDLE LOW)],
message: '優先度は (HIGH MIDDLE LOW) から選択して下さい。'
}
validates :limit, datetime: {
message: '正式な日時を入力してください。'
}
end
呼び出すときは、new
してからvalid?
するとバリデーションチェックしてくれます。
# (略)
task = Task.new({ user_id:, name:, limit:, priority: })
result =
if !task.valid? # バリデーションを実行し、判定結果を真偽値で返却
ResponseGenerator.validation_error(task.errors.full_messages)
# full_messagesでバリデーションエラーのメッセージ文字列の配列が取得できる
elsif !task.save # テーブル保存を実行
ResponseGenerator.db_error
else
ResponseGenerator.success
end
# (略)
ResponseGenerator
というのは私が勝手に作った返却値生成用のモジュールです。一応以下のアコーディオンの中に置いておきます。
ResponseGenerator(参考)
module ResponseGenerator
extend ActiveSupport::Concern
def success(body = nil)
return if body.nil?
{
body:,
status: 200
}
end
def validation_error(errors = [])
{
body: {
'title': 'params not validated.',
'invalid-params': errors
},
status: 400
}
end
def un_authorized_error
{
body: {
'title': 'unauthorized.'
},
status: 401
}
end
def not_found
{
body: {
'title': 'resource not found.'
},
status: 404
}
end
def db_error
{
body: {
'title': 'db operations failed.'
},
status: 500
}
end
module_function :success, :validation_error, :un_authorized_error, :not_found, :db_error
end
使い方その2 クラスメソッドの作成
Modelクラスに対して、クラスメソッドを定義することができます。self
をつけ忘れるとインスタンスメソッドになってしまうので注意。
目的は、一連の操作をモデルの役割としてメソッドにまとめることです。
以下は、クラスメソッド内部でトランザクションを使用してみた例です。トランザクション内で例外が発生するとロールバックしてくれる便利仕様、ありがとうございます。
class TaskClearEvent < ApplicationRecord
require 'date'
def self.task_clear(task_id, user_id) # selfつけないとインスタンスメソッドになる
# (略) cleared_taskとtaskはこの辺で定義してる
transaction do # トランザクション開始
cleared_task.save!
task.destroy!
event = new(
{
task_id: task.id,
cleared_task_id: cleared_task.id,
event_datetime: Time.current
}
)
event.save!
end # トランザクション終了
!event.nil? && event.present?
end
end
次回予告
今回はモデルを作成しました。次回はバックエンドで認証関係をやっていきたいと思います!