Обзор сопоставленных классов ORM

Обзор конфигурации отображения классов ORM.

Читателям, впервые знакомящимся с SQLAlchemy ORM и/или с Python в целом, рекомендуется просмотреть Быстрый старт ORM и предпочтительно проработать Самоучитель SQLAlchemy 1.4 / 2.0, где конфигурация ORM впервые представлена в Определение метаданных таблицы с помощью ORM.

Стили отображения ORM

SQLAlchemy имеет два различных стиля конфигурации мапперов, которые затем имеют дополнительные подварианты для их настройки. Вариативность стилей маппера присутствует для удовлетворения разнообразного списка предпочтений разработчиков, включая степень абстракции пользовательского класса от того, как он должен быть сопоставлен с таблицами и столбцами реляционной схемы, какие виды иерархий классов используются, включая наличие или отсутствие пользовательских схем метаклассов, и, наконец, присутствуют ли другие подходы к инструментации классов, например, если одновременно используются Python dataclasses.

В современной SQLAlchemy разница между этими стилями в основном поверхностная; когда определенный конфигурационный стиль SQLAlchemy используется для выражения намерения отобразить класс, внутренний процесс отображения класса происходит в основном одинаково для каждого из них, где конечным результатом всегда является определенный пользователем класс, который имеет Mapper, настроенный против выбираемой единицы, обычно представленной объектом Table, а сам класс был instrumented, чтобы включить поведение, связанное с реляционными операциями как на уровне класса, так и над экземплярами этого класса. Поскольку процесс во всех случаях в основном одинаков, классы, созданные на основе различных стилей, всегда полностью совместимы друг с другом.

Оригинальный API отображения обычно называют «классическим» стилем, в то время как более автоматизированный стиль отображения известен как «декларативный» стиль. SQLAlchemy теперь называет эти два стиля отображения императивное отображение и декларативное отображение.

Независимо от используемого стиля отображения, все ORM-отображения, начиная с SQLAlchemy 1.4, происходят из одного объекта, известного как registry, который представляет собой реестр отображенных классов. Используя этот реестр, набор конфигураций отображения может быть завершен как группа, а классы в определенном реестре могут ссылаться друг на друга по имени в процессе конфигурирования.

Изменено в версии 1.4: Декларативное и классическое отображение теперь называются «декларативным» и «императивным» отображением, и внутренне они едины, все происходят от конструкции registry, которая представляет собой набор связанных отображений.

Декларативное отображение

Декларативное отображение - это типичный способ построения отображений в современной SQLAlchemy. Наиболее распространенным шаблоном является построение базового класса с помощью функции declarative_base(), который будет применять процесс декларативного отображения ко всем производным от него подклассам. Ниже показана декларативная база, которая затем используется в декларативном отображении таблиц:

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base

# declarative base class
Base = declarative_base()

# an example mapping using the base
class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    nickname = Column(String)

Выше, вызываемый declarative_base() возвращает новый базовый класс, от которого могут наследоваться новые сопоставляемые классы, как выше построен новый сопоставляемый класс User.

Базовый класс относится к объекту registry, который поддерживает коллекцию связанных сопоставленных классов. Функция declarative_base() фактически является сокращением для создания сначала реестра с помощью конструктора registry, а затем генерации базового класса с помощью метода registry.generate_base():

from sqlalchemy.orm import registry

# equivalent to Base = declarative_base()

mapper_registry = registry()
Base = mapper_registry.generate_base()

Основные стили декларативного отображения более подробно описаны в следующих разделах:

В рамках декларативного сопоставленного класса также существует две разновидности того, как могут быть объявлены метаданные Table. К ним относятся:

  • Декларативная таблица - отдельные определения Column объединяются с именем таблицы и дополнительными аргументами, где процесс декларативного сопоставления построит объект Table для сопоставления.

  • Декларатив с императивной таблицей (также известный как гибридный декларатив) - Вместо того чтобы отдельно указывать имя таблицы и атрибуты, явно построенный объект Table ассоциируется с классом, который иначе отображается декларативно. Этот стиль отображения представляет собой гибрид «декларативного» и «императивного» отображения.

Документация по декларативному отображению продолжается на Сопоставление классов с декларативными.

Императивное картирование

Под императивным или классическим отображением понимается конфигурация отображаемого класса с помощью метода registry.map_imperatively(), когда целевой класс не включает никаких декларативных атрибутов класса. Исторически сложилось, что стиль «map imperative» достигается с помощью функции mapper(), однако теперь эта функция ожидает присутствия sqlalchemy.orm.registry().

Не рекомендуется, начиная с версии 1.4: Использование функции mapper() непосредственно для достижения классического сопоставления является устаревшим. Метод registry.map_imperatively() сохраняет идентичную функциональность, позволяя при этом разрешать другие сопоставленные классы на основе строк из реестра.

В «классическом» виде метаданные таблицы создаются отдельно с помощью конструкции Table, затем ассоциируются с классом User посредством метода registry.map_imperatively():

from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import registry

mapper_registry = registry()

user_table = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)


class User:
    pass


mapper_registry.map_imperatively(User, user_table)

Информация о сопоставленных атрибутах, таких как отношения с другими классами, предоставляется через словарь properties. В примере ниже показан второй объект Table, сопоставленный с классом Address, затем связанный с User через relationship():

address = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id)
    },
)

mapper_registry.map_imperatively(Address, address)

При использовании классических отображений классы должны быть указаны напрямую, без использования системы «поиска строк», предоставляемой Declarative. Выражения SQL обычно задаются в терминах объектов Table, т.е. address.c.id выше для отношения Address, а не Address.id, поскольку Address еще не может быть связано с метаданными таблицы, и мы не можем указать здесь строку.

Некоторые примеры в документации все еще используют классический подход, но обратите внимание, что классический и декларативный подходы полностью взаимозаменяемы. Обе системы в конечном итоге создают одну и ту же конфигурацию, состоящую из Table, определяемого пользователем класса, связанного вместе с mapper(). Когда мы говорим о «поведении mapper()», это относится и к использованию декларативной системы - она все еще используется, просто за кулисами.

Сопоставленные существенные компоненты класса

При использовании всех форм отображения, отображение класса может быть настроено различными способами путем передачи аргументов построения, которые становятся частью объекта Mapper. Функция, которая в конечном итоге получает эти аргументы, является функцией mapper(), которые поступают к ней от одной из функций отображения, определенных на объекте registry.

Существует четыре общих класса конфигурационной информации, которую ищет функция mapper():

Класс для отображения

Это класс, который мы создаем в нашем приложении. Обычно на структуру этого класса не накладывается никаких ограничений. [1] Когда класс Python сопоставлен, для него может быть только один Mapper объект. [2]

При отображении с помощью стиля отображения declarative, отображаемый класс либо является подклассом декларативного базового класса, либо обрабатывается декоратором или функцией, такой как registry.mapped().

При отображении в стиле imperative класс передается непосредственно как аргумент map_imperatively.class_.

Таблица или другой объект пункта from

В подавляющем большинстве случаев это экземпляр Table. Для более сложных случаев использования он может также относиться к любому виду объекта FromClause, наиболее распространенными альтернативными объектами являются Subquery и Join.

При отображении с помощью стиля отображения declarative предметная таблица либо генерируется декларативной системой на основе атрибута __tablename__ и представленных объектов Column, либо устанавливается с помощью атрибута __table__. Эти два стиля конфигурации представлены в Декларативная таблица и Декларатив с императивной таблицей (также известный как гибридный декларатив).

При сопоставлении со стилем imperative таблица предметов передается позиционно как аргумент map_imperatively.local_table.

В отличие от требования «один отобразитель на класс» для отображаемого класса, объект Table или другой FromClause, который является предметом отображения, может быть связан с любым количеством отображений. Сопоставление Mapper применяет модификации непосредственно к классу, определенному пользователем, но никак не изменяет данный Table или другой FromClause.

Словарь свойств

Это словарь всех атрибутов, которые будут связаны с отображаемым классом. По умолчанию Mapper генерирует записи для этого словаря, производные от заданного Table, в виде ColumnProperty объектов, каждый из которых ссылается на отдельную Column сопоставленную таблицу. Словарь свойств будет также содержать все другие виды MapperProperty объектов, которые необходимо настроить, чаще всего экземпляры, создаваемые конструкцией relationship().

При отображении в стиле отображения declarative словарь свойств генерируется декларативной системой путем сканирования отображаемого класса на наличие соответствующих атрибутов. Смотрите раздел Определение сопоставленных свойств с помощью декларативного метода для заметок об этом процессе.

При отображении в стиле imperative словарь свойств передается непосредственно как аргумент properties в registry.map_imperatively(), который передаст его в параметр mapper.properties.

Другие параметры конфигурации картографа

При отображении с помощью стиля отображения declarative дополнительные аргументы конфигурации отобразителя задаются через атрибут класса __mapper_args__. Примеры использования доступны в Опции конфигурации картографа с помощью декларативного метода.

При отображении в стиле imperative аргументы ключевых слов передаются в метод to registry.map_imperatively(), который передает их в функцию mapper().

Полный набор принимаемых параметров задокументирован в mapper.

Поведение сопоставленных классов

Для всех стилей отображения, использующих объект registry, характерно следующее поведение:

Конструктор по умолчанию

registry применяет конструктор по умолчанию, то есть метод __init__, ко всем сопоставленным классам, которые явно не имеют собственного метода __init__. Поведение этого метода таково, что он предоставляет удобный конструктор ключевых слов, который будет принимать в качестве необязательных аргументов ключевые слова всех названных атрибутов. Например:

from sqlalchemy.orm import declarative_base

Base = declarative_base()


class User(Base):
    __tablename__ = "user"

    id = Column(...)
    name = Column(...)
    fullname = Column(...)

Объект типа User выше будет иметь конструктор, который позволяет создавать объекты User как:

u1 = User(name="some name", fullname="some fullname")

Приведенный выше конструктор можно настроить, передав в параметр registry.constructor вызываемую программу Python, которая обеспечивает желаемое поведение по умолчанию __init__().

Конструктор также применяется к императивным отображениям:

from sqlalchemy.orm import registry

mapper_registry = registry()

user_table = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)


class User:
    pass


mapper_registry.map_imperatively(User, user_table)

Приведенный выше класс, отображенный императивно, как описано в Императивное картирование, также будет иметь конструктор по умолчанию, связанный с registry.

Добавлено в версии 1.4: Классические отображения теперь поддерживают стандартный конструктор уровня конфигурации, когда они отображаются с помощью метода registry.map_imperatively().

Runtime Introspection of Mapped classes, Instances and Mappers

Класс, который отображается с помощью registry, также будет иметь несколько атрибутов, общих для всех отображений:

  • Атрибут __mapper__ будет ссылаться на Mapper, который связан с классом:

    mapper = User.__mapper__

    Это Mapper также то, что возвращается при использовании функции inspect() против сопоставленного класса:

    from sqlalchemy import inspect
    
    mapper = inspect(User)
  • Атрибут __table__ будет ссылаться на Table, или более обобщенно на объект FromClause, к которому привязан класс:

    table = User.__table__

    Этот FromClause также возвращается при использовании атрибута Mapper.local_table в Mapper:

    table = inspect(User).local_table

    Для однотабличного отображения наследования, когда класс является подклассом, не имеющим собственной таблицы, атрибут Mapper.local_table, а также атрибут .__table__ будут None. Чтобы получить «selectable», который фактически выбирается во время запроса для данного класса, это можно сделать с помощью атрибута Mapper.selectable:

    table = inspect(User).selectable

Проверка объектов картографа

Как показано в предыдущем разделе, объект Mapper доступен из любого сопоставленного класса, независимо от метода, с помощью системы API для проверки во время выполнения. Используя функцию inspect(), можно получить объект Mapper из сопоставленного класса:

>>> from sqlalchemy import inspect
>>> insp = inspect(User)

Доступна подробная информация, включая Mapper.columns:

>>> insp.columns
<sqlalchemy.util._collections.OrderedProperties object at 0x102f407f8>

Это пространство имен, которое можно просматривать в виде списка или отдельных имен:

>>> list(insp.columns)
[Column('id', Integer(), table=<user>, primary_key=True, nullable=False), Column('name', String(length=50), table=<user>), Column('fullname', String(length=50), table=<user>), Column('nickname', String(length=50), table=<user>)]
>>> insp.columns.name
Column('name', String(length=50), table=<user>)

Другие пространства имен включают Mapper.all_orm_descriptors, которое включает все сопоставленные атрибуты, а также гибриды, прокси ассоциации:

>>> insp.all_orm_descriptors
<sqlalchemy.util._collections.ImmutableProperties object at 0x1040e2c68>
>>> insp.all_orm_descriptors.keys()
['fullname', 'nickname', 'name', 'id']

А также Mapper.column_attrs:

>>> list(insp.column_attrs)
[<ColumnProperty at 0x10403fde0; id>, <ColumnProperty at 0x10403fce8; name>, <ColumnProperty at 0x1040e9050; fullname>, <ColumnProperty at 0x1040e9148; nickname>]
>>> insp.column_attrs.name
<ColumnProperty at 0x10403fce8; name>
>>> insp.column_attrs.name.expression
Column('name', String(length=50), table=<user>)

См.также

Mapper

Проверка сопоставленных экземпляров

Функция inspect() также предоставляет информацию об экземплярах сопоставленного класса. При применении к экземпляру сопоставленного класса, а не к самому классу, возвращаемый объект называется InstanceState, который предоставляет ссылки не только на Mapper, используемый классом, но и на подробный интерфейс, предоставляющий информацию о состоянии отдельных атрибутов экземпляра, включая их текущее значение и то, как оно соотносится с загруженным в базу данных значением.

Дан экземпляр класса User, загруженный из базы данных:

>>> u1 = session.scalars(select(User)).first()

Функция inspect() вернет нам объект InstanceState:

>>> insp = inspect(u1)
>>> insp
<sqlalchemy.orm.state.InstanceState object at 0x7f07e5fec2e0>

С помощью этого объекта мы можем увидеть такие элементы, как Mapper:

>>> insp.mapper
<Mapper at 0x7f07e614ef50; User>

Session, к которому относится объект attached, если есть:

>>> insp.session
<sqlalchemy.orm.session.Session object at 0x7f07e614f160>

Информация о текущем persistence state для объекта:

>>> insp.persistent
True
>>> insp.pending
False

Информация о состоянии атрибутов, например, атрибуты, которые не были загружены или lazy loaded (предположим, что addresses относится к relationship() на сопоставленном классе к связанному классу):

>>> insp.unloaded
{'addresses'}

Информация о текущем in-Python статусе атрибутов, например, атрибуты, которые не были изменены с момента последнего flush:

>>> insp.unmodified
{'nickname', 'name', 'fullname', 'id'}

а также конкретную историю изменений атрибутов с момента последней промывки:

>>> insp.attrs.nickname.value
'nickname'
>>> u1.nickname = "new nickname"
>>> insp.attrs.nickname.history
History(added=['new nickname'], unchanged=(), deleted=['nickname'])

См.также

InstanceState

InstanceState.attrs

AttributeState

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