Нетрадиционные отображения

Сопоставление класса с несколькими таблицами

Сопоставители могут быть построены для произвольных реляционных единиц (называемых 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.

Вернуться на верх