RailsのActive Record モデルは原則フラットに保つ(名前空間に入れない)
CoC を最大限に活かし、逆方向推論の破綻とアップグレード負債を避ける個人的な実装指針です。
私は Active Record モデルをトップレベルでフラットに保ち、名前空間を使わない設計を原則とします。
理由は明確で、Railsの強みは Convention over Configuration(CoC) にあります。規約に従えば「設定なしで動く」——この利点を、モデルの階層化(名前空間化)は簡単に失わせます。関連・外部キー・再読み込みの逆方向推論で明示が増え、可読性と保守性が落ちるからです。
(コストと天秤をかけた上での目的があればいいが、なんとなくで使ってCoCから外れて設定が増える方向になるのが個人的に嫌)
参照: The Rails Doctrine
1. 命名方針 - 名前空間 vs 複合語フラット(推奨)
Rails では「質問」「回答」「注文明細」のようなサブエンティティを表現する際に、名前空間でまとめる方法と複合語でフラットにする方法があります。
1.1 名前空間モデルの例(❌避けたい)
# app/models/survey/question.rb
class Survey::Question < ApplicationRecord
has_many :answers, class_name: "Survey::Answer"
end
# app/models/survey/answer.rb
class Survey::Answer < ApplicationRecord
belongs_to :question, class_name: "Survey::Question"
end
-
メリット
- 「Survey 配下の要素」と階層で表現できる
-
デメリット
-
class_nameが必須になりがち -
他の名前空間と関連すると複雑化
-
Zeitwerk でパス・定数の不一致が起きやすい
-
1.2 複合語フラットモデルの例(⭕️推奨)
# app/models/survey_question.rb
class SurveyQuestion < ApplicationRecord
has_many :survey_answers, dependent: :destroy
end
# app/models/survey_answer.rb
class SurveyAnswer < ApplicationRecord
belongs_to :survey_question
end
-
メリット
- Rails の推論に完全準拠(
class_name不要) - ルートやフォームの推論も素直に動作
- Zeitwerk 下でも壊れにくい
- Rails の推論に完全準拠(
-
デメリット
- 「Survey という箱」がなく、階層的な整理感は薄い
1.3 比較表
| 観点 | 名前空間モデル | 複合語フラットモデル(推奨) |
|---|---|---|
| CoC 準拠 | △ (class_name / to_table 明示が増える) | ◎ (規約どおりに動く) |
| 可読性 | △ (階層は直感的だが記述が冗長) | ◎ (シンプルで Rails らしい) |
| 保守性 | △ (関連が跨ぐと複雑化、Zeitwerk の罠あり) | ◎ (壊れにくく移行も容易) |
| 構造表現力 | ○ (Survey::Question でまとまり感は出る) | △ (SurveyQuestion という複合語で表現) |
👉 結論:Active Record モデルはフラットな複合語命名がベストです。
名前空間は見た目の整理感はあるものの、Rails の思想(CoC)に逆行し、逆方向推論で負債を生む可能性が高いからです。
2. 「順方向」は素直でも「逆方向」でつまずく
名前空間モデルでも 順方向(クラス → テーブル) は自動で対応されます(例:Book::Order → book_orders)。
参照: Active Record Basics
ただし逆方向は破綻します。belongs_to / has_many / has_one では 完全修飾クラス名を明示しないと定数解決が崩れるため、class_name の増殖が避けられません。
参照: Active Record Associations — class_name
3. Zeitwerk 下の壊れやすさを避ける(フラットの利点)
Rails 6 以降の Zeitwerk では 「ディレクトリ=名前空間=定数」 の一致が必須です。
深い名前空間を増やすほど、わずかなズレで autoload/reload が不安定になります。フラット命名はこれを根本的に回避します。
参照: Autoloading and Reloading Constants (Zeitwerk mode)
4. SmartHRのリファクタ実例(実務知見)
SmartHR では User::ProfileImage のような名前空間モデルを採用していましたが、
-
関連定義が冗長
-
テーブル名・外部キーが長く直感に反しない
-
クエリでバグを誘発(このバグは社内勉強会で修正済み。rails/rails#46287)
といった問題により、約60ファイル程度のモデルをフラットに戻す大規模リファクタを行っています。
参照: SmartHR Tech Blog: モデル名をすべて変更した話
5. Active Record 以外では名前空間を活用
Active Record モデルはフラットであるべきですが、サービスクラス・フォームオブジェクト・バリューオブジェクト・クエリオブジェクト・ドメインモジュール等では名前空間を活用すべきです。
- 例:
Users::CreateService/Payments::Money/Reports::Generator
👉 結論:AR はフラット、非 AR は名前空間で整理が最も CoC に沿い、壊れにくく、チーム開発に強い。
6. 個人的なチームへの方向性としての合意
Active Record モデルは原則フラットに命名する(例:SurveyQuestion, OrderLineItem)。
名前空間は見た目の整理感はあるが、class_name 明示増・Zeitwerk の脆さ・推論崩れを生むため避けるべき。
一方で、Active Record 以外のクラスは目的に応じて名前空間を積極的に活用する。
参考リンク
名前空間付きモデルを採用しているGitLabの紹介
GitLab という大規模な OSS は敢えてその逆の道—名前空間付きモデルを積極的に使う設計を選んでいます。その背後には以下のような確固たる設計哲学と実務上の必要性があります。
-
ドメインを明確に区切る「Bounded Context」設計
GitLab はCi::BuildやMergeRequests::Diff,Notes::Diffなど、ドメインごとに名前空間でモデルを分けることを明文化し(config/bounded_contexts.ymlに定義)、RuboCop による強制で維持しています。
→ 巨大モノリスでの結合管理と文脈の一貫性を、コード上で確実に担保する狙いです。
参照:Software design guides – Bounded Context -
モデル名とテーブル名の不一致を積極的に吸収
「ユビキタス言語(プロダクト用語)を優先」するため、モデル名とテーブル名がズレる場合は、self.table_name = 'issue_emails'のように明示的にテーブル名を指定することを許容しています。これは Rails の CoC をあえて逸脱しつつ、意味論の一貫性を重視する判断です。
参照:Software design guides – Ubiquitous language・table_name mapping -
複数データベース/トランザクション境界の切り分け
GitLab ではCi::Buildのように異なる DB 接続を使うモデルもあり、明示的に接続を切り替えてトランザクション管理をする必要があります。
→ 名前空間は「物理的境界」をコード上で表し、リソース管理と一貫性の担保に貢献しています。
参照:Database transaction guidelines -
実例:Issue ドメインの Email モデル
GitLab OSS にあるIssue::Emailモデルは、self.table_name = 'issue_emails'を使って命名とテーブルを一致させず、「Issue」ドメインに関する Email モデルを明確に区別するための名前空間設計が適用されています。
参照:Issue::Email モデル(GitLab OSS)
まとめ
GitLab の名前空間付きモデル採用は、「Rails の CoC を守るフラット設計」とは異なるスタンスですが、大規模・多文脈・運用を見据えた堅牢な設計判断に基づいています。
私の提唱(標準規模ではフラット)が成立するのは「CoC の範囲内で軽快さ・安全性を得るため」ですが、GitLab では「別の責務・境界・DB・文脈をコード上で明確にする」ために名前空間を戦略的に使っていると思われます。
Discussion