Конфигурация ORM¶
Как отобразить таблицу, у которой нет первичного ключа?¶
SQLAlchemy ORM для сопоставления с конкретной таблицей требует, чтобы в ней был хотя бы один столбец, обозначенный как столбец первичного ключа; конечно, вполне возможны и многостолбцовые, т.е. составные, первичные ключи. Эти столбцы не должны быть известны базе данных как столбцы первичного ключа, хотя хорошо бы, чтобы они были известны. Необходимо только, чтобы столбцы повели себя как первичный ключ, например, как уникальный и не обнуляемый идентификатор для строки.
Большинство ORM требуют, чтобы объекты имели определенный первичный ключ, поскольку объект в памяти должен соответствовать однозначно идентифицируемой строке в таблице базы данных; по крайней мере, это позволяет объекту быть объектом для UPDATE и DELETE операторов, которые будут влиять только на строку этого объекта и ни на какую другую. Однако важность первичного ключа выходит далеко за рамки этого. В SQLAlchemy все ORM-сопоставленные объекты всегда однозначно связаны в рамках Session
с конкретной строкой базы данных с помощью шаблона identity map, который является центральным для системы единиц работы, используемой в SQLAlchemy, а также ключевым для наиболее распространенных (и не очень) моделей использования ORM.
Примечание
Важно отметить, что мы говорим только о SQLAlchemy ORM; приложение, построенное на Core и имеющее дело только с объектами Table
, конструкциями select()
и т.п., **не нуждается в наличии первичного ключа в таблице или каким-либо образом связанного с ней (хотя, опять же, в SQL все таблицы действительно должны иметь какой-то первичный ключ, чтобы вам не нужно было обновлять или удалять определенные строки).
Почти во всех случаях таблица действительно имеет так называемый candidate key, который представляет собой столбец или серию столбцов, однозначно идентифицирующих строку. Если таблица действительно не имеет такого ключа и имеет фактические полностью дублирующиеся строки, таблица не соответствует 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)
Все таблицы в реляционной базе данных должны иметь первичные ключи. Даже в таблице ассоциаций «многие-ко-многим» первичным ключом будет композит из двух столбцов ассоциации:
CREATE TABLE my_association (
user_id INTEGER REFERENCES user(id),
account_id INTEGER REFERENCES account(id),
PRIMARY KEY (user_id, account_id)
)
Как настроить столбец, который является зарезервированным словом Python или подобным ему?¶
Атрибутам на основе столбцов можно дать любое имя, желаемое в связке. См. Именование столбцов отличается от имен атрибутов.
Как получить список всех столбцов, отношений, сопоставленных атрибутов и т.д. для сопоставленного класса?¶
Вся эта информация доступна из объекта 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
в случае отображения с помощью наследования на составленный selectable.
Я получаю предупреждение или ошибку «Неявное объединение столбца X под атрибутом Y».¶
Это условие относится к случаю, когда отображение содержит два столбца, которые отображаются под одним и тем же именем атрибута из-за их имени, но нет никаких признаков того, что это сделано намеренно. Сопоставленный класс должен иметь явные имена для каждого атрибута, который должен хранить независимое значение; когда два столбца имеют одинаковые имена и не разграничены, они попадают под один и тот же атрибут, и в результате значение из одного столбца копируется в другой, на основе того, какой столбец был назначен атрибуту первым.
Такое поведение часто желательно и допускается без предупреждения в случае, когда два столбца связаны вместе через отношение внешнего ключа в связке наследования. Когда возникает предупреждение или исключение, проблему можно решить, либо назначив столбцы атрибутам с разными именами, либо, если необходимо объединить их вместе, используя 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, вышеописанное условие обнаруживается и предупреждает, что колонки id
в A
и B
объединяются под одноименным атрибутом 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"))
Предположим, мы хотим, чтобы A.id
и B.id
были зеркальным отражением друг друга, несмотря на то, что B.a_id
находится там, где находится A.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"))
Я использую Declarative и устанавливаю primaryjoin/secondaryjoin с помощью and_()
или or_()
, и получаю сообщение об ошибке, связанной с внешними ключами.¶
Ты это делаешь?
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
)
Это and_()
из двух строковых выражений, к которым SQLAlchemy не может применить никакого отображения. Декларативность позволяет указывать аргументы relationship()
в виде строк, которые преобразуются в объекты выражений с помощью eval()
. Но это не происходит внутри выражения and_()
- это специальная операция, которую declarative применяет только к целому тому, что передается в 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])
Почему ORDER BY
рекомендуется использовать с LIMIT
(особенно с subqueryload()
)?¶
Когда ORDER BY не используется для оператора SELECT, возвращающего строки, реляционная база данных может возвращать сопоставленные строки в любом произвольном порядке. Хотя такой порядок очень часто соответствует естественному порядку строк в таблице, это не так для всех баз данных и всех запросов. Следствием этого является то, что любой запрос, ограничивающий строки с помощью LIMIT
или OFFSET
, или просто выбирающий первую строку результата, отбрасывая остальные, не будет детерминированным с точки зрения того, какая строка результата будет возвращена, при условии, что существует более одной строки, соответствующей критериям запроса.
Хотя мы можем не заметить этого при простых запросах к базам данных, которые обычно возвращают строки в их естественном порядке, это становится более серьезной проблемой, если мы также используем subqueryload()
для загрузки связанных коллекций, и мы можем загрузить коллекции не так, как предполагалось.
SQLAlchemy реализует subqueryload()
путем выдачи отдельного запроса, результаты которого сопоставляются с результатами первого запроса. Мы видим, что два запроса выдаются следующим образом:
>>> session.query(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
Второй запрос встраивает первый запрос в качестве источника строк. Если внутренний запрос использует OFFSET
и/или LIMIT
без упорядочивания, два запроса могут получить разные результаты:
>>> user = session.query(User).options(subqueryload(User.addresses)).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
В зависимости от особенностей базы данных, есть вероятность, что мы получим результат, подобный следующему для двух запросов:
-- query #1
+--------+
|users_id|
+--------+
| 1|
+--------+
-- query #2
+------------+-----------------+---------------+
|addresses_id|addresses_user_id|anon_1_users_id|
+------------+-----------------+---------------+
| 3| 2| 2|
+------------+-----------------+---------------+
| 4| 2| 2|
+------------+-----------------+---------------+
Выше мы получили два ряда addresses
для user.id
из 2, и ни одного для 1. Мы потратили впустую две строки и не смогли загрузить коллекцию. Это коварная ошибка, потому что без просмотра SQL и результатов, ORM не покажет, что есть какая-то проблема; если мы обратимся к addresses
для User
, которые у нас есть, он выдаст ленивую загрузку для коллекции, и мы не увидим, что что-то пошло не так.
Решение этой проблемы заключается в том, чтобы всегда указывать детерминированный порядок сортировки, чтобы основной запрос всегда возвращал один и тот же набор строк. Обычно это означает, что вы должны Query.order_by()
на уникальном столбце таблицы. Первичный ключ является хорошим выбором для этого:
session.query(User).options(subqueryload(User.addresses)).order_by(User.id).first()
Обратите внимание, что стратегия joinedload()
eager loader не страдает от той же проблемы, поскольку когда-либо выдается только один запрос, поэтому запрос загрузки не может отличаться от основного запроса. Аналогично, стратегия selectinload()
eager loader также не имеет этой проблемы, поскольку она связывает загрузку коллекции непосредственно со значениями первичного ключа, которые только что были загружены.
См.также