Non-Traditional Mappings

Mapping a Class against Multiple Tables

マッパーは、単純なテーブルに加えて、任意のリレーショナルユニット( selectables と呼ばれます)に対して構築することができます。例えば、 join() ファンクションは、複数のテーブルで構成される選択可能なユニットを作成します。このユニットは、独自の複合主キーを持ち、 Table と同じ方法でマッピングすることができます:

from sqlalchemy import Table, Column, Integer, String, MetaData, join, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import column_property

metadata_obj = MetaData()

# define two Table objects
user_table = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String),
)

address_table = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String),
)

# define a join between them.  This
# takes place across the user.id and address.user_id
# columns.
user_address_join = join(user_table, address_table)

class Base(DeclarativeBase):
    metadata = metadata_obj

# map to it
class AddressUser(Base):
    __table__ = user_address_join

    id = column_property(user_table.c.id, address_table.c.user_id)
    address_id = address_table.c.id

上の例では、この結合は user テーブルと address テーブルの両方の列を表しています。 user.id 列と address.user_id 列は外部キーによって同一視されるので、マッピングでは1つの属性 AddressUser.id として定義され、 column_property() を使って特殊な列マッピングを示します。設定のこの部分に基づいて、フラッシュが発生すると、マッピングは新しい主キーの値を user.id 列から address.user_id 列にコピーします。

さらに、 address.id 列は address_id という名前の属性に明示的にマップされます。これは、同じ名前の AddressUser.id 属性からの address.id 列のマッピングを 明確にする ためです。ここでは、 address.user_id 外部キーと組み合わされた user テーブルを参照するように割り当てられています。

上記のマッピングの本来の主キーは、 user テーブルと address テーブルの主キー列を組み合わせた (user.id, address.id) の合成です。 AddressUser オブジェクトのIDはこの2つの値で表され、 AddressUser オブジェクトからは (AddressUser.id, AddressUser.address_id) として表されます。

AddressUser.id 列を参照する場合、ほとんどのSQL式は、マップされた列のリストの最初の列のみを使用します。これは、2つの列が同義であるためです。しかし、GROUP BY式のように、適切なコンテキストを使用しながら、つまりエイリアスなどに対応しながら、両方の列を同時に参照する必要がある特殊なユースケースでは、アクセッサ Comparator.expressions を使用できます:

stmt = select(AddressUser).group_by(*AddressUser.id.expressions)

New in version 1.3.17: Added the Comparator.expressions accessor.

Note

上記のような複数のテーブルに対するマッピングでは、対象となるテーブル内のローのINSERT、UPDATE、DELETEなどのパーシスタンスがサポートされています。ただし、1つのテーブルに対してUPDATEを実行し、同時に1つのレコードに対して他のテーブルに対してINSERTまたはDELETEを実行する操作はサポートされていません。つまり、レコードPtoQがテーブル”p”と”q”にマップされていて、”p”と”q”のLEFT OUTER JOINに基づくローがある場合、既存のレコードの”q”テーブル内のデータを変更するUPDATEを実行するには、”q”内のローが存在する必要があります。プライマリ・キーIDがすでに存在する場合、INSERTは発行されません。ローが存在しない場合、UPDATEによって影響を受けるローの数のレポートをサポートするほとんどのDBAPIドライバでは、ORMは更新されたローを検出できず、エラーが発生します。存在しない場合、データは黙って無視されます。

関連する行のオンザフライでの”挿入”を可能にするレシピでは、.MapperEvents.before_updateイベントを使用し、次のようにします。:

from sqlalchemy import event

@event.listens_for(PtoQ, "before_update")
def receive_before_update(mapper, connection, target):
    if target.some_required_attr_on_q is None:
        connection.execute(q_table.insert(), {"id": target.id})

上の例では、 Table.insert() でINSERT構文を作成し、フラッシュプロセスの他のSQLを発行するために使用されているものと同じ Connection を使用して実行することで、行が q_table テーブルに挿入されます。ユーザ指定のロジックは、”p”から”q”へのLEFT OUTER JOINに”q”側のエントリがないことを検出する必要があります。

Mapping a Class against Arbitrary Subqueries

結合に対するマッピングと同じように、普通の select() オブジェクトもマッパーと一緒に使うことができます。以下のサンプルコードは、サブクエリへの結合を含む select() に対して、 Customer というクラスをマッピングする方法を示しています。:

from sqlalchemy import select, func

subq = (
    select(
        func.count(orders.c.id).label("order_count"),
        func.max(orders.c.price).label("highest_order"),
        orders.c.customer_id,
    )
    .group_by(orders.c.customer_id)
    .subquery()
)

customer_select = (
    select(customers, subq)
    .join_from(customers, subq, customers.c.id == subq.c.customer_id)
    .subquery()
)

class Customer(Base):
    __table__ = customer_select

上の例では、 customer_select で表される行全体が customers テーブルの全ての列となり、さらに subq サブクエリで公開される order_counthighest_ordercustomer_id の列が追加されます。 Customer クラスをこの選択可能なクラスにマップすると、これらの属性を含むクラスが作成されます。

ORMが Customer の新しいインスタンスを保持する場合、実際にINSERTを受け取るのは customers テーブルだけです。これは、 orders テーブルの主キーがマッピングで表現されていないためです。ORMは、主キーをマッピングしたテーブルに対してのみINSERTを発行します。

Note

任意のSELECT文、特に上記のような複雑なSELECT文へのマッピングは、ほとんど必要ありません。これは必然的に複雑なクエリを生成する傾向があり、直接クエリを構築した場合よりも効率が悪くなることがよくあります。この手法は、SQLAlchemyの非常に初期の歴史にある程度基づいています。SQLAlchemyでは、 Mapper 構文は主要なクエリインターフェースを表すことを意図していました。現在の使用法では、 Query オブジェクトは、複雑な合成を含む事実上すべてのSELECT文を構築するために使用でき、「選択可能にマップする」アプローチよりも優先されるべきです。

Multiple Mappers for One Class

最近のSQLAlchemyでは、特定のクラスは一度に1つのいわゆる プライマリ マッパーによってマップされます。このマッパーは、マップされたクラスのクエリ、パーシステンス、インストルメンテーションという3つの主要な機能領域に関与します。プライマリマッパーの理論的根拠は、 Mapper がクラス自体を変更し、特定の Table に対してクラスを永続化するだけでなく、テーブルメタデータに従って具体的に構造化されたクラスの instrumenting 属性も変更するという事実に関係しています。実際にクラスをインストルメンテーションできるマッパーは1つだけなので、1つのクラスに複数のマッパーを均等に関連付けることはできません。

“非プライマリ”マッパーの概念はSQLAlchemyの多くのバージョンに存在していましたが、バージョン1.3ではこの機能は廃止されました。このような非プライマリマッパーが有用なケースの1つは、選択可能な選択肢に対してクラスとの関係を構築する場合です。このユースケースは aliased 構文を使用するのに適しており、 Relationship to Aliased Class で説明されています。

異なるシナリオで異なるテーブルに実際に完全に永続化できるクラスのユースケースに関しては、SQLAlchemyの非常に初期のバージョンでは、Hibernateから採用された「エンティティ名」機能として知られる機能が提供されていました。しかし、マッピングされたクラス自体がSQL式構築のソースになると、このユースケースはSQLAlchemy内で実行不可能になりました。つまり、クラスの属性自体がマッピングされたテーブル列に直接リンクされます。この機能は削除され、インストルメンテーションの曖昧さなしにこのタスクを達成するための単純なレシピ指向のアプローチに置き換えられました。つまり、それぞれが個別にマッピングされた新しいサブクラスを作成します。このパターンは現在、 Entity Name でレシピとして利用できます。