Python.SQLiteで毎回DBを開閉するクラス
これでDB操作の呼出を綺麗に書けそう。
成果物
Python.Sqlite3.class.20180303100000
概要
SQLite3でSQL実行時、毎回DBを開閉するクラスを書いた。
試行錯誤しメタプログラミングでフレームワーク作った。前回よりもコード量が増えた。
ソースコード
エンドポイント側は、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
注目すべきはdb
。MyDbTransactioner.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単位である
- なんとも中途半端
- ビジネスロジックという単位ではなくDB単位である
- 複数DBを操作する必要があるときは複数のアクセサを使う必要がある
- {任意DB名}Transactioner.py を実装するときに面倒
DbUrl
,DbKwargs
, の属性(property)が必要- AccesserMeta.pyで参照する
dataset.connect()
の引数である
- AccesserMeta.pyで参照する
必須プロパティを定義したクラスを継承したがエラーになった。謎。
./src/7/のコード参照。(db.tables
でエラーになる)
課題
現状
呼出側→全DBアクセス層→固有DBトランザクション層
理想
呼出側→ビジネスロジック層→DB接続層→トランザクション層→固有DB操作層
ひとつのトランザクション内で任意の複数DBを操作できるべき。db
, func
(固有DB操作メソッド) をそれぞれ複数もたせる必要がある。
開発環境
- Raspberry Pi 3 Model B
所感
前回のデコレータは簡単で良かった。やりたいことは、たったこれだけなんだけどなぁ……。