2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ドメイン駆動設計 データの整合性を保つ

Last updated at Posted at 2020-06-11
1 / 14

はじめに


目次

  • 整合性とは
  • 致命的な不具合を確認する
  • ユニークキー制約による防衛
  • トランザクションによる防衛

整合性とは

  • 矛盾がなく、一貫性のある事・ズレがないこと
  • 例えば、銀行の送金システムの場合、
    • :money_with_wings: 送金側口座のお金を減らす処理
    • :moneybag: 送金先口座のお金を増やす処理
  • これらが全て実行完了、あるいは未実行の状態を担保すること
  • ソフトウェアにとって、データの整合性を保つことは重要なこと
    • 整合性を保つパターンについて確認していきましょう!

致命的な不具合を確認する

  • ユーザ登録処理
  • 「ユーザ名重複を許可しない」というドメイン上のルールがある
  public class UserApplicationService
  {
      private readonly IUserFactory userFactory; 
      private readonly IUserRepository userRepository;
      private readonly UserService userService;

      ...

      public void Register(UserRegisterCommand command)
      {
          var name = new UserName(command.Name);
          var user = userFactory.Create(name);

          // 重複チェック
          if (userService.Exists(user))
          {
              throw new CanNotRegisterUserException(user, "ユーザは既に存在しています。");
          }

          userRepository.Save(user);
      }
  }

↑のコードは一見正しいが、同じユーザ名で同時に登録処理が実行されると重複チェックをすり抜けてしまう問題がある
image.png


ユニークキー制約による防衛

  • データベースの機能で、特定のカラムがユニークであることを保証する
  • データの整合性を守るために積極的に利用すべき
  • ただし、ユニークキー制約はルールを守る主体ではなく、セーフティネットとして活用されるべき
  • 「ユーザ名重複を許可しない」というドメイン上のルールが、データベースのユニークキー制約という特定の技術基盤に依存するのはよくない
  public class UserApplicationService
  {
      private readonly IUserFactory userFactory; 
      private readonly IUserRepository userRepository;
      private readonly UserService userService;

      ...

      public void Register(UserRegisterCommand command)
      {
          var name = new UserName(command.Name);
          var user = userFactory.Create(name);

          // DBのユニークキー制約で重複防げるから重複チェック不要???
          //   →ここにも書くべき。DBのユニークキー制約はあくまでセーフティネット
          if (userService.Exists(user))
          {
              throw new CanNotRegisterUserException(user, "ユーザは既に存在しています。");
          }

          userRepository.Save(user);
      }
  }

トランザクションによる防衛

  • いくつかやり方がある
    • データベースのトランザクション機能を利用する
    • トランザクションスコープを利用する
    • AOPを利用する
    • ユニットオブワークを利用する

トランザクションによる防衛2

  • データベースのトランザクション機能を利用する
  • 最も一般的なパターン
  • DBコネクションからトランザクション開始・コミットを行う
  • エラー発生時はロールバックすることで変更を取り消せる
  • データをロックするので、ロック範囲がなるべく小さく済むように気を付ける
  • Railsでは ActiveRecord::Base.transaction

image.png

  • 特定の技術基盤に依存するのはよくないとか言っておきながらDBのトランザクション機能に依存するのはいいのか? :thinking:
    • 整合性を維持するのは低次元な詳細である特定の技術基盤の役目なので問題ない
      • 先ほどのユーザ登録処理の例の通り、プログラムだけでは限界がある
    • ビジネスロジックに記載すべきは、整合性が必要な処理であることを明示的に主張すること(詳細は気にしなくてよい)

トランザクションによる防衛3

  • トランザクションスコープを利用したパターン
  • DBコネクションを扱う低次元な処理を TransactionScope でラップすることで、アプリケーションサービスは整合性を維持するための詳細を気にせずに済む
    • TransactionScope の実装を変えることで、アプリケーションサービスには影響なくRDB以外にも対応できる
  public class UserApplicationService
  {
      private readonly IUserFactory userFactory; 
      private readonly IUserRepository userRepository;
      private readonly UserService userService;

      public UserApplicationService(IUserFactory userFactory, IUserRepository userRepository, UserService userService)
      {
          this.userFactory = userFactory;
          this.userRepository = userRepository;
          this.userService = userService;
      }

      public void Register(UserRegisterCommand command)
      {
          // トランザクションスコープを生成する
          // using句のスコープ内でコネクションが開かれると自動的にトランザクションが開始される
          using (var transaction = new TransactionScope())
          {
              var name = new UserName(command.Name);
              var user = userFactory.Create(name);

              if (userService.Exists(user))
              {
                  throw new CanNotRegisterUserException(user, "ユーザは既に存在しています。");
              }

              userRepository.Save(user);
              // 処理を反映する際にはコミット処理を行う
              transaction.Complete();
          }
      }
  }

トランザクションによる防衛3

  • AOP(Aspect Oriented Programming)を利用したパターン
  • アノテーションでトランザクションスコープを宣言
  • トランザクションスコープよりも宣言的だし、処理の内容を見なくてもトランザクション処理であることが分かるので良い
  • Ruby, Railsの標準機能ではできない。。
    • Gemはいくつかある
Javaの実装例
public class UserApplicationService {
  private final UserRepository userRepository;
  private final UserFactory userFactory;
  private final UserServiceuserService;

  @Transactional // ←これ!! メソッドが正常終了したらコミット、例外投げられたらロールバック
  public void Register(UserRegisterCommandcommand) {
    UserName userName = newUserName(command.getName());
    Useruser = userFactory.create(userName);

    if (userService.exists(user)) {
      throw newCanNotRegisterUserException(user,"ユーザは既に存在しています。")
    }

    userRepository.save(user);
  }
}

トランザクションによる防衛4

  • ユニットオブワークを利用したパターン
  • ユニットオブワークはあるオブジェクトの変更を記録するオブジェクト
  • 変更した状態を保持しておき、Commitメソッドを呼び出してデータストアに反映する
  • C#のEntityFrameworkはユニットオブワークの実装らしい
public class UnitOfWork
{
  // Registerから始まるメソッドが呼ばれると、インスタンスの状態を変更記録として保持(この時点ではデータストアに適用しない)
  public void RegisterNew(object value) { ... }
  public void RegisterDirty(object value) { ... }
  public void RegisterClean(object value) { ... }
  public void RegisterDeleted(object value) { ... }

  // Commitメソッドが呼ばれると、そこまでの変更をまとめてデータストアに適用する
  public void Commit() { ... }
}

気になったこと

  • 「あるデータを更新するときは別のデータも合わせて更新が必要である」というドメイン知識はどこに実装すべきか?
    • ユースケースに依存するのであればアプリケーションサービスでよさそう
    • データ自体に依存するのであれば、エンティティ??? :thinking:
  • Railsでいう after_create after_save の知識をどこに持てばよいか分からない :thinking:

まとめ

  • トランザクションスコープ、あるいはAOPを使うと良い
    • なければそれに準ずる仕組みを自前で用意する
    • メソッド自体がトランザクションの範囲になるの分かりやすいし良さそう
  • 一つのトランザクションで更新対象とするデータ範囲はなるべく小さくするよう気を付ける
    • ロックが広範囲に渡ると処理が失敗する可能性が上がる
    • デッドロックの危険もある
  • アプリケーションサービスにはトランザクションの宣言だけ記述し、トランザクション処理の詳細が漏れ出ないようにする

おしまい

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?