やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Python.SQLiteで毎回DBを開閉するクラス

これでDB操作の呼出を綺麗に書けそう。

成果物

GitHubPython.Sqlite3.class.20180303100000

概要

SQLite3でSQL実行時、毎回DBを開閉するクラスを書いた。

試行錯誤しメタプログラミングフレームワーク作った。前回よりもコード量が増えた。

ソースコード

./src/6/配下のソースコードが対象。

エンドポイント側は、DBごとの操作メソッドを呼び出すイメージ。

run.py

from db.Accesser import Accesser

table_name = 'MyTable'
nt = {'Id': 'integer', 'Name': 'text'}
Accesser().MyDb.CreateTable(table_name, **nt)

nv = {'Id': 0, 'Name': 'A'}
Accesser().MyDb.Insert(table_name, **nv)

AccesserクラスはAccesserMeta.pyによって動的に作られる。DBファイル名の属性を持つように。(MyDb)

属性値になるのはDB操作クラス。./src/6/db/transact/配下にある。{DB名}Transactioner.py書式のファイルを読み取り、インスタンス生成して、Accesserの属性値としてセットする。

MyDbTransactioner.pyはDB操作クラス。任意のDB操作を実装する。DBファイルパスを受け取ったり、指定できる。任意のDB操作メソッドを作れる。第一引数db(dataset.connect()インスタンス)を受け取れる。

datasetはDBのO/Rマッパ。dataset.connect()でDB接続する)

class MyDbTransactioner:
    def __init__(self):
        self.__db_url = 'sqlite:///test.db'
        self.__dataset_kwargs = None
    @property
    def DbUrl(self): return self.__db_url
    @property
    def DbKwargs(self): return self.__dataset_kwargs

    def CreateTable(self, db, table_name, **name_types):
        if table_name in db.tables: self.DropTable(db, table_name)
        columns = ', '.join(k+' '+v for k,v in name_types.items())
        sql = f'create table {table_name} ({columns});'
        print(sql)
        db.query(sql)
    def DropTable(self, db, table_name):
        sql = f'drop table {table_name};'
        print(sql)
        db.query(sql)
    def Insert(self, db, table_name, **kv):
        columns = ','.join([k for k in kv.keys()])
        values = ','.join(self.__GetInsertValues(kv.values()))
        sql = f'insert into {table_name} ({columns}) values ({values});'
        print(sql)
        db.query(sql)
    def __GetInsertValues(self, values):
        vals = []
        for v in values:
            if isinstance(v, int): vals.append(str(v))
            else: vals.append(f"'{v}'")
        return vals

注目すべきはdbMyDbTransactioner.pyでメソッドを定義するときはdbある。なのに呼出側run.pyでメソッドを呼び出すときはdbない

その仕組みはAccesserMeta.pyを追っていけば分かる。Transactioner.pyにより、dbを渡す処理をはさむ。設定したTransactsioner.TransactメソッドをAccesserのメソッドとして持たせている。メタプログラミングによるフレームワーク作成の賜物。

以下抜粋。コメント参照

AccesserMeta.py

class AccesserMeta(type):

    # Accesserクラス作成
    def __new__(cls, name, bases, attrs):
        for name, transact in cls.__GetTransactioners():
            accesser = cls.__Create(transact)
            attrs[name] = accesser
        return type.__new__(cls, name, bases, attrs)

    # ./transact/配下の {任意DB名}Transactioner.py ファイルを全読込してそのインスタンスを返す
    @classmethod
    def __GetTransactioners(cls):
        path_this = pathlib.Path(__file__).parent
        path_transact = path_this / 'transact'
        path_transacts = path_transact.glob('?*Transactioner.py')
        for path in path_transacts:
            filename = path.name.rstrip('.py')
            module = importlib.import_module(f'db.transact.{filename}')
            cls = getattr(module, filename)
            yield filename.rstrip('Transactioner'), cls()

    # 1. {任意DB名}Transactioner.py クラスのメソッドの引数 に db を渡す
    # 2. そのメソッドをAccesserクラスの属性としてセット
    @classmethod
    def __Create(cls, target_class):
        attrs = {}
        # target_classの自作公開メソッドをすべて取得
        for method_info in inspect.getmembers(target_class, inspect.ismethod):
            method_name = method_info[0]
            method = method_info[1]
            if not isinstance(method, types.MethodType): continue
            if method_name.startswith('_'): continue
            t = Transactioner()
            t.TransactionMethod = method
            t.DbUrl = target_class.DbUrl
            t.DbKwargs = target_class.DbKwargs
            attrs[method_name] = t.Transact
        accesserType = type(target_class.__class__.__name__ + 'Accesser', (object,), attrs)
        return accesserType()

Transactioner.py

# DBの開閉処理
class Transactioner:
   ...
    def Transact(self, *args, **kwargs):
        with dataset.connect(self.__db_url, self.__dataset_kwargs) as db:
            db.begin()
            res = self.__transaction(db, *args, **kwargs)
            db.commit()
            return res
   ...

実行

$ python3 run.py

問題

  • コードが複雑化した
  • 1クラス1DBしか扱えない
    • 複数DBを操作する必要があるときは複数のアクセサを使う必要がある
  • {任意DB名}Transactioner.py を実装するときに面倒
    • DbUrl, DbKwargs, の属性(property)が必要

必須プロパティを定義したクラスを継承したがエラーになった。謎。 ./src/7/のコード参照。(db.tablesでエラーになる)

課題

現状

呼出側→全DBアクセス層→固有DBトランザクション層

理想

呼出側→ビジネスロジック層→DB接続層→トランザクション層→固有DB操作層

ひとつのトランザクション内で任意の複数DBを操作できるべき。db, func(固有DB操作メソッド) をそれぞれ複数もたせる必要がある。

開発環境

所感

前回のデコレータは簡単で良かった。やりたいことは、たったこれだけなんだけどなぁ……。