目的
- Rails案件(だけの話ではないだろうけど)には、「このメソッドを変えると、どこに影響が出るんだろう」とか「このクラスはどこで作られたり、更新されてるんだっけ」という疑問が頻繁に起こる。
- なので、なるべくその疑問がスピーディに解消できるような工夫をまとめておく。
工夫集
ActiveRecordのcreate
やupdate
は「決まった形」から生やす
- 特定のActiveRecordの
create
やupdate
と言った、DB処理があちこちに散らばっている場合には、検索しやすいように特定の形に限定しておくことで、楽に影響範囲を調査できる。 - 具体的には、呼び出し元を
AdminUser
とする場合には、以下の形に限定する。-
AdminUser
(Camelケース) -
admin_user
(Snakeケース)
-
- 加えて、DBメソッドも以下に限定する
new
save, save!
create, create!
update, update!
destroy, destroy!
find_or_create_by, find_or_create_by!
find_or_initialize_by
- こうすれば、パターンとしてはせいぜい(2 x 7 = 14通り)になるので(あれ?結構大変?)、思わぬ見過ごしが減って、影響範囲がすぐにわかるようになる。
- あと、newで呼び出す場合は必ず以下のどちらかの形になる
@admin_user = AdminUser.new(...)
admin_user = AdminUser.new(...)
アンチパターン
- 「コードを短くしたい」という名目で
au.create
とかにしてしまうと、発見しづらくなる - has_many関連のモデルを
User.dogs.create
とかで作成するのも、上のパターンから外れるのでNG
DB処理が走る箇所にはユニットテストを仕込む
- これをしておくだけでも、見落としをだいぶん防げる。
- 基本全てのメソッドを手動で試し尽くすことはできないので、崩れて欲しくないロジックなどにはテストを必ず挟むこと
DBスキーマの修正とFactory、DBシードの修正をセットにする
- 個人的な意見だが、「プロジェクトの開発環境の健康度」と「プロジェクトのDBシードの管理具合」の間には正の相関があると思っている。
- 一応「DBシード」とは、「開発環境のDBに入れるデフォルトデータ」のことである。
- そして「良いDBシード」とは、「アプリのビュー表示ロジックの、ありとある可能性を網羅できるデータ」とする。
- これがしっかり管理されていると、コードの修正によって問題が発生した時に、すぐさま何処のビューに「エラー」として表示されるため、バグの発見が早まるのだ。
- なので、「DBに新しいフィールドが追加・更新・削除される」と言った場合には、必ずセットでFactoryとSeedを修正することをセットにする。
コントローラーアクションへのアクセス権限ロジックはPunditに集約する
- ログインユーザーの状態(プランの違いだったり、登録情報の有無だったり)によって、「コントローラーの特定のアクション」をさせたくないとする。
- かといって、その条件文をコントローラー内に書くと、コントローラーが「重く」なってしまう。
- そんな時にはpunditというGemが役に立つ。
- これを使えば以下のように、権限周りの条件文を
app/policies
以下のディレクトリで一元管理することができる。(細かい使い方は簡単なので、リポジトリを参照)
# app/policies/article_policy.rb
# ログインユーザーの「記事オブジェクト」に関する行動範囲
class ArticlePolicy < ApplicationPolicy
# 詳細ページへは誰でもいける
def show?
true
end
# newページへはcreateができる権限でないと行けない
def new?
create?
end
# ログインユーザーがpennameを入力してないと作成できない。
def create?
user.penname.present?
end
end
決まり切った処理はServiceクラスでパッケージ化
- 複数のテーブルが関連するDB処理の場合、順番が大事なケースがある。
- その場合、Serviceクラスで処理を全てパッケージ化すると、管理がとても楽になる
- サービスクラスの鉄則として、クラス内で直接モデルを呼び出さない。必要なモデルは全て引数から入れる
- いわゆる「ダックタイピング」な疎結合のクラスを作ることで、モデルの呼び出しを全てコントローラー内に収めることができる。
- また小ネタ的な使い方だが、サービスクラス内で処理されるモデルを全部
attr_reader
に登録しておくことで、「ああこのサービスクラスではこのモデルを処理してるのか」が一発でわかる。 - あとは「なんのためのサービスクラスか」をコメントしておくと、よりGOOD。
# app/services/sample_service.rb
# UserとDogを同時に保存するサービスクラス
class SampleService
attr_reader :user, :dog
def initialize(user, dog)
@user = user
@dog = dog
end
# ActiveRecord::RecordInvalidをthrowする可能性があるため
def call!
ActiveRecord::Base.transaction do
user.save!
dog.save!
# これはアウト
# Dog.create!(user: user)
end
end
end