Integration with dataclasses and attrs

SQLAlchemy as of version 2.0 features “native dataclass” integration where an Annotated Declarative Table mapping may be turned into a Python dataclass by adding a single mixin or decorator to mapped classes.

New in version 2.0: Integrated dataclass creation with ORM Declarative classes

There are also patterns available that allow existing dataclasses to be mapped, as well as to map classes instrumented by the attrs third party integration library.

Declarative Dataclass Mapping

SQLAlchemy Annotated Declarative Table mappings may be augmented with an additional mixin class or decorator directive, which will add an additional step to the Declarative process after the mapping is complete that will convert the mapped class in-place into a Python dataclass, before completing the mapping process which applies ORM-specific instrumentation to the class. The most prominent behavioral addition this provides is generation of an __init__() method with fine-grained control over positional and keyword arguments with or without defaults, as well as generation of methods like __repr__() and __eq__().

From a PEP 484 typing perspective, the class is recognized as having Dataclass-specific behaviors, most notably by taking advantage of PEP 681 “Dataclass Transforms”, which allows typing tools to consider the class as though it were explicitly decorated using the @dataclasses.dataclass decorator.

Note

Support for PEP 681 in typing tools as of July 3, 2022 is limited and is currently known to be supported by Pyright, but not yet Mypy. When PEP 681 is not supported, typing tools will see the __init__() constructor provided by the DeclarativeBase superclass, if used, else will see the constructor as untyped.

Dataclass conversion may be added to any Declarative class either by adding the MappedAsDataclass mixin to a DeclarativeBase class hierarchy, or for decorator mapping by using the registry.mapped_as_dataclass() class decorator.

The MappedAsDataclass mixin may be applied either to the Declarative Base class or any superclass, as in the example below:

  1. from sqlalchemy.orm import DeclarativeBase
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. from sqlalchemy.orm import MappedAsDataclass
  5. class Base(MappedAsDataclass, DeclarativeBase):
  6. """subclasses will be converted to dataclasses"""
  7. class User(Base):
  8. __tablename__ = "user_account"
  9. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  10. name: Mapped[str]

Or may be applied directly to classes that extend from the Declarative base:

  1. from sqlalchemy.orm import DeclarativeBase
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. from sqlalchemy.orm import MappedAsDataclass
  5. class Base(DeclarativeBase):
  6. pass
  7. class User(MappedAsDataclass, Base):
  8. """User class will be converted to a dataclass"""
  9. __tablename__ = "user_account"
  10. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  11. name: Mapped[str]

When using the decorator form, only the registry.mapped_as_dataclass() decorator is supported:

  1. from sqlalchemy.orm import Mapped
  2. from sqlalchemy.orm import mapped_column
  3. from sqlalchemy.orm import registry
  4. reg = registry()
  5. @reg.mapped_as_dataclass
  6. class User:
  7. __tablename__ = "user_account"
  8. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  9. name: Mapped[str]

Class level feature configuration

Support for dataclasses features is partial. Currently supported are the init, repr, eq, order and unsafe_hash features, match_args and kw_only are supported on Python 3.10+. Currently not supported are the frozen and slots features.

When using the mixin class form with MappedAsDataclass, class configuration arguments are passed as class-level parameters:

  1. from sqlalchemy.orm import DeclarativeBase
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. from sqlalchemy.orm import MappedAsDataclass
  5. class Base(DeclarativeBase):
  6. pass
  7. class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True):
  8. """User class will be converted to a dataclass"""
  9. __tablename__ = "user_account"
  10. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  11. name: Mapped[str]

When using the decorator form with registry.mapped_as_dataclass(), class configuration arguments are passed to the decorator directly:

  1. from sqlalchemy.orm import registry
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. reg = registry()
  5. @reg.mapped_as_dataclass(unsafe_hash=True)
  6. class User:
  7. """User class will be converted to a dataclass"""
  8. __tablename__ = "user_account"
  9. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  10. name: Mapped[str]

For background on dataclass class options, see the dataclasses documentation at @dataclasses.dataclass.

Attribute Configuration

SQLAlchemy native dataclasses differ from normal dataclasses in that attributes to be mapped are described using the Mapped generic annotation container in all cases. Mappings follow the same forms as those documented at Declarative Table with mapped_column(), and all features of mapped_column() and Mapped are supported.

Additionally, ORM attribute configuration constructs including mapped_column(), relationship() and composite() support per-attribute field options, including init, default, default_factory and repr. The names of these arguments is fixed as specified in PEP 681. Functionality is equivalent to dataclasses:

Another key difference from dataclasses is that default values for attributes must be configured using the default parameter of the ORM construct, such as mapped_column(default=None). A syntax that resembles dataclass syntax which accepts simple Python values as defaults without using @dataclases.field() is not supported.

As an example using mapped_column(), the mapping below will produce an __init__() method that accepts only the fields name and fullname, where name is required and may be passed positionally, and fullname is optional. The id field, which we expect to be database-generated, is not part of the constructor at all:

  1. from sqlalchemy.orm import Mapped
  2. from sqlalchemy.orm import mapped_column
  3. from sqlalchemy.orm import registry
  4. reg = registry()
  5. @reg.mapped_as_dataclass
  6. class User:
  7. __tablename__ = "user_account"
  8. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  9. name: Mapped[str]
  10. fullname: Mapped[str] = mapped_column(default=None)
  11. # 'fullname' is optional keyword argument
  12. u1 = User("name")

Column Defaults

In order to accommodate the name overlap of the default argument with the existing Column.default parameter of the Column construct, the mapped_column() construct disambiguates the two names by adding a new parameter mapped_column.insert_default, which will be populated directly into the Column.default parameter of Column, independently of what may be set on mapped_column.default, which is always used for the dataclasses configuration. For example, to configure a datetime column with a Column.default set to the func.utc_timestamp() SQL function, but where the parameter is optional in the constructor:

  1. from datetime import datetime
  2. from sqlalchemy import func
  3. from sqlalchemy.orm import Mapped
  4. from sqlalchemy.orm import mapped_column
  5. from sqlalchemy.orm import registry
  6. reg = registry()
  7. @reg.mapped_as_dataclass
  8. class User:
  9. __tablename__ = "user_account"
  10. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  11. created_at: Mapped[datetime] = mapped_column(
  12. insert_default=func.utc_timestamp(), default=None
  13. )

With the above mapping, an INSERT for a new User object where no parameter for created_at were passed proceeds as:

  1. >>> with Session(e) as session:
  2. ... session.add(User())
  3. ... session.commit()
  4. BEGIN (implicit)
  5. INSERT INTO user_account (created_at) VALUES (utc_timestamp())
  6. [generated in 0.00010s] ()
  7. COMMIT

Integration with Annotated

The approach introduced at Mapping Whole Column Declarations to Python Types illustrates how to use PEP 593 Annotated objects to package whole mapped_column() constructs for re-use. This feature is supported with the dataclasses feature. One aspect of the feature however requires a workaround when working with typing tools, which is that the PEP 681-specific arguments init, default, repr, and default_factory must be on the right hand side, packaged into an explicit mapped_column() construct, in order for the typing tool to interpret the attribute correctly. As an example, the approach below will work perfectly fine at runtime, however typing tools will consider the User() construction to be invalid, as they do not see the init=False parameter present:

  1. from typing import Annotated
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. from sqlalchemy.orm import registry
  5. # typing tools will ignore init=False here
  6. intpk = Annotated[int, mapped_column(init=False, primary_key=True)]
  7. reg = registry()
  8. @reg.mapped_as_dataclass
  9. class User:
  10. __tablename__ = "user_account"
  11. id: Mapped[intpk]
  12. # typing error: Argument missing for parameter "id"
  13. u1 = User()

Instead, mapped_column() must be present on the right side as well with an explicit setting for mapped_column.init; the other arguments can remain within the Annotated construct:

  1. from typing import Annotated
  2. from sqlalchemy.orm import Mapped
  3. from sqlalchemy.orm import mapped_column
  4. from sqlalchemy.orm import registry
  5. intpk = Annotated[int, mapped_column(primary_key=True)]
  6. reg = registry()
  7. @reg.mapped_as_dataclass
  8. class User:
  9. __tablename__ = "user_account"
  10. # init=False and other pep-681 arguments must be inline
  11. id: Mapped[intpk] = mapped_column(init=False)
  12. u1 = User()

Relationship Configuration

The Mapped annotation in combination with relationship() is used in the same way as described at Basic Relationship Patterns. When specifying a collection-based relationship() as an optional keyword argument, the relationship.default_factory parameter must be passed and it must refer to the collection class that’s to be used. Many-to-one and scalar object references may make use of relationship.default if the default value is to be None:

  1. from typing import List
  2. from sqlalchemy import ForeignKey
  3. from sqlalchemy.orm import Mapped
  4. from sqlalchemy.orm import mapped_column
  5. from sqlalchemy.orm import registry
  6. from sqlalchemy.orm import relationship
  7. reg = registry()
  8. @reg.mapped_as_dataclass
  9. class Parent:
  10. __tablename__ = "parent"
  11. id: Mapped[int] = mapped_column(primary_key=True)
  12. children: Mapped[List["Child"]] = relationship(
  13. default_factory=list, back_populates="parent"
  14. )
  15. @reg.mapped_as_dataclass
  16. class Child:
  17. __tablename__ = "child"
  18. id: Mapped[int] = mapped_column(primary_key=True)
  19. parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
  20. parent: Mapped["Parent"] = relationship(default=None)

The above mapping will generate an empty list for Parent.children when a new Parent() object is constructed without passing children, and similarly a None value for Child.parent when a new Child() object is constructed without passsing parent.

While the relationship.default_factory can be automatically derived from the given collection class of the relationship() itself, this would break compatibility with dataclasses, as the presence of relationship.default_factory or relationship.default is what determines if the parameter is to be required or optional when rendered into the __init__() method.

Using Non-Mapped Dataclass Fields

When using Declarative dataclasses, non-mapped fields may be used on the class as well, which will be part of the dataclass construction process but will not be mapped. Any field that does not use Mapped will be ignored by the mapping process. In the example below, the fields ctrl_one and ctrl_two will be part of the instance-level state of the object, but will not be persisted by the ORM:

  1. from sqlalchemy.orm import Mapped
  2. from sqlalchemy.orm import mapped_column
  3. from sqlalchemy.orm import registry
  4. reg = registry()
  5. @reg.mapped_as_dataclass
  6. class Data:
  7. __tablename__ = "data"
  8. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  9. status: Mapped[str]
  10. ctrl_one: Optional[str] = None
  11. ctrl_two: Optional[str] = None

Instance of Data above can be created as:

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

A more real world example might be to make use of the Dataclasses InitVar feature in conjunction with the __post_init__() feature to receive init-only fields that can be used to compose persisted data. In the example below, the User class is declared using id, name and password_hash as mapped features, but makes use of init-only password and repeat_password fields to represent the user creation process (note: to run this example, replace the function your_crypt_function_here() with a third party crypt function, such as bcrypt or argon2-cffi):

  1. from dataclasses import InitVar
  2. from typing import Optional
  3. from sqlalchemy.orm import Mapped
  4. from sqlalchemy.orm import mapped_column
  5. from sqlalchemy.orm import registry
  6. reg = registry()
  7. @reg.mapped_as_dataclass
  8. class User:
  9. __tablename__ = "user_account"
  10. id: Mapped[int] = mapped_column(init=False, primary_key=True)
  11. name: Mapped[str]
  12. password: InitVar[str]
  13. repeat_password: InitVar[str]
  14. password_hash: Mapped[str] = mapped_column(init=False, nullable=False)
  15. def __post_init__(self, password: str, repeat_password: str):
  16. if password != repeat_password:
  17. raise ValueError("passwords do not match")
  18. self.password_hash = your_crypt_function_here(password)

The above object is created with parameters password and repeat_password, which are consumed up front so that the password_hash variable may be generated:

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

Changed in version 2.0.0rc1: When using registry.mapped_as_dataclass() or MappedAsDataclass, fields that do not include the Mapped annotation may be included, which will be treated as part of the resulting dataclass but not be mapped, without the need to also indicate the __allow_unmapped__ class attribute. Previous 2.0 beta releases would require this attribute to be explicitly present, even though the purpose of this attribute was only to allow legacy ORM typed mappings to continue to function.

Applying ORM Mappings to an existing dataclass

SQLAlchemy’s native dataclass support builds upon the previous version of the feature first introduced in SQLAlchemy 1.4, which supports the application of ORM mappings to a class after it has been processed with the @dataclass decorator, by using either the registry.mapped() class decorator, or the registry.map_imperatively() method to apply ORM mappings to the class using Imperative. This approach is still viable for applications that are using partially or fully imperative mapping forms with dataclasses.

For fully Declarative mapping combined with dataclasses, the Declarative Dataclass Mapping approach should be preferred.

New in version 1.4: Added support for direct mapping of Python dataclasses

To map an existing dataclass, SQLAlchemy’s “inline” declarative directives cannot be used directly; ORM directives are assigned using one of three techniques:

The general process by which SQLAlchemy applies mappings to a dataclass is the same as that of an ordinary class, but also includes that SQLAlchemy will detect class-level attributes that were part of the dataclasses declaration process and replace them at runtime with the usual SQLAlchemy ORM mapped attributes. The __init__ method that would have been generated by dataclasses is left intact, as is the same for all the other methods that dataclasses generates such as __eq__(), __repr__(), etc.

Mapping dataclasses using Declarative With Imperative Table

An example of a mapping using @dataclass using Declarative with Imperative Table (a.k.a. Hybrid Declarative) is below. A complete Table object is constructed explicitly and assigned to the __table__ attribute. Instance fields are defined using normal dataclass syntaxes. Additional MapperProperty definitions such as relationship(), are placed in the __mapper_args__ class-level dictionary underneath the properties key, corresponding to the Mapper.properties parameter:

  1. from __future__ import annotations
  2. from dataclasses import dataclass, field
  3. from typing import List, Optional
  4. from sqlalchemy import Column, ForeignKey, Integer, String, Table
  5. from sqlalchemy.orm import registry, relationship
  6. mapper_registry = registry()
  7. @mapper_registry.mapped
  8. @dataclass
  9. class User:
  10. __table__ = Table(
  11. "user",
  12. mapper_registry.metadata,
  13. Column("id", Integer, primary_key=True),
  14. Column("name", String(50)),
  15. Column("fullname", String(50)),
  16. Column("nickname", String(12)),
  17. )
  18. id: int = field(init=False)
  19. name: Optional[str] = None
  20. fullname: Optional[str] = None
  21. nickname: Optional[str] = None
  22. addresses: List[Address] = field(default_factory=list)
  23. __mapper_args__ = { # type: ignore
  24. "properties": {
  25. "addresses": relationship("Address"),
  26. }
  27. }
  28. @mapper_registry.mapped
  29. @dataclass
  30. class Address:
  31. __table__ = Table(
  32. "address",
  33. mapper_registry.metadata,
  34. Column("id", Integer, primary_key=True),
  35. Column("user_id", Integer, ForeignKey("user.id")),
  36. Column("email_address", String(50)),
  37. )
  38. id: int = field(init=False)
  39. user_id: int = field(init=False)
  40. email_address: Optional[str] = None

In the above example, the User.id, Address.id, and Address.user_id attributes are defined as field(init=False). This means that parameters for these won’t be added to __init__() methods, but Session will still be able to set them after getting their values during flush from autoincrement or other default value generator. To allow them to be specified in the constructor explicitly, they would instead be given a default value of None.

For a relationship() to be declared separately, it needs to be specified directly within the Mapper.properties dictionary which itself is specified within the __mapper_args__ dictionary, so that it is passed to the constructor for Mapper. An alternative to this approach is in the next example.

Mapping dataclasses using Declarative Mapping

Deprecated since version 2.0: This approach to Declarative mapping with dataclasses should be considered as legacy. It will remain supported however is unlikely to offer any advantages against the new approach detailed at Declarative Dataclass Mapping.

The fully declarative approach requires that Column objects are declared as class attributes, which when using dataclasses would conflict with the dataclass-level attributes. An approach to combine these together is to make use of the metadata attribute on the dataclass.field object, where SQLAlchemy-specific mapping information may be supplied. Declarative supports extraction of these parameters when the class specifies the attribute __sa_dataclass_metadata_key__. This also provides a more succinct method of indicating the relationship() association:

  1. from __future__ import annotations
  2. from dataclasses import dataclass, field
  3. from typing import List
  4. from sqlalchemy import Column, ForeignKey, Integer, String
  5. from sqlalchemy.orm import registry, relationship
  6. mapper_registry = registry()
  7. @mapper_registry.mapped
  8. @dataclass
  9. class User:
  10. __tablename__ = "user"
  11. __sa_dataclass_metadata_key__ = "sa"
  12. id: int = field(
  13. init=False, metadata={"sa": mapped_column(Integer, primary_key=True)}
  14. )
  15. name: str = field(default=None, metadata={"sa": mapped_column(String(50))})
  16. fullname: str = field(default=None, metadata={"sa": mapped_column(String(50))})
  17. nickname: str = field(default=None, metadata={"sa": mapped_column(String(12))})
  18. addresses: List[Address] = field(
  19. default_factory=list, metadata={"sa": relationship("Address")}
  20. )
  21. @mapper_registry.mapped
  22. @dataclass
  23. class Address:
  24. __tablename__ = "address"
  25. __sa_dataclass_metadata_key__ = "sa"
  26. id: int = field(
  27. init=False, metadata={"sa": mapped_column(Integer, primary_key=True)}
  28. )
  29. user_id: int = field(
  30. init=False, metadata={"sa": mapped_column(ForeignKey("user.id"))}
  31. )
  32. email_address: str = field(default=None, metadata={"sa": mapped_column(String(50))})

Using Declarative Mixins with Dataclasses

In the section Composing Mapped Hierarchies with Mixins, Declarative Mixin classes are introduced. One requirement of declarative mixins is that certain constructs that can’t be easily duplicated must be given as callables, using the declared_attr decorator, such as in the example at Mixing in Relationships:

  1. class RefTargetMixin:
  2. @declared_attr
  3. def target_id(cls):
  4. return mapped_column("target_id", ForeignKey("target.id"))
  5. @declared_attr
  6. def target(cls):
  7. return relationship("Target")

This form is supported within the Dataclasses field() object by using a lambda to indicate the SQLAlchemy construct inside the field(). Using declared_attr() to surround the lambda is optional. If we wanted to produce our User class above where the ORM fields came from a mixin that is itself a dataclass, the form would be:

  1. @dataclass
  2. class UserMixin:
  3. __tablename__ = "user"
  4. __sa_dataclass_metadata_key__ = "sa"
  5. id: int = field(
  6. init=False, metadata={"sa": mapped_column(Integer, primary_key=True)}
  7. )
  8. addresses: List[Address] = field(
  9. default_factory=list, metadata={"sa": lambda: relationship("Address")}
  10. )
  11. @dataclass
  12. class AddressMixin:
  13. __tablename__ = "address"
  14. __sa_dataclass_metadata_key__ = "sa"
  15. id: int = field(
  16. init=False, metadata={"sa": mapped_column(Integer, primary_key=True)}
  17. )
  18. user_id: int = field(
  19. init=False, metadata={"sa": lambda: mapped_column(ForeignKey("user.id"))}
  20. )
  21. email_address: str = field(default=None, metadata={"sa": mapped_column(String(50))})
  22. @mapper_registry.mapped
  23. class User(UserMixin):
  24. pass
  25. @mapper_registry.mapped
  26. class Address(AddressMixin):
  27. pass

New in version 1.4.2: Added support for “declared attr” style mixin attributes, namely relationship() constructs as well as Column objects with foreign key declarations, to be used within “Dataclasses with Declarative Table” style mappings.

Mapping dataclasses using Imperative Mapping

As described previously, a class which is set up as a dataclass using the @dataclass decorator can then be further decorated using the registry.mapped() decorator in order to apply declarative-style mapping to the class. As an alternative to using the registry.mapped() decorator, we may also pass the class through the registry.map_imperatively() method instead, so that we may pass all Table and Mapper configuration imperatively to the function rather than having them defined on the class itself as class variables:

  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from dataclasses import field
  4. from typing import List
  5. from sqlalchemy import Column
  6. from sqlalchemy import ForeignKey
  7. from sqlalchemy import Integer
  8. from sqlalchemy import MetaData
  9. from sqlalchemy import String
  10. from sqlalchemy import Table
  11. from sqlalchemy.orm import registry
  12. from sqlalchemy.orm import relationship
  13. mapper_registry = registry()
  14. @dataclass
  15. class User:
  16. id: int = field(init=False)
  17. name: str = None
  18. fullname: str = None
  19. nickname: str = None
  20. addresses: List[Address] = field(default_factory=list)
  21. @dataclass
  22. class Address:
  23. id: int = field(init=False)
  24. user_id: int = field(init=False)
  25. email_address: str = None
  26. metadata_obj = MetaData()
  27. user = Table(
  28. "user",
  29. metadata_obj,
  30. Column("id", Integer, primary_key=True),
  31. Column("name", String(50)),
  32. Column("fullname", String(50)),
  33. Column("nickname", String(12)),
  34. )
  35. address = Table(
  36. "address",
  37. metadata_obj,
  38. Column("id", Integer, primary_key=True),
  39. Column("user_id", Integer, ForeignKey("user.id")),
  40. Column("email_address", String(50)),
  41. )
  42. mapper_registry.map_imperatively(
  43. User,
  44. user,
  45. properties={
  46. "addresses": relationship(Address, backref="user", order_by=address.c.id),
  47. },
  48. )
  49. mapper_registry.map_imperatively(Address, address)

Applying ORM mappings to an existing attrs class

The attrs library is a popular third party library that provides similar features as dataclasses, with many additional features provided not found in ordinary dataclasses.

A class augmented with attrs uses the @define decorator. This decorator initiates a process to scan the class for attributes that define the class’ behavior, which are then used to generate methods, documentation, and annotations.

The SQLAlchemy ORM supports mapping an attrs class using Declarative with Imperative Table or Imperative mapping. The general form of these two styles is fully equivalent to the Mapping dataclasses using Declarative Mapping and Mapping dataclasses using Declarative With Imperative Table mapping forms used with dataclasses, where the inline attribute directives used by dataclasses or attrs are unchanged, and SQLAlchemy’s table-oriented instrumentation is applied at runtime.

The @define decorator of attrs by default replaces the annotated class with a new __slots__ based class, which is not supported. When using the old style annotation @attr.s or using define(slots=False), the class does not get replaced. Furthermore attrs removes its own class-bound attributes after the decorator runs, so that SQLAlchemy’s mapping process takes over these attributes without any issue. Both decorators, @attr.s and @define(slots=False) work with SQLAlchemy.

Mapping attrs with Declarative “Imperative Table”

In the “Declarative with Imperative Table” style, a Table object is declared inline with the declarative class. The @define decorator is applied to the class first, then the registry.mapped() decorator second:

  1. from __future__ import annotations
  2. from typing import List
  3. from typing import Optional
  4. from attrs import define
  5. from sqlalchemy import Column
  6. from sqlalchemy import ForeignKey
  7. from sqlalchemy import Integer
  8. from sqlalchemy import MetaData
  9. from sqlalchemy import String
  10. from sqlalchemy import Table
  11. from sqlalchemy.orm import Mapped
  12. from sqlalchemy.orm import registry
  13. from sqlalchemy.orm import relationship
  14. mapper_registry = registry()
  15. @mapper_registry.mapped
  16. @define(slots=False)
  17. class User:
  18. __table__ = Table(
  19. "user",
  20. mapper_registry.metadata,
  21. Column("id", Integer, primary_key=True),
  22. Column("name", String(50)),
  23. Column("FullName", String(50), key="fullname"),
  24. Column("nickname", String(12)),
  25. )
  26. id: Mapped[int]
  27. name: Mapped[str]
  28. fullname: Mapped[str]
  29. nickname: Mapped[str]
  30. addresses: Mapped[List[Address]]
  31. __mapper_args__ = { # type: ignore
  32. "properties": {
  33. "addresses": relationship("Address"),
  34. }
  35. }
  36. @mapper_registry.mapped
  37. @define(slots=False)
  38. class Address:
  39. __table__ = Table(
  40. "address",
  41. mapper_registry.metadata,
  42. Column("id", Integer, primary_key=True),
  43. Column("user_id", Integer, ForeignKey("user.id")),
  44. Column("email_address", String(50)),
  45. )
  46. id: Mapped[int]
  47. user_id: Mapped[int]
  48. email_address: Mapped[Optional[str]]

Note

The attrs slots=True option, which enables __slots__ on a mapped class, cannot be used with SQLAlchemy mappings without fully implementing alternative attribute instrumentation, as mapped classes normally rely upon direct access to __dict__ for state storage. Behavior is undefined when this option is present.

Mapping attrs with Imperative Mapping

Just as is the case with dataclasses, we can make use of registry.map_imperatively() to map an existing attrs class as well:

  1. from __future__ import annotations
  2. from typing import List
  3. from attrs import define
  4. from sqlalchemy import Column
  5. from sqlalchemy import ForeignKey
  6. from sqlalchemy import Integer
  7. from sqlalchemy import MetaData
  8. from sqlalchemy import String
  9. from sqlalchemy import Table
  10. from sqlalchemy.orm import registry
  11. from sqlalchemy.orm import relationship
  12. mapper_registry = registry()
  13. @define(slots=False)
  14. class User:
  15. id: int
  16. name: str
  17. fullname: str
  18. nickname: str
  19. addresses: List[Address]
  20. @define(slots=False)
  21. class Address:
  22. id: int
  23. user_id: int
  24. email_address: Optional[str]
  25. metadata_obj = MetaData()
  26. user = Table(
  27. "user",
  28. metadata_obj,
  29. Column("id", Integer, primary_key=True),
  30. Column("name", String(50)),
  31. Column("fullname", String(50)),
  32. Column("nickname", String(12)),
  33. )
  34. address = Table(
  35. "address",
  36. metadata_obj,
  37. Column("id", Integer, primary_key=True),
  38. Column("user_id", Integer, ForeignKey("user.id")),
  39. Column("email_address", String(50)),
  40. )
  41. mapper_registry.map_imperatively(
  42. User,
  43. user,
  44. properties={
  45. "addresses": relationship(Address, backref="user", order_by=address.c.id),
  46. },
  47. )
  48. mapper_registry.map_imperatively(Address, address)

The above form is equivalent to the previous example using Declarative with Imperative Table.