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

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

Для читателей, впервые знакомящихся с SQLAlchemy ORM и/или с Python в целом, рекомендуется просмотреть Быстрый старт ORM и, желательно, проработать Унифицированный учебник по SQLAlchemy, где конфигурация ORM впервые представлена в Использование декларативных форм ORM для определения метаданных таблицы.

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

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

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

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

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

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

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

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

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


# declarative base class
class Base(DeclarativeBase):
    pass


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

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str] = mapped_column(String(30))
    nickname: Mapped[Optional[str]]

Выше, класс DeclarativeBase используется для создания нового базового класса (в документации SQLAlchemy он обычно называется Base, но может иметь любое желаемое имя), от которого могут наследоваться новые сопоставляемые классы, как показано выше, создается новый сопоставляемый класс User.

Изменено в версии 2.0: Суперкласс DeclarativeBase заменяет использование функции declarative_base() и методов registry.generate_base(); подход суперкласса интегрируется с инструментами PEP 484 без использования плагинов. Замечания по миграции см. в Декларативные модели ORM.

Базовый класс относится к объекту registry, который хранит коллекцию связанных сопоставленных классов. а также к объекту MetaData, который хранит коллекцию объектов Table, к которым сопоставлены классы.

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

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

  • Декларативная таблица с mapped_column() - столбцы таблицы объявляются inline внутри сопоставленного класса с помощью директивы mapped_column() (или в унаследованной форме, используя непосредственно объект Column). Директива mapped_column() также может быть опционально объединена с аннотациями типов с помощью класса Mapped, который может предоставить некоторые подробности о сопоставленных колонках напрямую. Директивы столбцов в сочетании с директивами __tablename__ и необязательными директивами __table_args__ уровня класса позволят процессу декларативного отображения построить объект Table для отображения.

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

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

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

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

Совет

Императивная форма отображения - это менее используемая форма отображения, которая берет свое начало с самых первых выпусков SQLAlchemy в 2006 году. По сути, это средство обхода декларативной системы для обеспечения более «голой» системы отображения, и не предлагает современных возможностей, таких как поддержка PEP 484. Поэтому в большинстве примеров документации используются формы Declarative, а новым пользователям рекомендуется начинать с конфигурации Declarative Table.

Изменено в версии 2.0: Метод registry.map_imperatively() теперь используется для создания классических отображений. Автономная функция sqlalchemy.orm.mapper() фактически удалена.

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

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)

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

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

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

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

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

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

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

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

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

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

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

При сопоставлении со стилем 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 DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str]

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

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

Совет

Функция Декларативное отображение классов данных предоставляет альтернативный способ генерации метода по умолчанию __init__() с помощью классов данных Python и позволяет создать очень настраиваемую форму конструктора.

Класс, включающий явный метод __init__(), будет поддерживать этот метод, и конструктор по умолчанию применяться не будет.

Чтобы изменить используемый по умолчанию конструктор, к параметру registry.constructor можно предоставить пользовательский вызываемый элемент Python, который будет использоваться в качестве конструктора по умолчанию.

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

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'])
Вернуться на верх