【オレオレ】MySQLをPythonで操作したい時の設計【フレームワーク】

252, 2019-11-23

目次

はじめに

ライブラリやフレームワークを使わずに Python で WordPress を操作したい時がありました。
色々と設計を試しているとある程度、方向性が見えてきました。
この記事ではその設計について書いています。

PythonでMySQLを使う

Python の MySQL のインターフェースについては↓の記事が参考になりました。

設計を考える

DBのテーブルを一つのオブジェクトとして考える。テーブルはリレーで互いに関連を持つ。
例えば、WordPress では wp_posts と wp_postmeta が関連を持つ。
wp_posts.ID と wp_postmeta.post_id など。
wp_posts のレコードが削除されると、関連を持った wp_postmeta.post_id のレコード群も削除できると嬉しい。
ということは、

Post.delete()
PostMeta.delete()
PostMeta.delete()
...

これらの処理を内包するクラスを作成すると

Page.delete()

こうなる。
ああ、何か簡単そうだ。これにしよう(白目

設計する

DBのテーブルへの問い合わせを誰に担当させるか。
これはテーブルから作成したオブジェクトにさせよう。
こまかい処理はユーザに任せて、大雑把な処理のインターフェースを提供したい。
たとえば insert や update や delete.

meta = PostMeta()
meta.insert()
meta.update()
meta.delete()

そうするとDBへクエリを送らなきゃいけないから、オブジェクトにDBへのインターフェースを提供する必要がある。

db = Database()

meta = PostMeta(db)
meta.insert()
meta.update()
meta.delete()

db.commit()

insert を実現させるために、オブジェクトにバッファを用意する。

class PostMeta:
    def __init__(self, db):
        self._db = db
        self._meta = {
                'meta_id': None,
                'post_id': None,
                'meta_key': None,
                'meta_value': None,
        }

    def set(self, key, value):
        TODO

    def get(self, key):
        TODO

ユーザはバッファに値をセットしたら後はオブジェクトに任せてアニメ鑑賞でもする。相撲観戦も捨てがたい。

db = Database()

meta = PostMeta(db)
meta.set('meta_key', 'key name')
meta.set('meta_value', 'value')
meta.insert()

db.commit()

insert はユニークなキーで auto_increment な meta_id を返す。これはINSERT後に

SELECT LAST_INSERT_ID()

で取得できる。

db = Database()

meta = PostMeta(db)
meta.set('meta_key', 'key name')
meta.set('meta_value', 'value')
meta_id = meta.insert()
meta.set('meta_id', meta_id)

db.commit()

meta_id をセットしたら、update も可能になる。

db = Database()

meta = PostMeta(db)
meta.set('meta_key', 'key name')
meta.set('meta_value', 'value')

meta_id = meta.insert()
meta.set('meta_id', meta_id)

meta.set('meta_value', 'update value')
meta.update()

db.commit()

バッファを用意しているから、delete などは一度バッファにロードさせてから行う。
これはコピー・アンドゥ等が必要でなければ無駄だから、削除用のクラスか関数を別に用意して、delete()内部で委譲させよう。
ロードもファクトリーを用意して、ファクトリーにさせればオブジェクトがシンプルになって良い予感がする。
ロードのキーの取得はユーザに任せよう。

db = Database()
factory = Factory(db)

post_id = db.execute('SELECT ID FROM wp_posts WHERE post_title=(%s)', ('my post title',))

# delete_metas(post_id)

metas = factory.load_metas(post_id=post_id)
for meta in metas:
    meta.delete()

db.commit()

こんな感じで Post も用意して、それらを内包する Page も作成する。

db = Database()
factory = Factory(db)

page = factory.make_page()
post = page.get_post()
post.set('post_title', 'my post title')

meta = factory.make_meta()
meta.set('meta_key', 'key name')
meta.set('meta_value', 'value')
page.set_meta(meta)

page.insert()

db.commit()

Page の insert は各オブジェクトに任せる。

def insert(self):
    # Post
    post_id = self._post.insert()
    self._post.set('ID', post_id)

    # PostMeta
    for meta in self._metas:
        meta.set('post_id', post_id)
        meta_id = meta.insert()
        meta.set('meta_id', meta_id)

self._metas は、meta_key をキーに dict にして重複した PostMeta を上書きしちゃう。

def set_meta(self, meta)
    # TODO: error check
    self._metas[meta.get('meta_key')] = meta

これで、例えば Factory に色々 Page のテンプレを用意しておけば簡単に書けそう。

db = Database()
factory = Factory(db)

apple_page = factory.make_page('apple')
banana_page = factory.make_page('banana')

apple_page.insert()
banana_page.insert()

db.commit()

Post と PostMeta は内部のバッファが違うだけだから、バッファをコンポジションすればラクかもしれない。
継承と抽象クラスを使ってみたら、いい感じにコード量が減った。
バッファのコンポジションは Factory がちゃんとやらないといけない。

Record   Commit
    \      /
    PostMeta

Commit: DB Interface (Abstract Class)
Record: Buffer (Base Class)

Record は dict のラップだけど、これもう dict で良いのかもしれない。
移植性を考えると、ある程度ラップしてインターフェースを統一したほうが良い気がする。

db = Database()
fields = {
    'meta_id': None,
    'post_id': None,
    'meta_key': None,
    'meta_value': None,
}
meta = PostMeta(db, fields)

meta.set('meta_key', 'key name')
meta.set('meta_value', 'value')

# Meta.insert() は id を返す
meta_id = meta.insert()
meta.set('meta_id', meta_id)

TODO

予測される(経験上の)バグとして、バッファの更新ミスがある。これは Page の作成者がちゃんとやる必要がある。

meta_id = meta.insert()
meta.set('meta_id', meta_id)  # これを忘れると
meta.update()  # UPDATE できない

バッファの更新と言えば、Page の作成者はバッファの更新を明確に定義する必要がある。たとえば delete で id を更新するのかしないのか(検証してない)。
更新しなければ insert でバッファに残った古い ID で INSERT し、更新すれば新しい ID で INSERT する。
たぶんユーザが選択できるようにするのが吉。

def delete(self):
    self._post.delete()
    self._post.set('ID', None)  # 更新方法は?
    for meta in self._metas:
        meta.delete()
        meta.set('meta_id', None)  # 更新方法は?

おまけ

複数のポストをインサートしてから、一秒ごとにポストのタイトルをマルチスレッドで更新するテスト。

WordPressを生のPythonで操作している所

おわりに

DBと連携した設計は始めてで、この設計も5回書き直しましたが、奥が深い・・・。

フレームワークの設計者は偉大

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク