1つの設定を複数モデルに分散させるRailsパターン
はじめに
Railsアプリを開発している中で、「あるモデルの設定を、関連する複数のモデルに自動で反映させたい」 という課題に直面しました。
そこで使用した実装方法を、学習塾システムを題材に、「クラス管理者が生徒の通知設定を一括で管理できる仕組み」を実装するパターンとして紹介します。
この方法を用いることで、保守性・拡張性の高い「設定の継承」をシンプルに実現できます。
前提
サービス概要
以下のようなシステムを想定します:
- 学習塾で利用されるシステム
- 各生徒に対して「宿題リマインダー通知」や「お知らせ通知」を行える
- クラスごとに通知方針が異なる(受験クラスは通知多め、自習重視のクラスは通知少なめ等)
モデル定義
CramSchool
)および塾のクラス(Classroom
)
学習塾(学習塾には複数のクラスがあり、クラスには複数の生徒が在籍しています。
クラスには、その特徴に応じた通知のデフォルト設定があります。
# 学習塾
class CramSchool < ApplicationRecord
has_many :classrooms
has_many :students, through: :classrooms
end
# 塾のクラス
class Classroom < ApplicationRecord
belongs_to :cram_school
has_many :students
has_one :default_setting
# クラス作成と同時に、デフォルト設定も作成する
after_create :create_default_setting!
end
Student
)
塾の生徒(必ずどこか1つのクラスに在籍しています。
生徒ごとに、「SMS通知」と「Email通知」のON/OFFを行えます。
class Student < ApplicationRecord
belongs_to :classroom
has_one :cram_school, through: :classroom
has_one :sms_setting # 生徒ごとのSMS通知設定
has_one :email_setting # 生徒ごとのEmail通知設定
end
DefaultSetting
)
クラスごとのデフォルト設定(class DefaultSetting < ApplicationRecord
belongs_to :classroom
# 各通知タイプのデフォルト設定(Boolean型のカラム)
# - sms_homework_reminder(宿題リマインダーのSMS通知)
# - email_homework_reminder(宿題リマインダーのEmail通知)
# - sms_announcement(生徒へのお知らせのSMS通知)
# - email_announcement(生徒へのお知らせのEmail通知)
end
生徒ごとのSMS通知設定
class SmsSetting < ApplicationRecord
belongs_to :student
# Boolean型のカラム
# - homework_reminder(宿題リマインダー)
# - announcement(生徒へのお知らせ)
end
生徒ごとのEmail通知設定
class EmailSetting < ApplicationRecord
belongs_to :student
# Boolean型のカラム
# - homework_reminder(宿題リマインダー)
# - announcement(生徒へのお知らせ)
end
課題
クラスの管理者は、クラスごとの通知のデフォルト設定を管理できます。このデフォルト値を、新しい生徒がクラスに入った時に自動適用する必要があります。
しかし、通知方法が複数(SMS、Email)あり、それぞれ別々のモデルで管理している場合、単純に実装すると以下の問題が生じます:
- SMS/Email以外の通知が増えると、コードをコピペして修正箇所が増える
- カラム追加のたびにこのメソッドを修正する必要がある
- 他の箇所でもデフォルト設定を参照する場合は、そこも修正が必要
def apply_default_setting
default_setting = classroom.default_setting
# SMS設定を個別に指定
create_sms_setting!(
homework_reminder: default_setting.sms_homework_reminder,
announcement: default_setting.sms_announcement
# 他の通知タイプも同様に追加が必要
)
# Email設定を個別に指定
create_email_setting!(
homework_reminder: default_setting.email_homework_reminder,
announcement: default_setting.email_announcement
# 他の通知タイプも同様に追加が必要
)
end
この課題を解決するのが、以下で紹介する「マッピング」を用いた実装パターンです。
実装パターン
1. マッピング定数の定義
各設定モデルに、自身の属性とデフォルト設定の属性の対応マップを定義します。
class SmsSetting < ApplicationRecord
belongs_to :student
# 自分がもつ属性と、それに対応するデフォルト設定の属性名のマッピング
DEFAULT_SETTING_MAP = {
"homework_reminder" => "sms_homework_reminder",
"announcement" => "sms_announcement"
}.freeze
end
class EmailSetting < ApplicationRecord
belongs_to :student
DEFAULT_SETTING_MAP = {
"homework_reminder" => "email_homework_reminder",
"announcement" => "email_announcement"
}.freeze
end
2. デフォルト設定を適用するメソッドの実装
各設定モデルに、DefaultSettingから設定用パラメータを生成するクラスメソッドを作成します。
ポイント:
-
transform_values
メソッドを用いることで、マッピングのvalues部分だけを変換 -
send
メソッドを用いることで、動的にDefaultSettingの属性を参照
class SmsSetting < ApplicationRecord
# 既存のコード...
def self.build_for_default(default_setting:)
DEFAULT_SETTING_MAP.transform_values do |default_attr|
default_setting.send(default_attr)
end
# 実行例: { "homework_reminder" => true, "announcement" => false }
end
end
class EmailSetting < ApplicationRecord
# 既存のコード...
def self.build_for_default(default_setting:)
DEFAULT_SETTING_MAP.transform_values do |default_attr|
default_setting.send(default_attr)
end
# 実行例: { "homework_reminder" => true, "announcement" => true }
end
end
3. 設定反映の実装
Studentモデルに、デフォルト設定を個々の通知設定に適用するメソッドを実装します。after_create
コールバックを用いて、Studentが作成されたタイミングで自動的にデフォルト設定を適用させます。
class Student < ApplicationRecord
belongs_to :classroom
has_one :sms_setting
has_one :email_setting
after_create :apply_default_setting
private
# デフォルト設定を適用する
def apply_default_setting
default_setting = classroom.default_setting
# SMS設定の作成&デフォルト設定を適用
# **を使ってハッシュを展開
create_sms_setting!(**SmsSetting.build_for_default(default_setting:))
# Email設定の作成&デフォルト設定を適用
create_email_setting!(**EmailSetting.build_for_default(default_setting:))
end
end
使用例
# 学習塾の作成
cram = CramSchool.create!(name: "進学塾ABC")
# 塾のクラスの作成
classroom = cram.classrooms.create!(name: "大学進学コース")
# クラスのデフォルト設定を更新
classroom.default_setting.update!(
sms_homework_reminder: true,
email_homework_reminder: true,
sms_announcement: false,
email_announcement: true
)
# 新しくクラスに入る生徒の作成
student = classroom.students.create!(name: "田中太郎")
# 生徒の通知設定が、自動的にクラスのデフォルト設定に反映されていることを確認
puts student.sms_setting.homework_reminder # => true
puts student.email_setting.homework_reminder # => true
puts student.sms_setting.announcement # => false
puts student.email_setting.announcement # => true
このパターンの利点
保守性の向上
新しい通知タイプ(定期テスト通知、面談予約通知等)を追加する場合でも、各通知モデルのDEFAULT_SETTING_MAP
に項目を追加し、対応するカラムをマイグレーションで追加するだけで済みます。
class SmsSetting < ApplicationRecord
DEFAULT_SETTING_MAP = {
"homework_reminder" => "sms_homework_reminder",
"announcement" => "sms_announcement",
"exam_reminder" => "sms_exam_reminder", # 例:定期テストのSMS通知を追加
}.freeze
end
一貫性の確保
すべての通知設定が同じパターンで管理されるため、コードの構造が統一され、理解しやすくなります。実装者以外のメンバーでも、一度パターンを理解すれば、他の設定についても容易に把握できそうです。
拡張性の向上
新しい通知方法(例:LINE通知、Slack通知、アプリ内プッシュ通知等)を追加する場合も、同じ仕組みを使って簡単に対応できます。
# 例:LINE通知を導入する場合
class LineSetting < ApplicationRecord
belongs_to :student
DEFAULT_SETTING_MAP = {
"homework_reminder" => "line_homework_reminder",
"announcement" => "line_announcement"
}.freeze
def self.build_for_default(default_setting:)
DEFAULT_SETTING_MAP.transform_values { |default_attr|
default_setting.send(default_attr)
}
end
end
おわりに
今回紹介したように、
- 定数によるマッピング
- メタプログラミングによるシンプルな処理
を組み合わせることで、1つのマスター設定を複数モデルに効率よく反映できます。
学習塾システム以外にも、様々なドメインで応用できるパターンですので、ぜひ参考にしてみてください!
Discussion