ORM Configuration¶
How do I map a table that has no primary key?¶
SQLAlchemy ORMは、特定のテーブルにマップするために、少なくとも1つの列が主キー列として指定されている必要があります。複数列、つまり複合主キーももちろん完全に実現可能です。これらの列は、実際にデータベースに主キー列として認識されている必要は ありません が、認識されていることは良い考えです。列が主キーと同じように 動作する ことだけが必要です。たとえば、行に対して一意でNULLを使用できない識別子として動作することが必要です。
ほとんどのORMでは、メモリ内のオブジェクトはデータベーステーブル内の一意に識別可能な行に対応する必要があるため、オブジェクトにはある種の主キーが定義されている必要があります。少なくとも、これにより、そのオブジェクトの行だけに影響し、他の行には影響しないUPDATE文とDELETE文のターゲットにすることができます。しかし、主キーの重要性はそれをはるかに超えています。SQLAlchemyでは、ORMにマップされたすべてのオブジェクトは、 identity map と呼ばれるパターンを使用して、 Session 内で特定のデータベース行に常に一意にリンクされます。このパターンは、SQLAlchemyで使用される作業システムの単位の中心であり、ORM使用の最も一般的な(そしてあまり一般的ではない)パターンの鍵でもあります。
Note
ここで重要なのは、ここではSQLAlchemy ORMについて話しているだけだということです。SQLAlchemy ORMはCore上に構築され、Table オブジェクトや select() 構文などだけを扱うアプリケーションですが、主キーが存在したり、何らかの形でテーブルに関連付けられたりする必要は ありません (ただし、SQLでは、実際に特定の行を更新したり削除したりする必要がないように、すべてのテーブルは実際に何らかの種類の主キーを持つ必要があります)。
ほとんどの場合、テーブルには candidate key と呼ばれるものがあります。これは、1つの候補キーを一意に識別する1つまたは一連の列です。テーブルに本当にこれがなく、実際に完全に重複した行がある場合、そのテーブルは first normal form に対応していないため、マッピングできません。そうでなければ、最適な候補キーを構成する列をマッパーに直接適用することができます:
class SomeClass(Base):
__table__ = some_table_with_no_pk
__mapper_args__ = {
"primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
}さらに良いのは、完全に宣言されたテーブルメタデータを使用する場合、それらの列に対して「primary_key=True」フラグを使用することです。:
class SomeClass(Base):
__tablename__ = "some_table_with_no_pk"
uid = Column(Integer, primary_key=True)
bar = Column(String, primary_key=True)リレーショナル・データベース内のすべてのテーブルには、主キーが必要です。多対多の関連付けテーブルであっても、主キーは次の2つの関連付け列の複合です。:
CREATE TABLE my_association (
user_id INTEGER REFERENCES user(id),
account_id INTEGER REFERENCES account(id),
PRIMARY KEY (user_id, account_id)
)How do I configure a Column that is a Python reserved word or similar?¶
列ベースの属性には、マッピングで必要な任意の名前を付けることができます。 Naming Declarative Mapped Columns Explicitly を参照してください。
How do I get a list of all columns, relationships, mapped attributes, etc. given a mapped class?¶
この情報はすべて Mapper オブジェクトから取得できます。
特定のマップされたクラスの Mapper を取得するには、そのクラスに対して inspect() 関数を呼び出します:
from sqlalchemy import inspect
mapper = inspect(MyClass)ここから、次のようなプロパティを使用して、クラスに関するすべての情報にアクセスできます。:
Mapper.attrs- マップされたすべての属性の名前空間です。属性自体はMapperPropertyのインスタンスで、該当する場合、マップされ たSQL式や列につながる追加の属性が含まれています。
Mapper.column_attrs- 列とSQL式の属性に限定された、マップされた属性の名前空間です。Mapper.columnsを使用してColumnオブジェクトを直接取得することもできます。
Mapper.relationships- すべてのRelationshipProperty属性の名前空間です。
Mapper.all_orm_descriptors- マップされたすべての属性の名前空間と、hybrid_property、AssociationProxyなどのシステムを使用して定義されたユーザ定義の属性。
Mapper.columns-Columnオブジェクトの名前空間と、マッピングに関連付けられた他の名前付きSQL式です。
Mapper.mapped_table- このマッパーがマップされているTableまたは他の選択可能なもの。
Mapper.local_table- このマッパーにとって”ローカル”なTableです。これはMapper.mapped_tableとは、継承を使ってcomposed selectableにマップされたマッパーの場合は異なります。
I’m getting a warning or error about “Implicitly combining column X under attribute Y”¶
この条件は、名前のために同じ属性名でマップされている2つの列がマッピングに含まれているが、これが意図的であることを示すものではない場合を指します。マップされたクラスは、独立した値を格納するすべての属性に対して明示的な名前を持つ必要があります。2つの列が同じ名前を持ち、明確にされていない場合、それらは同じ属性に分類され、その結果、最初に属性に割り当てられた列に基づいて、一方の列の値が他方の列に コピー されます。
この動作は多くの場合望ましいものであり、2つの列が継承マッピング内の外部キー関係を介してリンクされている場合に警告なしに許可されます。警告または例外が発生した場合、この問題は、列を異なる名前の属性に割り当てるか、それらを組み合わせる場合は column_property() を使用してこれを明示的に行うことで解決できます。
次に例を示します。:
from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("a.id"))SQLAlchemyバージョン0.9.5では、上記の条件が検出され、 A と B の id 列が同じ名前の属性 id の下に結合されていることが警告されます。これは、 B オブジェクトの主キーが常に A オブジェクトの主キーを反映することを意味するため、深刻な問題です。
これを解決するマッピングは次のようになります。:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
b_id = Column("id", Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("a.id"))B.a_id が A.id と関連しているにもかかわらず、 A.id と B.id を互いの鏡にしたいと思ったとします。 column_property() を使ってこれらを組み合わせることができます:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
# probably not what you want, but this is a demonstration
id = column_property(Column(Integer, primary_key=True), A.id)
a_id = Column(Integer, ForeignKey("a.id"))I’m using Declarative and setting primaryjoin/secondaryjoin using an and_() or or_(), and I am getting an error message about foreign keys.¶
このようにしていますか?:
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
)これは2つの文字列式の and_() であり、SQLAlchemyはこれに対してマッピングを適用することができません。宣言型では、 relationship() 引数を文字列として指定できます。これは eval() を使って式オブジェクトに変換されます。しかし、これは and_() 式の中では起こりません。宣言型の特殊な操作であり、primaryjoinや他の引数に文字列として渡されたものの*全体*にのみ適用されます:
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
)または、必要なオブジェクトが既に使用可能な場合は、文字列をスキップします。:
class MyClass(Base):
# ....
foo = relationship(
Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
)同じ考え方が、 foreign_keys のような他の引数にも当てはまります:
# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])
# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")
# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])
# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
foo_id = Column(...)
bar_id = Column(...)
# ...
foo = relationship(Dest, foreign_keys=[foo_id, bar_id])Why is ORDER BY recommended with LIMIT (especially with subqueryload())?¶
行を返すSELECT文にORDER BYを使用しない場合、リレーショナル・データベースは一致した行を任意の順序で返すことができます。この順序は、テーブル内の行の自然な順序に対応することがよくありますが、すべてのデータベースとすべてのクエリに当てはまるわけではありません。この結果、 LIMIT や OFFSET を使用して行を制限するクエリや、単に結果の最初の行を選択して残りを破棄するクエリは、クエリの条件に一致する行が複数あると仮定すると、どの結果行が返されるかを決定できません。
通常は自然な順序で行を返すデータベースに対する単純な問い合わせでは、これに気付かないかもしれませんが、関連するコレクションをロードするために parcelsload() も使用しており、意図したとおりにコレクションをロードしていない可能性がある場合には、これはより大きな問題になります。
SQLAlchemyは別の問い合わせを発行して degenerateload() を実装しており、その結果は最初の問い合わせの結果と一致します。以下のように2つの問い合わせが発行されています。:
>>> session.scalars(select(User).options(subqueryload(User.addresses))).all()
-- the "main" query
SELECT users.id AS users_id
FROM users
-- the "load" query issued by subqueryload
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id FROM users) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
2番目のクエリは、最初のクエリを行のソースとして埋め込みます。内側のクエリが順序付けなしで OFFSET や LIMIT を使用すると、2つのクエリで同じ結果が得られない場合があります。:
>>> user = session.scalars(
... select(User).options(subqueryload(User.addresses)).limit(1)
... ).first()
-- the "main" query
SELECT users.id AS users_id
FROM users
LIMIT 1
-- the "load" query issued by subqueryload
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id FROM users LIMIT 1) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
データベースの仕様によっては、2つのクエリに対して次のような結果が得られる可能性があります。:
-- query #1
+--------+
|users_id|
+--------+
| 1|
+--------+
-- query #2
+------------+-----------------+---------------+
|addresses_id|addresses_user_id|anon_1_users_id|
+------------+-----------------+---------------+
| 3| 2| 2|
+------------+-----------------+---------------+
| 4| 2| 2|
+------------+-----------------+---------------+上記では、2の user.id に対して2つの addresses 行を受け取り、1に対しては何も受け取りません。2行を無駄にして、実際にコレクションをロードできませんでした。これは潜行的なエラーです。なぜなら、SQLと結果を見なければ、ORMは何か問題があることを示さないからです。もし私たちが持っている User の addresses にアクセスすると、コレクションに対して遅延ロードが発生し、実際に何か問題があったことはわかりません。
この問題を解決するには、常に確定的なソート順を指定して、メインクエリが常に同じ行セットを返すようにします。これは一般的に、テーブルの一意の列に対して Select.order_by() を指定する必要があることを意味します。これには主キーを選択するとよいでしょう:
session.scalars(
select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()joinedload() eager loader戦略は同じ問題に悩まされないことに注意してください。なぜなら、発行されるクエリは1つだけなので、ロードクエリはメインクエリと異なることはできません。同様に、 selectinload() eager loader戦略も、コレクションのロードをロードされたばかりの主キー値に直接リンクしているので、この問題はありません。
See also
What are default, default_factory and insert_default and what should I use?¶
PEP-681データクラス変換が追加されたことにより、SQLAlchemyのAPIには多少の衝突があります。これは、その命名規則に厳密です。 Declarative Dataclass Mapping に示されているように、 MappedAsDataclass を使用している場合、PEP-681が機能します。MappedAsDataclassを使用していない場合、これは適用されません。
Part One - Classic SQLAlchemy that is not using dataclasses¶
MappedAsDataclass を使って いない 場合、SQLAlchemyで長年使われてきたように、 mapped_column() (および Column )構文はパラメータ mapped_column.default をサポートします。これはPython側のデフォルト(データベースのスキーマ定義の一部となるサーバ側のデフォルトとは対照的に)を示し、 INSERT 文が発行されたときに発生します。このデフォルトは、文字列のような 任意の 静的Python値、 または Python呼び出し可能関数、 または SQLAlchemy SQL構文です。 mapped_column.default の完全なドキュメントは Client-Invoked SQL Expressions にあります。
MappedAsDataclass を 使用しない ORMマッピングで mapped_column.default を使用する場合、このデフォルト値/呼び出し可能な値は、 オブジェクトを最初に構築したときには表示されません 。これは、SQLAlchemyがオブジェクトに対して INSERT 文を作成したときにのみ行われます。
注意すべき非常に重要なことは、 mapped_column() (および Column )を使用する場合、古典的な mapped_column.default
パラメータは mapped_column.insert_default という新しい名前でも利用できます。 mapped_column() を構築していて、 MappedAsDataclass を 使用していない 場合、 mapped_column.default パラメータと mapped_column.insert_default パラメータは 同義 です。
Part Two - Using Dataclasses support with MappedAsDataclass¶
MappedAsDataclass 、つまり Declarative Dataclass Mapping で使用される特定の形式のマッピングを 使用している 場合、 mapped_column.default キーワードの意味が変わります。この名前が動作を変えるのは理想的ではないことは認識していますが、PEP-681では mapped_column.default がこの意味を持つことを要求しているので、他に選択肢はありませんでした。
データクラスを使用する場合、 mapped_column.default パラメータは Python Dataclasses に記述されている方法で使用する必要があります。これは文字列や数値のような定数値を参照し、 構築されるとすぐにオブジェクトに適用されます 。また、この時点では、 Column の mapped_column.default パラメータにも適用されます。この場合、オブジェクトに存在しなくても自動的に INSERT 文で使用されます。構築時にオブジェクトに適用されるデータクラスの呼び出し可能オブジェクトを使用する場合は、 mapped_column.default_factory を使用します。
上記のパート1で説明した mapped_column.default の INSERT のみの動作にアクセスするには、代わりに mapped_column.insert_default パラメータを使用します。データクラスが使用される場合、 mapped_column.insert_default は、パラメータが静的な値または呼び出し可能なコアレベルの”デフォルト”プロセスへの直接ルートであり続けます。
Construct |
Works with dataclasses? |
Works without dataclasses? |
Accepts scalar? |
Accepts callable? |
Populates object immediately? |
|---|---|---|---|---|---|
✔ |
✔ |
✔ |
Only if no dataclasses |
Only if dataclasses |
|
✔ |
✔ |
✔ |
✔ |
✖ |
|
✔ |
✖ |
✖ |
✔ |
Only if dataclasses |