doc.dev1x.org

Unit of Work

1. 目的

複数のDB操作モジュールのトランザクションをまとめて取り扱いたい

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()

3. 解決策

トランザクションをまとめて管理するクラスを定義

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 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)
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.")

使い方

if __name__=='__main__':

    uow = UnitOfWork()
    cmd = SaveCommand("Value01", "Value02")
    uow.do(cmd)  # => Start Transaction. Execute SQL. Execute SQL. Commit.

4. メリット

それぞれのモジュールの責務を単一に抑えつつ、トランザクション制御をまとめて取り扱えるようになった

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を関数オブジェクトに置き換えるパターン

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.