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

SQLAlchemy 1.4 имеет ограниченную поддержку отображений ORM, которые устанавливаются на классы, уже предварительно проинструктированные с помощью встроенной библиотеки Python dataclasses или сторонней интеграционной библиотеки attrs.

Совет

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

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

Модуль dataclasses, добавленный в Python 3.7, предоставляет декоратор классов @dataclass для автоматической генерации шаблонных определений общих методов объектов, включая __init__(), __repr()__ и другие методы. SQLAlchemy поддерживает применение отображений ORM к классу после его обработки декоратором @dataclass, используя либо декоратор класса registry.mapped(), либо метод registry.map_imperatively() для применения отображений ORM к классу с помощью Imperative.

Добавлено в версии 1.4: Добавлена поддержка прямого отображения классов данных Python

Для отображения существующего класса данных нельзя напрямую использовать «встроенные» декларативные директивы 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(). Альтернатива этому подходу представлена в следующем примере.

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

Полностью декларативный подход требует, чтобы объекты 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))})

Сопоставление классов данных с помощью 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)

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

В разделе Составление сопоставленных иерархий с помощью миксинов представлены классы декларативных миксинов. Одним из требований к декларативным миксинам является то, что определенные конструкции, которые не могут быть легко продублированы, должны быть представлены в виде вызываемых конструкций с использованием декоратора 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».

Применение отображений 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 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()


@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)),
        Column("nickname", String(12)),
    )
    id: int
    name: str
    fullname: str
    nickname: str
    addresses: 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: int
    user_id: int
    email_address: 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.

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