SQLを用いたWebアプリで振る舞いをテーブルに持たせるべきか
Discussion
解決したいこと
ASP.NET MVC + SQL Serverでアプリを作っているのですが、ロールにどう役割を持たせるかということで悩んでいます。
以下の様なテーブルを作るとします。
CREATE TABLE SIMPLE_ROLE (
ID INT PRIMARY KEY,
NAME NVARCHAR(10) NOT NULL
)
CREATE TABLE EMPLOYEE (
EMPLOYEE_CODE CHAR(10) PRIMARY KEY,
ROLE INT NOT NULL -- SIMPLE_ROLE.IDが外部キー
STATUS INT NOT NULL -- おそらくSTATUSテーブルが外部にあると思われるが省略
-- その他データを省略
)
それに意味合いを持たせる方法として次の2つが考えられます。
1. プログラム側でロジックとしてハードコードする
今回の場合、C#側でコードを持たせます。架空のコードですので、実用性の面では疑問符が付く感じです。
// using, namespace省略
internal class EmployeesRoleChecker
{
// コンストラクタ等省略
public void ChangeStatus(SomeConnection db, string code, string targetCode, int newStatus)
{
EMPLOYEE employee = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == code);
if (employee.ROLE == 1) // 管理者という体。実際にはどこかで定数として定義される
{
EMPLOYEE target = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == targetCode);
target.STATUS = newStatus;
db.saveChanges();
}
}
}
メリット
- DB側のデータが軽くなる。
- ロジックに制限が無い。
デメリット
- DBにはコードと値だけが入っていることになり、それらの意味合いをコード内でも定義しているため、わざわざテーブルに入れておく意味は薄い。
- データの意味合いが曖昧なので、凝集が低くなりがち。
- 追加ロジックの更新が抜け落ちるとバグが発生する。
- 誤ったロジックを実装すればバグが発生する。
- ロール番号に変更があるとコードも修正しなければならない。
- 他のロールも同じ動作をするという場合は、条件にハードコーディングを追加したり、ロジックをコピーしなければならない。
- 上記が相まって、仕様を確定できない。
2. DB側に論理値として動作を持たせる
ロールのテーブルに処理の可否を表すフラグを追加します。
ALTER TABLE SIMPLE_ROLE
ADD CAN_CHANGE_STATUS BIT NOT NULL;
// もろもろ省略。以下このコメントも省略
internal class EmployeesRoleChecker
{
public void ChangeStatus(SomeConnection db, string code, string targetCode, int newStatus)
{
EMPLOYEE employee = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == code);
SIMPLE_ROLE role = db.SIMPLE_ROLE.SingleOrDefault(rl => rl.ID = employee.ROLE)
if (role.CAN_CHANGE_STATUS)
{
EMPLOYEE target = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == targetCode);
target.STATUS = newStatus;
db.saveChanges();
}
}
}
メリット
- DB上でロールの振る舞いに関して一元管理できる。
- 名前さえ適切なら、ロールが何をするかより伝わりやすいので、バグが減る。
- すべて論理値で持たせるため、コードが堅牢になる。
- ロールの設計・変更が柔軟にできる。
- 外部キー制約が使える(なお、データ入力時にコケる場合があるので、勤務先では外部キー制約を使っていない)。
デメリット
- 2値(
NULL
許容を使うなら3値)しか持たせられないため、複雑な分岐を実装できない。 - ロールが複雑になるにつれフラグが多くなり、管理が行き届かなくなってしまう。
- DBとプログラムの結合度が上がってしまう。
3. enum
で管理する
DBに持たせていたロールの情報を完全にC#で定義します。
internal enum Role
{
Administrator = 1,
Editor,
Subscriber,
// ...
}
internal class EmployeesRoleChecker
{
public void ChangeStatus(SomeConnection db, string code, string targetCode, int newStatus)
{
EMPLOYEE employee = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == code);
switch (employee.ROLE)
{
case Role.Administrator:
EMPLOYEE target = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == targetCode);
target.STATUS = newStatus;
db.saveChanges();
break;
case Role.Editor:
if (newStatus != 0)
{
EMPLOYEE target = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == targetCode);
target.STATUS = newStatus;
db.saveChanges();
}
break;
// 他のcase省略
}
}
}
メリット
- DB側のデータがさらに軽くなる。
- ロジックに制限が無い。
- コードから処理が追いやすくなる。
- コードの散逸をある程度防げる。
デメリット
- データの意味合いが曖昧なので、凝集が低くなりがち。
- 外部キー制約が使えないため、不正ロール番号が入らないように注意深くなる必要がある。
- 追加ロジックの更新が抜け落ちるとバグが発生する。
- 誤ったロジックを実装すればバグが発生する。
- 他のロールも同じ動作をするという場合は、条件にハードコーディングを追加したり、ロジックをコピーしなければならない。
- 上記が相まって、仕様を確定できない。
デメリットに関しては、Javaの超多機能なenumだったら多少緩和されるかもしれません。その代わりにロール番号をプロパティとして持たなければなりません。DB仕様の融通が利くのなら、enum名(name()
メソッドの戻り値)をテーブルに入れられるようにしてもいいでしょう。
4. interface
としてロールを定義する
「良いコード/悪いコードで学ぶ設計入門」の「6.2.6 よりスマートにswitch文重複を解消するinterface」にヒントを得た方法です。やはりDB上にロールを用意しません。また、マジックナンバーを防止するため、3. のenum
を流用しています。
internal interface IRole
{
string Name { get; }
void ChangeStatus(SomeConnection db, string targetCode, int newStatus);
}
internal class Administrator : IRole
{
public string Name => "管理者";
public void ChangeStatus(SomeConnection db, string targetCode, int newStatus)
{
EMPLOYEE target = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == targetCode);
target.STATUS = newStatus;
db.saveChanges();
}
}
internal static class Roles
{
internal IReadOnlyDictionary<Role, IRole> TheRoles => RolesList;
private Dictionary<Role, IRole> RolesList = new
{
new { Role.Administrator, new Administrator()},
new { Role.Editor, new Editor()}, // Editorの実装は省略
// ...
};
}
internal class EmployeesRoleChecker
{
public void ChangeStatus(SomeConnection db, string code, string targetCode, int newStatus)
{
EMPLOYEE employee = db.EMPLOYEE.SingleOrDefault(em => em.EMPLOYEE_CODE == code);
IRole role = Roles.RolesList[employee.ROLE];
role.ChangeStatus(db, targetCode, newStatus);
}
}
メリット
- DB側のデータがさらに軽くなる。
- 処理が抽象化されることでロジックの管理がしやすくなる。
-
EmployeesRoleChecker
がすっきりする。 - 共通処理が多い場合は抽象基底クラスを利用することで、ボイラープレートを最小化できる。
-
interface
の実装制約により、メソッド実装漏れは回避できる。 - 適切なコメントを
interface
に残すことにより、ロジックの間違いが起こる可能性は緩和される。 - オブジェクト指向的にロールの役割が明確化する。
デメリット
- 外部キー制約が使えないため、不正ロール番号が入らないように注意深くなる必要がある。
-
interface
にメソッドを追加する際にコストがかかる。 - 追加メソッドをデフォルト実装や抽象基底クラス側で吸収するとバグの温床になる。
絶対は無いと思いますが、この中でどれがいい方法でしょうか。データベースで意味合いを完結させたい私としては2番がいいと思いますが、同僚からデメリットを指摘されて、確かにな、と悩んでいます。