Unit of Work
1. 目的
複数のDB操作モジュールのトランザクションをまとめて取り扱いたい
- ビジネスロジックから複数のDB操作モジュール(リポジトリ)を扱う必要がある時、それらの実行は同じトランザクションで扱いたい
- トランザクションが分割されてしまうとACID特性が満たされず、トランザクションの意味が無い
- 例えば、以下のコードのようにDB操作モジュールのメソッドの中で個別にトランザクション処理を行っていると、
Repository01
とRepository02
のトランザクションは分断されてしまう
class Repository01:
def __init__(self, db):
self.db = db
def save(self, value):
try:
sql = "insert into TBL01 (ROW1) values (?)"
param = [value]
self.db.begin()
self.db.execute(sql, param)
self.db.commit()
except:
self.db.rollback()
class Repository02:
def __init__(self, db):
self.db = db
def save(self, value):
try:
sql = "insert into TBL02 (ROW1) values (?)"
param = [value]
self.db.begin()
self.db.execute(sql, param)
self.db.commit()
except:
self.db.rollback()
2. 課題
トランザクション単位でメソッドを切るのは悪手
- 上述のような状況で、まず思いつくのが同じトランザクションを行う処理をひとつのメソッドにまとめてしまう事である
- 具体的には以下のようなコードになる
class Repository01:
def __init__(self, db):
self.db = db
def save_tbl1_and_tbl2(self, value):
try:
sql1 = "insert into TBL01 (ROW1) values (?)"
param1 = [value]
sql2 = "insert into TBL02 (ROW1) values (?)"
param2 = [value]
self.db.begin()
self.db.execute(sql1, param1)
self.db.execute(sql2, param2)
self.db.commit()
except:
self.db.rollback()
- しかし、このようなコードでは、以下のような複数の問題が発生してしまう
- (1) トランザクションのパターン毎にメソッドを増やす必要がある
- (2) データ操作モジュールの責務が曖昧になっていく
- (3) 同時に更新するテーブルの数が増えれば増えるほどコードの可読性も低下していく
- したがって、あまり良い方法とは言えない
3. 解決策
トランザクションをまとめて管理するクラスを定義
- 先述した課題を解決する為に、トランザクション制御のみを管理するモジュールを導入する(=Unit of Work)
class UnitOfWork:
def __init__(self):
self.db = DB()
def do(self, cmd):
try:
self.db.begin()
cmd.execute(self.db)
self.db.commit()
except:
self.db.rollback()
cmd
は後述するが、データ操作モジュールを内包したデータ操作処理をカプセル化したクラス(Commandパターン参照)
データ操作モジュールとその操作をカプセル化したモジュールを定義
class Repository01:
def save(self, db, value):
sql = "insert into TBL01 (ROW1) values (?)"
param = [value]
db.execute(sql, param)
class Repository02:
def save(self, db, value):
sql = "insert into TBL02 (ROW1) values (?)"
param = [value]
db.execute(sql, param)
class Command:
def execute(self, db):
pass
class SaveCommand(Command):
def __init__(self, value1, value12):
self.value1 = value1
self.value2 = value2
self.repo1 = Repository01()
self.repo2 = Repository02()
def execute(self, db):
self.repo1.save(db, self.value1)
self.repo2.save(db, self.value2)
- 具体的なDB操作は
Repository
が担当し、Repository
の実行とそれに必要なデータの保持をCommand
が請け負う
Repository
のsave
メソッドの中ではトランザクション制御は行わず、トランザクション制御はUnitOfWork
モジュールに委譲する形になる
- DBのコネクションについては話を簡単にする為、ここではダミー実装とする
class DB:
def begin(self):
print("Start Transaction.")
def commit(self):
print("Commit.")
def rollback(self):
print("Rollback.")
def execute(self, sql, param):
print("Execute SQL.")
使い方
Command
のインスタンスをUnitOfWork
のdo
メソッドに与えて、まとめてデータ保存処理を実行させる
if __name__=='__main__':
uow = UnitOfWork()
cmd = SaveCommand("Value01", "Value02")
uow.do(cmd) # => Start Transaction. Execute SQL. Execute SQL. Commit.
4. メリット
それぞれのモジュールの責務を単一に抑えつつ、トランザクション制御をまとめて取り扱えるようになった
Repository
は単一のテーブルに対するSQLの実行、Command
はデータ操作処理のカプセル化、UnitOfWork
はトランザクションの制御とそれぞれのモジュールの役割は明確になった
UnitOfWork
を導入することでRepository
はトランザクションを気にすることなく、データの保存処理に専念できるようになった
UnitOfWork
を導入することで複数のデータ操作モジュールのトランザクションをまとめて取り扱えるようになった
5. デメリット
構成モジュール数の増加に伴う複雑化
- データ操作とトランザクションを構成するモジュール数が増えるので、全体的な複雑度は上昇する
6. 注意事項
Appendix-1 サンプルコード全体
# coding: utf-8
class UnitOfWork:
def __init__(self):
self.db = DB()
def do(self, cmd):
try:
self.db.begin()
cmd.execute(self.db)
self.db.commit()
except:
self.db.rollback()
class DB:
def begin(self):
print("Start Transaction.")
def commit(self):
print("Commit.")
def rollback(self):
print("Rollback.")
def execute(self, sql, param):
print("Execute SQL.")
class Repository01:
def save(self, db, value):
sql = "insert into TBL01 (ROW1) values (?)"
param = [value]
db.execute(sql, param)
class Repository02:
def save(self, db, value):
sql = "insert into TBL02 (ROW1) values (?)"
param = [value]
db.execute(sql, param)
class Command:
def execute(self, db):
pass
class SaveCommand(Command):
def __init__(self, value1, value12):
self.value1 = value1
self.value2 = value2
self.repo1 = Repository01()
self.repo2 = Repository02()
def execute(self, db):
self.repo1.save(db, self.value1)
self.repo2.save(db, self.value2)
if __name__=='__main__':
uow = UnitOfWork()
cmd = SaveCommand("Value01", "Value02")
uow.do(cmd) # => Start Transaction. Execute SQL. Execute SQL. Commit.
Appendix-2 Commandを関数オブジェクトに置き換えるパターン
- 本編の説明では言語依存を避ける為、全てのモジュールをクラスとして定義したが、
Command
はクラスではなく関数オブジェクトにした方がコードがシンプルになる
class UnitOfWork:
def __init__(self):
self.db = DB()
def do(self, fn):
try:
self.db.begin()
fn(self.db)
self.db.commit()
except:
self.db.rollback()
# RepositoryとDBは変化無しなので省略
# Commandは必要無くなったので全て削除
if __name__=='__main__':
uow = UnitOfWork()
def _save(db):
repo1 = Repository01()
repo2 = Repository02()
repo1.save(db, "Value01")
repo2.save(db, "Value02")
uow.do(_save) # => Start Transaction. Execute SQL. Execute SQL. Commit.
- Pythonでは複数行の無名関数が書けないので内部関数として定義しているが、無名関数に複数行書ける言語なら無名関数を使う方がよい
- トランザクションのグループは個別のビジネスロジックに強く依存する事が多いので、あえてクラスにしても再利用する可能性が低く、そうであるなら局所的なものとしてビジネスロジック内で無名関数(or 内部関数)にしてしまっても問題は起こりづらい