DatabaseやORMにおける関心の分離を意識した設計
はじめに
「関心の分離を意識した名前設計で巨大クラスを爆殺する」という記事を読んで、これはDatabase設計やORMを使ったクラス設計にも応用できるのではと思いました。
この記事では、DatabaseとORMにおける関心の分離の重要性と、それを実現するための具体的な方法について解説します。特に、プロジェクトが成長するにつれて発生しがちな巨大なテーブルやクラスを避けるための考え方を共有します。
関心の分離の概念
定義
関心の分離とは、異なる機能や役割を持つ部分を分離し、それぞれが独立して変更できるようにする設計原則です。これは、ソフトウェア設計全般において重要な概念であり、コードの可読性や保守性、再利用性を高めるために欠かせないものです。
メリット
関心の分離を行うことで、以下のような利点があります。
- コードの可読性が向上: 複雑な構造をシンプルにし、理解しやすくします。コードの意図が明確になるため、新しいメンバーでも迅速に理解できます。
- 保守性が向上: 独立した部分のみを変更すれば良いので、修正が容易になります。バグ修正や機能追加がスムーズに行えます。
- 再利用性の向上: 汎用的な部分を他のプロジェクトでも再利用しやすくなります。例えば、共通のユーティリティクラスやサービスを他のプロジェクトに転用できます。
Databaseにおける関心の分離
データベース設計の基本原則
データベース設計では、関心の分離を意識してテーブルを設計することが重要です。関心の分離を無視すると、巨大なテーブルが出来上がり、パフォーマンスやメンテナンス性に悪影響を及ぼします。
正規化
正規化は、データの重複を避け、データの整合性を保つための基本的な手法です。
テーブルの分割
テーブルを分割することで、データの管理が容易になります。例えば、ユーザーデータを管理する場合、基本情報(名前、メールアドレス)と詳細情報(住所、電話番号)を別々のテーブルに分けることが考えられます。こうすることで、異なる関心を持つデータを明確に分離できます。
例
データベースを設計していると、はじめはある程度小さなテーブルであっても、名前の意味が大きいとプロダクトが成長するにつれてどんどん新しいカラムが追加されていきます。
その結果テーブルのカラム数が100を超える、とても横におおきなテーブルが出来上がります。(縦:row / 横:column)
またSQLを作る際にJOINする手間や、DBのパフォーマンスを気にして弊社では一つのテーブルに追加していく傾向があります。
(特にそうしていくという合意はとっていないので、テーブル名の意味が大きいと感覚的にそうなりやすいのだと思います)
そのため、はじめにつけたテーブル名の意味が大きければ大きいほど、超巨大なテーブルとなっていきます。
特に多少かすっているが、意味として分けたほうが良さそうなものであっても追加されていく傾向にあります。
多対一や多対多になるものは正規化されていきますが、一対一であれば分離しないことが本当に多いです。
パフォーマンスは適切に index を付与することで、そこまでのオーバヘッドは無いはずなので、データベースにおいても関心を分離した適切な名前をつけて、テーブルが横に大きくなりすぎないようにしたいと思います。
ただ Class 設計と違うなと思う点はデータベースにおいては意味が重複するデータを複数のテーブルに完全に分けると、データの不整合が起きたり、内部的にそれぞれがつながっているということを表現するために各テーブル毎に対する外部キーを設定したりと、大変な一面が存在します。
Class の場合だと Interface や Abstract Class などの抽象表現を利用して同じものを扱うことも容易にできますが、データベースだとそういった表現ができないのでどうしても中核となるテーブルは必要になってくるように思います。
そこで具体的に「予定」という大きな関心を集めるテーブルを例に考えてみたいと思います。
例えば、予定管理システムを設計しているとします。
- 予定には「仕事」「ゲーム」「家事」などがあります
- 各予定は異なるテーブルと紐づきます
- 「仕事」:「職場」テーブルから働く場所(拠点)を選択する必要があったり、業務内容の「タスク」と紐づく
- 「ゲーム」:「ゲームタイトル」をもっていて、実際にゲームを行う「部屋」と紐づく
- 「家事」:家事を行う「部屋」(ゲームと一緒)と「家事の種類」が紐づく
各予定には共通して「いつから」「いつまで」「誰がやるか」という情報が必要です。これらを関心の分離で設計すると、各予定ごとに別々のテーブルを用意しつつ、それらを統合する「コアとなる予定」テーブルを作成することで、一元管理が可能です。
これを仮に関心の分離ということでそれぞれ全くの別物としてテーブルを設計すると、予定の開始する順番を取得したいとなっても、それを実現することは難しくなります(できなくはないと思う)
逆にこれは全て「予定」だということで、一つのテーブルにすると、各予定に全く関係のない情報を無駄にもつ必要がでてきたり、仕事の予定では「タスク」は必須でも、「タスク」をNullableにしなければならないといった、データベースによるデータの保証ができなくなります。
そのためデータベースでは関心の分離をするために「仕事の予定」「ゲームの予定」「家事の予定」とテーブルを分けたとしても「コアとなる予定」という、それぞれを紐づけるためのテーブルも必要になると思います。
そうすることで外部キーを設定する際も、各予定テーブルが「コアとなる予定」テーブルのIDを持つことで、分離をしながら同じ概念であるデータを保証するべきところでは保証できるようになります。
実際の設計例
実際に上記の例を元にテーブル設計をすると、予定管理システムでは以下のようななります。
-
core_schedules
:コアとなる予定テーブルid
start_datetime
end_datetime
user_id
-
work_schedules
:仕事の予定テーブルid
core_schedule_id
workplace_id
task_id
-
game_schedule
:ゲームの予定テーブルid
core_schedule_id
game_title
room_id
-
household_schedule
:家事の予定テーブルid
core_schedule_id
room_id
task_type
分割の利点
このようにテーブルを分割することで、各予定タイプごとに特化した情報を管理できます。これにより、以下のような利点が得られます。
- データの整合性:各テーブルが特定の関心に特化しているため、データの整合性を保ちやすくなります
- クエリの効率化:必要なデータのみをクエリすることができ、パフォーマンスが向上します
- 保守性の向上:新しい予定タイプを追加する場合でも、既存のテーブルに影響を与えることなく拡張が可能です
ORMにおける関心の分離
ORMの役割
ORM(Object-Relational Mapping)は、データベースとオブジェクト指向プログラムの橋渡しを行います。ORMを使用することで、データベース操作を抽象化し、プログラマがSQLの詳細に煩わされることなくデータ操作を行えます。
例
初めからデータベースを設計できるならテーブル毎に関心を分離した名前をつけて、適切な大きさのテーブルを作っていくことができます。
しかしながら現実問題、本番環境で縦にも横にもとても大きなテーブルが既に出来上がっていて、簡単には構造を変更できないということもあると思います。
そういった場合にはテーブル構造を論理的に分離して考えることで、ORMで利用するクラスを分離することができると思います。
ORMのクラスとテーブルが一対一となるように実装されることが(弊社では)多いのですが、それはつまり100個のカラムがあると100個のプロパティを持つことを意味しています。
こうして多くの情報をもつクラスに様々な機能を付け加えていくと、あっという間に巨大な神クラスが出来上がります。
先ほどの予定管理システムの例で考えてみます。
「予定」クラスに「仕事」「ゲーム」「家事」という3つの関心事が集中している状態だと、「予定」という意味では正しい機能がどんどん追加されていきます。
例えば以下のようなメソッドが追加されます
- 開始時間を取得する Method (どの予定でも必ず値を返す)
- 「仕事」の場所の名前を取得する Method (ただし「仕事」の予定以外は null を返す)
- 「ゲーム」のタイトルを取得する Method (ただし「ゲーム」の予定以外は null を返す)
- 「家事」の種類を取得する Method (ただし「家事」の予定以外は null を返す)
これらは「予定」とひとくくりにしたら、確かに間違ってはいないのかもしれません
しかしこれは「仕事の予定」クラスのように分離可能な内容だと思います。
オブジェクト指向で考えるなら「予定」という基底クラスを定義して「仕事の予定」という子クラスを作るべきです。
しかしテーブルと一対一で対応するクラスを作らなければならないという既成概念にとらわれると分離することができなくなります。フレームワークによっては、デフォルトで一対一のクラスを自動生成するものだとなおさらです。
しかしデータベースで不要な情報を持つということは、カラムはNullableである可能性があります。そうではなくても、構造の変更はできないかもしれないが、Defaultの設定は可能だと思います。
そう考えるとクラスでは関心事に分離した名前のクラスを作り、不要なデータはDefaultの値が入るようにすれば、クラスとテーブルが多対一となるように設計しても問題ないことになります。
そのため予定の例をORMで設計すると以下のようにすることでしょう。
- 「予定」クラスを基底クラスとし、それぞれ「仕事の予定」「ゲームの予定」「家事の予定」クラスを継承します
- 基底クラスには共通のメソッドを持たせ、各派生クラスには特定の関心事に関連するメソッドを持たせます
クラス設計の例
予定システムでは以下のようなクラス構造を持ちます。
-
CoreSchedule
クラスid
start_time
end_time
user_id
-
WorkSchedule
クラスid
core_schedule_id
workplace_id
task_id
-
GameSchedule
クラスid
core_schedule_id
game_title
room_id
-
HouseholdSchedule
クラスid
core_schedule_id
room_id
task_type_id
メソッドの追加
各クラスには、それぞれの関心事に関連するメソッドを追加します。
-
CoreSchedule
クラスgetStartTime()
getEndTime()
getUser()
-
WorkSchedule
クラスgetWorkplaceName()
getTask()
-
GameSchedule
クラスgetGameTitle()
getRoom()
-
HouseholdSchedule
クラスgetRoom()
getTaskType()
クラスの分離の利点
このようにクラスを分離することで、以下のような利点が得られます。
- コードの可読性の向上:各クラスが特定の関心に特化しているため、コードがシンプルで読みやすくなります
- 保守性の向上:各クラスが独立しているため、修正や拡張が容易になります
- 再利用性の向上:特定の関心事に特化したクラスを他のプロジェクトでも再利用しやすくなります
実際の開発における課題と対策
既存システムの改修
現実的には、既存のシステムがすでに巨大なテーブルやクラスで構成されており、簡単には変更できない場合が多いです。このような場合には、以下のような対策を考えることが重要です。
部分的なリファクタリング
既存のシステム全体を一度に改修するのは現実的ではないため、部分的にリファクタリングを行います。例えば、特定の機能やモジュールに関心の分離を適用し、徐々に改善していく方法があります。
テーブルの分割とマイグレーション
巨大なテーブルを分割する際には、データのマイグレーションが必要です。このプロセスは慎重に計画し、データの整合性を保ちながら段階的に移行することが重要です。
ORMの利用
ORMを利用する場合、テーブルとモデルを一対一で対応させるのが一般的ですが、関心の分離を意識してモデルを設計することで、巨大なクラスを避けることができます。
モデルの分割
ORMモデルを関心ごとに分割することで、クラスの巨大化を防ぎます。例えば、Schedule
モデルを分割して、WorkSchedule
、GameSchedule
、HouseholdSchedule
といった具合に派生クラスを作成します。
関心の分離を活用した設計
関心の分離を意識した設計を行うことで、コードの可読性や保守性が向上します。また、モデルを分割することで、各モデルが特定の関心に特化し、必要なデータのみを保持することができます。
まとめ
データベース設計でもORMを利用する場合でも、関心の分離を意識した名前を付け、必要に応じて情報を統合するためのコアテーブルや基底クラスを作成することが重要です。これにより、コードの可読性や保守性が向上し、再利用性も高まります。
関心の分離を意識した設計は、初期段階での手間はかかりますが、長期的には多くのメリットがあります。プロジェクトが成長し、要件が複雑になるにつれて、その効果はより顕著になります。この記事が少しでも参考になれば幸いです。
あくまで個人的な意見ですなので、反対意見や他の意見も大歓迎です。
この記事を読んで、関心の分離を意識した設計の重要性について考えるきっかけになればと思います。