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_count
、 highest_order
、 customer_id
の列が追加されます。 Customer
クラスをこの選択可能なクラスにマップすると、これらの属性を含むクラスが作成されます。
ORMが Customer
の新しいインスタンスを保持する場合、実際にINSERTを受け取るのは customers
テーブルだけです。これは、 orders
テーブルの主キーがマッピングで表現されていないためです。ORMは、主キーをマッピングしたテーブルに対してのみINSERTを発行します。
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 でレシピとして利用できます。