Интеграция с dataclasses и attrs

SQLAlchemy начиная с версии 2.0 поддерживает интеграцию «родных классов данных», где отображение Annotated Declarative Table может быть превращено в Python dataclass путем добавления единственного миксина или декоратора к отображаемым классам.

Добавлено в версии 2.0: Интегрированное создание классов данных с помощью декларативных классов ORM

Существуют также шаблоны, позволяющие отображать существующие классы данных, а также отображать классы, созданные с помощью сторонней интеграционной библиотеки attrs.

Декларативное отображение классов данных

Сопоставления SQLAlchemy Annotated Declarative Table могут быть дополнены дополнительным классом mixin или директивой decorator, что добавит дополнительный шаг к декларативному процессу после завершения сопоставления, который преобразует сопоставленный класс in-place в Python dataclass, перед завершением процесса сопоставления, который применяет специфичные для ORM instrumentation к классу. Наиболее заметным поведенческим дополнением является генерация метода __init__() с тонким контролем над позиционными и ключевыми аргументами с или без значений по умолчанию, а также генерация методов типа __repr__() и __eq__().

С точки зрения типизации PEP 484 класс распознается как имеющий специфическое для Dataclass поведение, прежде всего, используя преимущества PEP 681 «Dataclass Transforms», что позволяет инструментам типизации рассматривать класс так, как если бы он был явно оформлен с помощью декоратора @dataclasses.dataclass.

Примечание

Поддержка PEP 681 в инструментах типизации по состоянию на апрель 4, 2023 ограничена и в настоящее время известна в Pyright, а также в Mypy начиная с версии 1.2. Обратите внимание, что в Mypy 1.1.1 появилась поддержка PEP 681, но в ней не было корректного размещения дескрипторов Python, что приведет к ошибкам при использовании схемы отображения ORM SQLAlhcemy.

См.также

https://peps.python.org/pep-0681/#the-dataclass-transform-decorator - информация о том, как библиотеки типа SQLAlchemy обеспечивают поддержку PEP 681.

Преобразование классов данных может быть добавлено в любой декларативный класс либо путем добавления миксина MappedAsDataclass к иерархии классов DeclarativeBase, либо для отображения декоратора путем использования декоратора класса registry.mapped_as_dataclass().

Миксин MappedAsDataclass может быть применен либо к классу Declarative Base, либо к любому суперклассу, как в примере ниже:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(MappedAsDataclass, DeclarativeBase):
    """subclasses will be converted to dataclasses"""


class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Или может применяться непосредственно к классам, которые расширяются из базы Declarative:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

При использовании формы декоратора поддерживается только декоратор registry.mapped_as_dataclass():

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry


reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Конфигурация функций на уровне класса

Поддержка функций dataclasses является частичной. В настоящее время поддерживаются функции init, repr, eq, order и unsafe_hash, match_args и kw_only поддерживаются на Python 3.10+. В настоящее время не поддерживаются функции frozen и slots.

При использовании формы класса mixin с MappedAsDataclass аргументы конфигурации класса передаются как параметры уровня класса:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

При использовании формы декоратора с registry.mapped_as_dataclass() аргументы конфигурации класса передаются декоратору напрямую:

from sqlalchemy.orm import registry
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


reg = registry()


@reg.mapped_as_dataclass(unsafe_hash=True)
class User:
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Для получения информации о параметрах класса dataclass смотрите документацию dataclasses по адресу @dataclasses.dataclass.

Конфигурация атрибутов

Родные классы данных SQLAlchemy отличаются от обычных классов данных тем, что атрибуты, подлежащие сопоставлению, во всех случаях описываются с помощью контейнера общих аннотаций Mapped. Сопоставления имеют те же формы, что и те, что документированы в Декларативная таблица с mapped_column(), и поддерживаются все возможности mapped_column() и Mapped.

Кроме того, конструкции конфигурации атрибутов ORM, включая mapped_column(), relationship() и composite(), поддерживают параметры полей атрибутов, включая init, default, default_factory и repr. Имена этих аргументов фиксированы, как указано в PEP 681. Функциональность эквивалентна классам данных:

  • init, как в mapped_column.init, relationship.init, если False указывает, что поле не должно быть частью метода __init__()

  • default, как и в mapped_column.default, relationship.default указывает значение по умолчанию для поля, заданного в качестве аргумента ключевого слова в методе __init__().

  • default_factory, как и в mapped_column.default_factory, relationship.default_factory, указывает на вызываемую функцию, которая будет вызвана для создания нового значения по умолчанию для параметра, если он не передан явно в метод __init__().

  • repr Истина по умолчанию, указывает, что поле должно быть частью сгенерированного метода __repr__()

Еще одно ключевое отличие от классов данных заключается в том, что значения по умолчанию для атрибутов должны быть настроены с помощью параметра default конструкции ORM, например mapped_column(default=None). Синтаксис, напоминающий синтаксис dataclass, который принимает простые значения Python в качестве значений по умолчанию без использования @dataclases.field(), не поддерживается.

В качестве примера, используя mapped_column(), приведенное ниже отображение создаст метод __init__(), который принимает только поля name и fullname, где name является обязательным и может передаваться позиционно, а fullname является необязательным. Поле id, которое, как мы ожидаем, будет генерироваться базой данных, вообще не является частью конструктора:

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str] = mapped_column(default=None)


# 'fullname' is optional keyword argument
u1 = User("name")

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

Чтобы учесть перекрытие имен аргумента default с существующим параметром Column.default конструкции Column, конструкция mapped_column() разграничивает эти два имени путем добавления нового параметра mapped_column.insert_default, который будет заполнен непосредственно в параметр Column.default конструкции Column, независимо от того, что может быть задано в параметре mapped_column.default, который всегда используется для конфигурации dataclasses. Например, для конфигурации столбца datetime с параметром Column.default, установленным на func.utc_timestamp() SQL-функции, но где параметр является необязательным в конструкторе:

from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    created_at: Mapped[datetime] = mapped_column(
        insert_default=func.utc_timestamp(), default=None
    )

С приведенным выше отображением, INSERT для нового объекта User, где не было передано ни одного параметра для created_at, выполняется как:

>>> with Session(e) as session:
...     session.add(User())
...     session.commit()
{execsql}BEGIN (implicit)
INSERT INTO user_account (created_at) VALUES (utc_timestamp())
[generated in 0.00010s] ()
COMMIT

Интеграция с аннотированным

Подход, представленный в Сопоставление объявлений целых столбцов с типами Python, иллюстрирует, как использовать PEP 593 Annotated объекты для упаковки целых mapped_column() конструкций для повторного использования. Эта возможность поддерживается функцией dataclasses. Однако один аспект этой возможности требует обходного пути при работе с инструментами набора текста, который заключается в том, что специфические аргументы PEP 681 init, default, repr и default_factory должны находиться в правой части, упакованные в явную конструкцию mapped_column(), чтобы инструмент набора текста правильно интерпретировал атрибут. В качестве примера, приведенный ниже подход будет прекрасно работать во время выполнения, однако инструменты типизации будут считать конструкцию User() недействительной, поскольку они не увидят присутствия параметра init=False:

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

# typing tools will ignore init=False here
intpk = Annotated[int, mapped_column(init=False, primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"
    id: Mapped[intpk]


# typing error: Argument missing for parameter "id"
u1 = User()

Вместо этого mapped_column() должен присутствовать и в правой части с явным заданием для mapped_column.init; остальные аргументы могут оставаться внутри конструкции Annotated:

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

intpk = Annotated[int, mapped_column(primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    # init=False and other pep-681 arguments must be inline
    id: Mapped[intpk] = mapped_column(init=False)


u1 = User()

Использование миксинов и абстрактных суперклассов

Любые миксины или базовые классы, используемые в сопоставленном классе MappedAsDataclass, которые включают атрибуты Mapped, сами должны быть частью иерархии MappedAsDataclass, как в примере ниже с использованием миксина:

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)


class Base(DeclarativeBase, MappedAsDataclass):
    pass


class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

Средства проверки типов Python, поддерживающие PEP 681, в противном случае не будут считать атрибуты из микшинов, не относящихся к классу данных, частью класса данных.

Не рекомендуется, начиная с версии 2.0.8: Использование миксинов и абстрактных баз в иерархиях MappedAsDataclass или registry.mapped_as_dataclass(), которые сами не являются классами данных, не рекомендуется, поскольку эти поля не поддерживаются PEP 681 как принадлежащие классу данных. В этом случае выдается предупреждение, которое позже станет ошибкой.

Конфигурация отношений

Аннотация Mapped в сочетании с relationship() используется так же, как описано в Основные модели взаимоотношений. При указании коллекции relationship() в качестве необязательного аргумента ключевого слова, необходимо передать параметр relationship.default_factory, который должен ссылаться на класс коллекции, который будет использоваться. Ссылки на объекты «многие-к-одному» и скалярные объекты могут использовать relationship.default, если значение по умолчанию должно быть None:

from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

reg = registry()


@reg.mapped_as_dataclass
class Parent:
    __tablename__ = "parent"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        default_factory=list, back_populates="parent"
    )


@reg.mapped_as_dataclass
class Child:
    __tablename__ = "child"
    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
    parent: Mapped["Parent"] = relationship(default=None)

Приведенное выше отображение будет генерировать пустой список для Parent.children, когда новый объект Parent() будет построен без передачи children, и аналогично значение None для Child.parent, когда новый объект Child() будет построен без передачи parent.

Хотя relationship.default_factory можно автоматически вывести из заданного класса коллекции самого relationship(), это нарушит совместимость с классами данных, поскольку наличие relationship.default_factory или relationship.default определяет, будет ли параметр обязательным или необязательным при выводе в метод __init__().

Использование не сопоставленных полей класса данных

При использовании декларативных классов данных в классе могут использоваться и не отображаемые поля, которые будут частью процесса построения класса данных, но не будут отображаться. Любое поле, не использующее Mapped, будет проигнорировано процессом отображения. В примере ниже поля ctrl_one и ctrl_two будут частью состояния объекта на уровне экземпляра, но не будут сохранены ORM:

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class Data:
    __tablename__ = "data"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    status: Mapped[str]

    ctrl_one: Optional[str] = None
    ctrl_two: Optional[str] = None

Экземпляр Data выше может быть создан как:

d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")

Более реальным примером может быть использование функции Dataclasses InitVar в сочетании с функцией __post_init__() для получения init-only полей, которые могут быть использованы для компоновки сохраняемых данных. В приведенном ниже примере класс User объявлен с использованием id, name и password_hash в качестве отображаемых функций, но использует только init-поля password и repeat_password для представления процесса создания пользователя (примечание: чтобы выполнить этот пример, замените функцию your_crypt_function_here() на стороннюю криптографическую функцию, например bcrypt или argon2-cffi):

from dataclasses import InitVar
from typing import Optional

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

    password: InitVar[str]
    repeat_password: InitVar[str]

    password_hash: Mapped[str] = mapped_column(init=False, nullable=False)

    def __post_init__(self, password: str, repeat_password: str):
        if password != repeat_password:
            raise ValueError("passwords do not match")

        self.password_hash = your_crypt_function_here(password)

Приведенный выше объект создается с параметрами password и repeat_password, которые потребляются заранее, чтобы можно было сгенерировать переменную password_hash:

>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz")
>>> u1.password_hash
'$6$9ppc... (example crypted string....)'

Изменено в версии 2.0.0rc1: При использовании registry.mapped_as_dataclass() или MappedAsDataclass могут быть включены поля, не включающие аннотацию Mapped, которые будут рассматриваться как часть результирующего класса данных, но не будут отображаться, без необходимости указывать атрибут класса __allow_unmapped__. Предыдущие бета-версии 2.0 требовали явного присутствия этого атрибута, несмотря на то, что его назначение заключалось только в том, чтобы позволить унаследованным типизированным отображениям ORM продолжать функционировать.

Интеграция с поставщиками альтернативных классов данных, такими как Pydantic

Предупреждение

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

Для совместимости с Pydantic, пожалуйста, рассмотрите SQLModel <https://sqlmodel.tiangolo.com/> ORM, который построен с Pydantic поверх SQLAlchemy ORM, который включает специальные детали реализации, которые явно устраняют эти несовместимости.

Вызов функции класса SQLAlchemy MappedAsDataclass и метода registry.mapped_as_dataclass() непосредственно в декораторе класса стандартной библиотеки Python dataclasses.dataclass после применения к классу процесса декларативного отображения. Этот вызов функции может быть заменен на альтернативные поставщики классов данных, такие как Pydantic, с помощью параметра dataclass_callable, принимаемого MappedAsDataclass в качестве аргумента ключевого слова класса, а также registry.mapped_as_dataclass():

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import registry


class Base(
    MappedAsDataclass,
    DeclarativeBase,
    dataclass_callable=pydantic.dataclasses.dataclass,
):
    pass


class User(Base):
    __tablename__ = "user"

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

Приведенный выше класс User будет применен в качестве класса данных, используя вызываемый Pydantic pydantic.dataclasses.dataclasses. Этот процесс доступен как для сопоставленных классов, так и для миксинов, которые расширяются из MappedAsDataclass или которые имеют registry.mapped_as_dataclass(), применяемые напрямую.

Добавлено в версии 2.0.4: Добавлены параметры класса dataclass_callable и методов MappedAsDataclass и registry.mapped_as_dataclass(), а также скорректированы некоторые внутренние функции классов данных, чтобы приспособить их к более строгим функциям классов данных, таким как функции Pydantic.

Применение отображений ORM к существующему классу данных (использование унаследованного класса данных)

Legacy Feature

Описанные здесь подходы заменены функцией Декларативное отображение классов данных, новой в серии 2.0 SQLAlchemy. Эта новая версия функции основывается на поддержке классов данных, впервые добавленной в версии 1.4, которая описана в этом разделе.

Для отображения существующего класса данных нельзя напрямую использовать «встроенные» декларативные директивы SQLAlchemy; директивы ORM назначаются с помощью одного из трех методов:

Общий процесс, с помощью которого SQLAlchemy применяет отображения к классу данных, такой же, как и для обычного класса, но также включает в себя то, что SQLAlchemy обнаружит атрибуты уровня класса, которые были частью процесса объявления классов данных, и заменит их во время выполнения обычными атрибутами, отображаемыми SQLAlchemy ORM. Метод __init__, который был бы сгенерирован dataclasses, остается нетронутым, как и все остальные методы, генерируемые dataclasses, такие как __eq__(), __repr__() и т.д.

Сопоставление уже существующих классов данных с помощью декларативной таблицы с императивной таблицей

Ниже приведен пример отображения @dataclass с использованием Декларатив с императивной таблицей (также известный как гибридный декларатив). Полный объект Table строится явно и присваивается атрибуту __table__. Поля экземпляра определяются с использованием обычного синтаксиса класса данных. Дополнительные определения MapperProperty, такие как relationship(), помещаются в словарь __mapper_args__ уровня класса под ключом properties, соответствующим параметру Mapper.properties:

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Optional

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

mapper_registry = registry()


@mapper_registry.mapped
@dataclass
class 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)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
    addresses: List[Address] = field(default_factory=list)

    __mapper_args__ = {  # type: ignore
        "properties": {
            "addresses": relationship("Address"),
        }
    }


@mapper_registry.mapped
@dataclass
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: Optional[str] = None

В приведенном выше примере атрибуты User.id, Address.id и Address.user_id определены как field(init=False). Это означает, что параметры для них не будут добавлены в методы __init__(), но Session все равно смогут их установить, получив их значения при промывке от автоинкремента или другого генератора значений по умолчанию. Чтобы позволить им быть указанными в конструкторе явно, вместо этого им будет присвоено значение по умолчанию None.

Чтобы relationship() был объявлен отдельно, его нужно указать непосредственно в словаре Mapper.properties, который сам указан в словаре __mapper_args__, чтобы он был передан в конструктор для Mapper. Альтернатива этому подходу представлена в следующем примере.

Сопоставление уже существующих классов данных с использованием полей декларативного стиля

Legacy Feature

Этот подход к декларативному отображению с использованием классов данных следует рассматривать как наследие. Он будет поддерживаться, однако вряд ли даст какие-либо преимущества по сравнению с новым подходом, подробно описанным в Декларативное отображение классов данных.

Обратите внимание, что mapped_column() не поддерживается при таком использовании; конструкция Column должна по-прежнему использоваться для объявления метаданных таблицы в поле metadata в dataclasses.field().

Полностью декларативный подход требует, чтобы объекты Column были объявлены как атрибуты класса, что при использовании классов данных будет конфликтовать с атрибутами уровня класса данных. Подход, позволяющий объединить их вместе, заключается в использовании атрибута metadata на объекте dataclass.field, где может быть предоставлена специфическая для SQLAlchemy информация о сопоставлении. Declarative поддерживает извлечение этих параметров, когда класс указывает атрибут __sa_dataclass_metadata_key__. Это также обеспечивает более лаконичный метод указания ассоциации relationship():

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List

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

mapper_registry = registry()


@mapper_registry.mapped
@dataclass
class User:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    name: str = field(default=None, metadata={"sa": Column(String(50))})
    fullname: str = field(default=None, metadata={"sa": Column(String(50))})
    nickname: str = field(default=None, metadata={"sa": Column(String(12))})
    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": relationship("Address")}
    )


@mapper_registry.mapped
@dataclass
class Address:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(init=False, metadata={"sa": Column(ForeignKey("user.id"))})
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})

Использование декларативных миксинов с уже существующими классами данных

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

class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column("target_id", ForeignKey("target.id"))

    @declared_attr
    def target(cls):
        return relationship("Target")

Эта форма поддерживается в объекте Dataclasses field() с помощью лямбды для обозначения конструкции SQLAlchemy внутри field(). Использование declared_attr() для окружения лямбды необязательно. Если бы мы хотели создать наш приведенный выше класс User, в котором поля ORM были бы получены из миксина, который сам по себе является классом данных, форма была бы следующей:

@dataclass
class UserMixin:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"

    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})

    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": lambda: relationship("Address")}
    )


@dataclass
class AddressMixin:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(
        init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))}
    )
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})


@mapper_registry.mapped
class User(UserMixin):
    pass


@mapper_registry.mapped
class Address(AddressMixin):
    pass

Добавлено в версии 1.4.2: Добавлена поддержка миксин-атрибутов в стиле «declared attr», а именно конструкций relationship(), а также объектов Column с объявлениями внешних ключей, для использования в связках в стиле «Dataclasses with Declarative Table».

Сопоставление уже существующих классов данных с помощью Imperative Mapping

Как было описано ранее, класс, установленный как класс данных с помощью декоратора @dataclass, может быть дополнительно декорирован с помощью декоратора registry.mapped(), чтобы применить к классу отображение в декларативном стиле. В качестве альтернативы использованию декоратора registry.mapped() мы можем передать класс через метод registry.map_imperatively(), чтобы передать все конфигурации Table и Mapper императивно в функцию, а не определять их в самом классе как переменные класса:

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field
from typing import List

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@dataclass
class User:
    id: int = field(init=False)
    name: str = None
    fullname: str = None
    nickname: str = None
    addresses: List[Address] = field(default_factory=list)


@dataclass
class Address:
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: str = None


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

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)

Применение отображений ORM к существующему классу attrs

Библиотека attrs - это популярная сторонняя библиотека, которая предоставляет функции, аналогичные классам данных, с множеством дополнительных возможностей, не встречающихся в обычных классах данных.

Класс, дополненный attrs, использует декоратор @define. Этот декоратор инициирует процесс сканирования класса на наличие атрибутов, определяющих поведение класса, которые затем используются для создания методов, документации и аннотаций.

SQLAlchemy ORM поддерживает отображение класса attrs с помощью Declarative with Imperative Table или Imperative отображения. Общая форма этих двух стилей полностью эквивалентна формам отображения Сопоставление уже существующих классов данных с использованием полей декларативного стиля и Сопоставление уже существующих классов данных с помощью декларативной таблицы с императивной таблицей, используемым с классами данных, где встроенные директивы атрибутов, используемые классами данных или attrs, остаются неизменными, а таблично-ориентированная инструментация SQLAlchemy применяется во время выполнения.

Декоратор attrs @define по умолчанию заменяет аннотированный класс новым классом на основе __slots__, который не поддерживается. При использовании аннотации старого стиля @attr.s или при использовании define(slots=False) класс не заменяется. Кроме того, после запуска декоратора атрибуты, связанные с классом, удаляются, так что процесс отображения SQLAlchemy принимает эти атрибуты без каких-либо проблем. Оба декоратора, @attr.s и @define(slots=False), работают с SQLAlchemy.

Сопоставление атрибутов с декларативной «императивной таблицей»

В стиле «Декларативный с императивной таблицей» объект Table объявляется в строке с декларативным классом. Сначала к классу применяется декоратор @define, затем декоратор registry.mapped():

from __future__ import annotations

from typing import List
from typing import Optional

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@mapper_registry.mapped
@define(slots=False)
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("FullName", String(50), key="fullname"),
        Column("nickname", String(12)),
    )
    id: Mapped[int]
    name: Mapped[str]
    fullname: Mapped[str]
    nickname: Mapped[str]
    addresses: Mapped[List[Address]]

    __mapper_args__ = {  # type: ignore
        "properties": {
            "addresses": relationship("Address"),
        }
    }


@mapper_registry.mapped
@define(slots=False)
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: Mapped[int]
    user_id: Mapped[int]
    email_address: Mapped[Optional[str]]

Примечание

Опция attrs slots=True, которая включает __slots__ на сопоставленном классе, не может использоваться с сопоставлениями SQLAlchemy без полной реализации альтернативы attribute instrumentation, поскольку сопоставленные классы обычно полагаются на прямой доступ к __dict__ для хранения состояния. Поведение при наличии этой опции не определено.

Сопоставление атрибутов с помощью императивного отображения

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

from __future__ import annotations

from typing import List

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@define(slots=False)
class User:
    id: int
    name: str
    fullname: str
    nickname: str
    addresses: List[Address]


@define(slots=False)
class Address:
    id: int
    user_id: int
    email_address: Optional[str]


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

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 с Imperative Table.

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