Конфигурация 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-выражений. Для непосредственного доступа к объектамColumn
может потребоваться использованиеMapper.columns
.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()
)?¶
Когда в операторе SELECT, возвращающем строки, не используется ORDER BY, реляционная база данных может возвращать найденные строки в любом произвольном порядке. Хотя такой порядок очень часто соответствует естественному порядку следования строк в таблице, это не так для всех баз данных и не для всех запросов. Следствием этого является то, что любой запрос, ограничивающий строки с помощью LIMIT
или OFFSET
, или просто выбирающий первую строку результата, отбрасывая остальные, не будет детерминированным с точки зрения того, какая строка результата будет возвращена, при условии, что существует более одной строки, соответствующей критериям запроса.
Хотя мы можем не заметить этого при простых запросах к базам данных, которые обычно возвращают строки в их естественном порядке, это становится более серьезной проблемой, если мы также используем subqueryload()
для загрузки связанных коллекций, и мы можем загрузить коллекции не так, как предполагалось.
SQLAlchemy реализует subqueryload()
путем выдачи отдельного запроса, результаты которого сопоставляются с результатами первого запроса. Мы видим, что два запроса выдаются следующим образом:
>>> session.scalars(select(User).options(subqueryload(User.addresses))).all()
{execsql}-- the "main" query
SELECT users.id AS users_id
FROM users
{stop}
{execsql}-- 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.scalars(
... select(User).options(subqueryload(User.addresses)).limit(1)
... ).first()
{execsql}-- the "main" query
SELECT users.id AS users_id
FROM users
LIMIT 1
{stop}
{execsql}-- 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
, то будет выдана ленивая загрузка коллекции, и мы не увидим, что что-то пошло не так.
Решение этой проблемы заключается в том, чтобы всегда задавать детерминированный порядок сортировки, чтобы основной запрос всегда возвращал один и тот же набор строк. Обычно это означает, что необходимо Select.order_by()
на уникальном столбце таблицы. Для этого хорошо подходит первичный ключ:
session.scalars(
select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()
Заметим, что стратегия joinedload()
eager loader не страдает от этой проблемы, поскольку в ней выполняется только один запрос, поэтому запрос на загрузку не может отличаться от основного запроса. Аналогичным образом, стратегия selectinload()
с нетерпеливым загрузчиком также не имеет этой проблемы, поскольку она связывает загрузку коллекции непосредственно со значениями первичного ключа, которые только что были загружены.
См.также