Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

SQLを用いたWebアプリで振る舞いをテーブルに持たせるべきか

解決したいこと

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#側でコードを持たせます。架空のコードですので、実用性の面では疑問符が付く感じです。

EmployeesRoleChecker.cs
// 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;
EmployeesRoleChecker.cs
// もろもろ省略。以下このコメントも省略
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#で定義します。

Role.cs
internal enum Role
{
    Administrator = 1,
    Editor,
    Subscriber,
    // ...
}
EmployeesRoleChecker.cs
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を流用しています。

IRole.cs
internal interface IRole
{
    string Name { get; }
    void ChangeStatus(SomeConnection db, string targetCode, int newStatus);    
}
Administrator.cs
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();
    }
}
Roles.cs
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の実装は省略
        // ...
    };
}
EmployeesRoleChecker.cs
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番がいいと思いますが、同僚からデメリットを指摘されて、確かにな、と悩んでいます。

0

PHPとPostgreSQLで、今1を2に変更していっているところです。ロールや分岐が少ないうちは1で十分だったんですが。
自分の場合はロール(というか種類?)が20ほどあり、21個目をバグを出さずに楽に追加するにはという観点と仕様を一覧表示したいという観点からの変更です。
おそらく1のプログラムも残りそうです。一つのロールで一箇所でしか使わない些細なプログラムの場合はフラグにするメリットもあまり無いので。

2値(NULL許容を使うなら3値)しか持たせられないため、複雑な分岐を実装できない。
ロールが複雑になるにつれフラグが多くなり、管理が行き届かなくなってしまう。

フラグを多くする、intにする、などで複雑な分岐ができますし、SIMPLE_ROLE を受け取ってIRoleの実装を返すものを作れば 4 も併用できると思います。
併用の場合はIRoleではなくIChangeStatus のような機能ごとのインターフェースにするかもしれません。

DBとプログラムの結合度が上がってしまう。

DBとプログラムの開発者が異なっていたりDBの定義を気軽に変更できない環境だとちょっと面倒かもしれないですね。

3 の enum は、言語やツールで網羅性がチェックできるなら考えます。

4 の interface は、ロールが少なく各ロール個別の処理が重い場合は考えます。例のような「管理者」「編集者」「閲覧者」みたいな感じなら 4 にするかもしれません。

まあケースバイケースですよねえ…。
画面上から権限と対応する機能の追加削除をしたいなら選択の余地は無いですし。

1Like

Your answer might help someone💌