Нетрадиционные отображения¶
Сопоставление класса с несколькими таблицами¶
Сопоставители могут быть построены для произвольных реляционных единиц (называемых 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
приравниваются по внешнему ключу, поэтому в отображении они определяются как один атрибут AddressUser.id
, используя column_property()
для указания специализированного отображения столбцов. Основываясь на этой части конфигурации, отображение будет копировать новые значения первичного ключа из user.id
в столбец address.user_id
, когда произойдет смывка.
Кроме того, столбец address.id
явно сопоставлен с атрибутом address_id
. Это делается для разъяснения отображения столбца address.id
от одноименного атрибута AddressUser.id
, который здесь назначен для ссылки на таблицу user
в сочетании с внешним ключом address.user_id
.
Естественным первичным ключом приведенного выше отображения является композит (user.id, address.id)
, так как это столбцы первичного ключа таблиц user
и address
, объединенные вместе. Идентичность объекта AddressUser
будет в терминах этих двух значений и представляется от объекта AddressUser
как (AddressUser.id, AddressUser.address_id)
.
При обращении к столбцу AddressUser.id
в большинстве SQL-выражений будет использоваться только первый столбец в списке сопоставленных столбцов, поскольку эти два столбца являются синонимами. Однако в особых случаях, например, в выражении GROUP BY, когда необходимо одновременно ссылаться на оба столбца, используя при этом правильный контекст, то есть учитывая псевдонимы и т.п., можно использовать аксессор Comparator.expressions
:
stmt = select(AddressUser).group_by(*AddressUser.id.expressions)
Добавлено в версии 1.3.17: Добавлен аксессор Comparator.expressions
.
Примечание
Сопоставление с несколькими таблицами, как показано выше, поддерживает постоянство, то есть INSERT, UPDATE и DELETE строк в целевых таблицах. Однако оно не поддерживает операцию, которая бы обновляла одну таблицу и выполняла INSERT или DELETE в других одновременно для одной записи. То есть, если запись PtoQ сопоставлена с таблицами «p» и «q», где она имеет строку, основанную на LEFT OUTER JOIN из «p» и «q», то при выполнении операции UPDATE, которая должна изменить данные в таблице «q» в существующей записи, строка в «q» должна существовать; она не выдаст INSERT, если идентификатор первичного ключа уже присутствует. Если строка не существует, то для большинства драйверов DBAPI, поддерживающих отчет о количестве строк, затронутых UPDATE, 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})
где выше, строка вставляется в таблицу q_table
путем создания конструкции INSERT с помощью Table.insert()
, затем выполняется с помощью данной Connection
, которая используется для выдачи других SQL для процесса промывки. Пользовательская логика должна будет определить, что LEFT OUTER JOIN от «p» к «q» не имеет записи для стороны «q».
Сопоставление класса с произвольными подзапросами¶
Подобно отображению на join, обычный объект select()
также может быть использован с отображающим устройством. Приведенный ниже фрагмент примера иллюстрирует отображение класса Customer
на select()
, который включает присоединение к подзапросу:
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
с этим selectable затем создает класс, который будет содержать эти атрибуты.
Когда ORM сохраняет новые экземпляры Customer
, только таблица customers
будет получать INSERT. Это происходит потому, что первичный ключ таблицы orders
не представлен в отображении; ORM будет выдавать INSERT только в таблицу, для которой он отобразил первичный ключ.
Примечание
Практика отображения на произвольные операторы SELECT, особенно сложные, как описано выше, почти никогда не нужна; она неизбежно приводит к созданию сложных запросов, которые часто менее эффективны, чем те, которые были бы получены при прямом построении запросов. Эта практика в какой-то степени основана на ранней истории SQLAlchemy, где конструкция Mapper
была предназначена для представления основного интерфейса запросов; в современном использовании объект Query
может быть использован для построения практически любого оператора SELECT, включая сложные композиты, и ему следует отдавать предпочтение перед подходом «map-to-selectable».
Несколько картографов для одного класса¶
В современной SQLAlchemy конкретный класс одновременно отображается только одним так называемым основным отобразителем. Этот отобразитель участвует в трех основных областях функциональности: запрос, сохранение и инструментация отображаемого класса. Обоснование первичного маппера связано с тем, что Mapper
модифицирует сам класс, не только персистируя его к определенному Table
, но и instrumenting атрибуты класса, которые структурированы специально в соответствии с метаданными таблицы. Невозможно, чтобы с классом было связано более одного маппера в равной степени, так как только один маппер может реально инструментировать класс.
Концепция «не основного» сопоставителя существовала во многих версиях SQLAlchemy, однако начиная с версии 1.3 эта функция устарела. Единственный случай, когда такой не основной сопоставитель полезен, это при построении отношения к классу по альтернативному selectable. Для этого случая теперь используется конструкция aliased
, которая описана в Взаимосвязь с классом Aliased.
Что касается случая использования класса, который может быть полностью сохранен в разных таблицах по разным сценариям, то в самых ранних версиях SQLAlchemy для этого предлагалась функция, адаптированная из Hibernate, известная как «имя сущности». Однако этот вариант использования стал невыполнимым в SQLAlchemy, когда сам сопоставленный класс стал источником построения SQL-выражений; то есть атрибуты класса напрямую связываются с колонками сопоставленной таблицы. Эта возможность была удалена и заменена простым рецептом, ориентированным на выполнение этой задачи без каких-либо двусмысленностей инструментария - создание новых подклассов, каждый из которых отображается индивидуально. Этот шаблон теперь доступен в виде рецепта по адресу Entity Name.