Интеграция с 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 как принадлежащие классу данных. В этом случае выдается предупреждение, которое позже станет ошибкой.
См.также
При преобразовании <cls> в класс данных, атрибут(ы) происходят из суперкласса <cls>, который не является классом данных. - история обоснования
Конфигурация отношений¶
Аннотация 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 назначаются с помощью одного из трех методов:
При использовании «Declarative with Imperative Table» таблица/столбец, подлежащая отображению, определяется с помощью объекта
Table
, назначенного атрибуту__table__
класса; отношения определяются в словаре__mapper_args__
. Класс отображается с помощью декоратораregistry.mapped()
. Пример приведен ниже в Сопоставление уже существующих классов данных с помощью декларативной таблицы с императивной таблицей.При использовании полного «Declarative» декларативно интерпретируемые директивы, такие как
Column
,relationship()
, добавляются в словарь.metadata
конструкцииdataclasses.field()
, где они потребляются декларативным процессом. Класс снова отображается с помощью декоратораregistry.mapped()
. См. пример ниже в Сопоставление уже существующих классов данных с использованием полей декларативного стиля.Императивное» отображение может быть применено к существующему классу данных с помощью метода
registry.map_imperatively()
для создания отображения точно таким же образом, как описано в Императивное картирование. Это проиллюстрировано ниже в Сопоставление уже существующих классов данных с помощью Imperative Mapping.
Общий процесс, с помощью которого 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.