ライブラリ: Pony ORM

Pony ORM

今回は Python の ORM ライブラリである Pony ORM を紹介します。

動作確認に使ったバージョンは次のとおりです。

  • python 3.7.7
  • pony 0.7.12

Pony ORM とは

Pony ORM (以下「 Pony 」)は Python の ORM ライブラリです。 ORM をご存知でない方のために念のために説明すると、 ORM とはオブジェクト・リレーショナル・マッパーの略で、プログラミング言語の「オブジェクト」とリレーショナルデータベースのテーブルとの間のデータのやりとりをよきようにやってくれる機能のことです。

Pony の特徴として公式では次の 4 つが謳われています。

中でも目を引く特徴は直感的なシンタックスで、次のような Python らしいコードで SQL クエリを生成することができます。

select(c for c in Customer if sum(c.orders.total_price) > 1000)
Customer.select(lambda c: sum(c.orders.total_price) > 1000)

実際に使ってみましょう。

Pony をインストールする

pip でインストールします。

python -m pip install pony

インストールが完了したら Python のコンソールでバージョンが確認できるはずです。

import pony

print(pony.__version__)
# => 0.7.12

Pony の基本的な使い方

データベースに接続する・テーブルを定義する

Pony を使うときには、最初にデータベースオブジェクトを生成し、それを使ってテーブルに相当するクラスを定義した上で、そのクラスを使ってデータベースを操作します。

from pony import orm

DB_NAME = 'db.sqlite'

# データベースオブジェクトを生成する
db = orm.Database()
db.bind(provider='sqlite', filename=DB_NAME, create_db=True)

# データベーステーブルに相当するクラスを定義する
class Article(db.Entity):
    """記事データを格納するテーブル"""
    slug = orm.Required(str)
    title = orm.Required(str)
    body = orm.Optional(str)
    data = orm.Optional(orm.Json)

    def __str__(self):
        return 'Article(title="{}")'.format(self.title)

テーブルのカラムは主に pony.orm.Requiredpony.orm.Optional を使用して定義します。引数には対象のカラムの型を渡します。ここでの説明は割愛しますが、その他のカラム設定を行いたい場合は第 2 引数以降で指定することができます。

その他カラムの定義に使えるものとして次のクラスが用意されています。

  • PrimaryKey
  • Set
  • Discriminator

参考:

テーブルに相当するクラスの定義ができたら、続いて実際のテーブルを作成します。テーブルの作成は pony.orm.Databasegenerate_mapping() メソッドを使用します。

db.generate_mapping(create_tables=True)

テーブルにレコードを挿入する

テーブルが作成できたら、テーブルに相当するクラス( Article )を使ってレコードを登録することができます。

with orm.db_session:
    a1 = Article(slug='hello', title='こんにちは')
    a2 = Article(slug='good-bye', title='さようなら')
    orm.commit()

レコードを登録するには、データベースセッションを開いてテーブルに相当するクラスのインスタンスを生成してから、変更をコミットします。データベースセッションを開くにはコンテキストマネージャ pony.orm.db_session を使います。変更のコミットには pony.orm.commit() を使用します。コンテキストマネージャ pony.orm.db_session はコンテキスト脱出時に自動的にコミットを行ってくれるので、上の例の場合は pony.orm.commit() による明示的なコミットは不要です。

pony.orm.db_session は、上の例のようにコンテキストマネージャとして使う方法に加えて関数のデコレータとして使う方法が用意されています:

@orm.db_session
def update_related_data():
    ...

コンテキストマネージャとしてもデコレータとしても使える pony.orm.db_session ですが、上述の「変更のコミット」の他にも「例外があがったときのロールバック」「コネクションプールへのデータベースコネクションの返却」「データベースセッションキャッシュのクリア」等を自動で行ってくれる機能が備わっています。

テーブルのレコードを取得する

テーブルにレコードが登録できたらそれを取得してみましょう。おそらく Pony の最大の特徴はこのレコードの取得方法です。pony.orm.select() とジェネレータ式を使ってシンプル・直感的に SELECT クエリを生成することができます。

全レコードの全カラムを順番に取得する:

with orm.db_session:
    for article in orm.select(a for a in Article):
        print(article)
# => Article(title="こんにちは")
# => Article(title="さようなら")

クラスメソッド select() を使った方法も用意されています。

with orm.db_session:
    for article in Article.select():
        print(article)
# => Article(title="こんにちは")
# => Article(title="さようなら")

全レコードの特定のカラムだけ順番に取得する:

with orm.db_session:
    for slug, title in orm.select((a.slug, a.title) for a in Article):
        print(slug, title)
# => hello こんにちは
# => good-bye さようなら

全要素の特定のカラムを取得しリスト化する:

with orm.db_session:
    slugs = orm.select(a.slug for a in Article)[:]
    print(slugs)
# => ['hello', 'good-bye']

条件に合致する 1 レコードだけ取得する:

with orm.db_session:
    article = orm.select(a for a in Article if a.slug == 'hello').get()
    print(article)
# => Article(title="こんにちは")
with orm.db_session:
    article = Article.get(slug='hello')
    print(article)
# => Article(title="こんにちは")

get() は、対象のレコードが複数件見つかったときには MultipleObjectsFoundError という例外をあげます。

複数件マッチしうるクエリで最初の要素のみ取得したい場合は次の first() を使用します。

条件に合致する最初のレコードを取得する:

with orm.db_session:
    article = orm.select(a for a in Article if 'kkk' in a.slug).first()
    print(article)
# => Article(title="こんにちは")

first() は該当するレコードが 0 件だったときは None を返します。

ORDER BY 句を使う:

with orm.db_session:
    for article in orm.select(a for a in Article).order_by(Article.slug):
        print(article)
# => Article(title="さようなら")
# => Article(title="こんにちは")

WHERE 句を使う:

with orm.db_session:
    for article in orm.select(a for a in Article if a.title.startswith('さ')):
        print(article)
# => Article(title="さようなら")
with orm.db_session:
    for article in orm.select(a for a in Article).where(lambda a: a.title.startswith('さ')):
        print(article)
# => Article(title="さようなら")

WHERE 句はジェネレータ式の ifでも where() メソッドでも書くことができます( filter() というまた別のメソッドも用意されています)。

発行されるクエリを確認する:

with orm.db_session:
    print(orm.select(a for a in Article).order_by(Article.slug).get_sql())
# => SELECT "a"."id", "a"."slug", "a"."title", "a"."body", "a"."data"
# => FROM "Article" "a"
# => ORDER BY "a"."slug"

発行されるクエリを確認したいときは get_sql() メソッドが使えます。

レコードを操作する

データベーステーブルの各レコードに相当するインスタンスにも便利なメソッドが用意されています。

primary key を取得する:

article.get_pk()
# => 2

複数のフィールドの値をまとめてセットする:

article.set(body='sayonara sayonara', data={'message': 'onara'})

dict に変換する:

article.to_dict()
# => {'id': 2, 'slug': 'good-bye', 'title': 'さようなら', 'body': 'sayonara sayonara', 'data': {'message': 'onara'}}

これらの他にも SQL と Python にある程度馴染みがあれば直感的に使える機能が多数用意されています。

この記事を書いたときに確認したかぎりでは API は 1 ページにまとまっているので、「こんなもの無いかな」と思ったときにも探しやすいと思います。

ということで、 Python のライブラリ Pony ORM の紹介でした。実際に利用してみたいと思う方は公式のドキュメント・リポジトリを見てみてください。

参考: