Интеграция с 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 назначаются с помощью одного из трех методов:
При использовании «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()
. Альтернатива этому подходу представлена в следующем примере.
Сопоставление классов данных с помощью декларативного сопоставления¶
Полностью декларативный подход требует, чтобы объекты 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.