@@ -52,7 +52,8 @@ decorator.
5252Dataclass conversion may be added to any Declarative class either by adding the
5353:class: `_orm.MappedAsDataclass ` mixin to a :class: `_orm.DeclarativeBase ` class
5454hierarchy, or for decorator mapping by using the
55- :meth: `_orm.registry.mapped_as_dataclass ` class decorator.
55+ :meth: `_orm.registry.mapped_as_dataclass ` class decorator or its
56+ functional variant :func: `_orm.mapped_as_dataclass `.
5657
5758The :class: `_orm.MappedAsDataclass ` mixin may be applied either
5859to the Declarative ``Base `` class or any superclass, as in the example
@@ -231,13 +232,14 @@ and ``fullname`` is optional. The ``id`` field, which we expect to be
231232database-generated, is not part of the constructor at all::
232233
233234 from sqlalchemy.orm import Mapped
235+ from sqlalchemy.orm import mapped_as_dataclass
234236 from sqlalchemy.orm import mapped_column
235237 from sqlalchemy.orm import registry
236238
237239 reg = registry()
238240
239241
240- @reg. mapped_as_dataclass
242+ @mapped_as_dataclass(reg)
241243 class User:
242244 __tablename__ = "user_account"
243245
@@ -268,13 +270,14 @@ but where the parameter is optional in the constructor::
268270
269271 from sqlalchemy import func
270272 from sqlalchemy.orm import Mapped
273+ from sqlalchemy.orm import mapped_as_dataclass
271274 from sqlalchemy.orm import mapped_column
272275 from sqlalchemy.orm import registry
273276
274277 reg = registry()
275278
276279
277- @reg. mapped_as_dataclass
280+ @mapped_as_dataclass(reg)
278281 class User:
279282 __tablename__ = "user_account"
280283
@@ -323,6 +326,7 @@ emit a deprecation warning::
323326 from typing import Annotated
324327
325328 from sqlalchemy.orm import Mapped
329+ from sqlalchemy.orm import mapped_as_dataclass
326330 from sqlalchemy.orm import mapped_column
327331 from sqlalchemy.orm import registry
328332
@@ -332,7 +336,7 @@ emit a deprecation warning::
332336 reg = registry()
333337
334338
335- @reg. mapped_as_dataclass
339+ @mapped_as_dataclass(reg)
336340 class User:
337341 __tablename__ = "user_account"
338342 id: Mapped[intpk]
@@ -348,6 +352,7 @@ the other arguments can remain within the ``Annotated`` construct::
348352 from typing import Annotated
349353
350354 from sqlalchemy.orm import Mapped
355+ from sqlalchemy.orm import mapped_as_dataclass
351356 from sqlalchemy.orm import mapped_column
352357 from sqlalchemy.orm import registry
353358
@@ -356,7 +361,7 @@ the other arguments can remain within the ``Annotated`` construct::
356361 reg = registry()
357362
358363
359- @reg. mapped_as_dataclass
364+ @mapped_as_dataclass(reg)
360365 class User:
361366 __tablename__ = "user_account"
362367
@@ -371,15 +376,19 @@ the other arguments can remain within the ``Annotated`` construct::
371376Using mixins and abstract superclasses
372377^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
373378
374- Any mixins or base classes that are used in a :class: `_orm.MappedAsDataclass `
375- mapped class which include :class: `_orm.Mapped ` attributes must themselves be
376- part of a :class: `_orm.MappedAsDataclass `
377- hierarchy, such as in the example below using a mixin::
379+ Mixin and abstract superclass are supported with the Declarative Dataclass
380+ Mapping by defining classes that are part of the :class: `_orm.MappedAsDataclass `
381+ hierarchy, either without including a declarative base or by setting
382+ ``__abstract__ = True ``. The example below illustrates a class ``Mixin `` that is
383+ not itself mapped, but serves as part of the base for a mapped class::
384+
385+ from sqlalchemy.orm import DeclarativeBase
386+ from sqlalchemy.orm import MappedAsDataclass
378387
379388
380389 class Mixin(MappedAsDataclass):
381390 create_user: Mapped[int] = mapped_column()
382- update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False )
391+ update_user: Mapped[Optional[int]] = mapped_column(default=None)
383392
384393
385394 class Base(DeclarativeBase, MappedAsDataclass):
@@ -395,21 +404,77 @@ hierarchy, such as in the example below using a mixin::
395404 username: Mapped[str] = mapped_column()
396405 email: Mapped[str] = mapped_column()
397406
398- Python type checkers which support :pep: `681 ` will otherwise not consider
399- attributes from non-dataclass mixins to be part of the dataclass.
407+ .. tip ::
400408
401- .. deprecated :: 2.0.8 Using mixins and abstract bases within
402- :class: `_orm.MappedAsDataclass ` or
403- :meth: `_orm.registry.mapped_as_dataclass ` hierarchies which are not
404- themselves dataclasses is deprecated, as these fields are not supported
405- by :pep: `681 ` as belonging to the dataclass. A warning is emitted for this
406- case which will later be an error.
409+ When using :class: `_orm.MappedAsDataclass ` without a declarative base in
410+ the hiearchy, the target class is still turned into a real Python dataclass,
411+ so that it may properly serve as a base for a mapped dataclass. Using
412+ :class: `_orm.MappedAsDataclass ` (or the :func: `_orm.unmapped_dataclass ` decorator
413+ described later in this section) is required in order for the class to be correctly
414+ recognized by type checkers as SQLAlchemy-enabled dataclasses. Declarative
415+ itself will reject mixins / abstract classes that are not themselves
416+ Declarative Dataclasses (e.g. they can't be plain classes nor can they be
417+ plain ``@dataclass `` classes).
407418
408- .. seealso ::
419+ .. seealso ::
409420
410- :ref: `error_dcmx ` - background on rationale
421+ :ref: `error_dcmx ` - further background
411422
423+ Another example, where an abstract base combines :class: `_orm.MappedAsDataclass `
424+ with ``__abstract__ = True ``::
412425
426+ from sqlalchemy.orm import DeclarativeBase
427+ from sqlalchemy.orm import MappedAsDataclass
428+
429+
430+ class Base(DeclarativeBase, MappedAsDataclass):
431+ pass
432+
433+
434+ class AbstractUser(Base):
435+ __abstract__ = True
436+
437+ create_user: Mapped[int] = mapped_column()
438+ update_user: Mapped[Optional[int]] = mapped_column(default=None)
439+
440+
441+ class User(AbstractUser):
442+ __tablename__ = "sys_user"
443+
444+ uid: Mapped[str] = mapped_column(
445+ String(50), init=False, default_factory=uuid4, primary_key=True
446+ )
447+ username: Mapped[str] = mapped_column()
448+ email: Mapped[str] = mapped_column()
449+
450+ Finally, for a hierarchy that's based on use of the :func: `_orm.mapped_as_dataclass `
451+ decorator, mixins may be defined using the :func: `_orm.unmapped_dataclass ` decorator::
452+
453+ from sqlalchemy.orm import registry
454+ from sqlalchemy.orm import mapped_as_dataclass
455+ from sqlalchemy.orm import unmapped_dataclass
456+
457+
458+ @unmapped_dataclass()
459+ class Mixin:
460+ create_user: Mapped[int] = mapped_column()
461+ update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)
462+
463+
464+ reg = registry()
465+
466+
467+ @mapped_as_dataclass(reg)
468+ class User(Mixin):
469+ __tablename__ = "sys_user"
470+
471+ uid: Mapped[str] = mapped_column(
472+ String(50), init=False, default_factory=uuid4, primary_key=True
473+ )
474+ username: Mapped[str] = mapped_column()
475+ email: Mapped[str] = mapped_column()
476+
477+ .. versionadded :: 2.1 Added :func:`_orm.unmapped_dataclass`
413478
414479.. _orm_declarative_dc_relationships :
415480
@@ -429,14 +494,15 @@ scalar object references may make use of
429494
430495 from sqlalchemy import ForeignKey
431496 from sqlalchemy.orm import Mapped
497+ from sqlalchemy.orm import mapped_as_dataclass
432498 from sqlalchemy.orm import mapped_column
433499 from sqlalchemy.orm import registry
434500 from sqlalchemy.orm import relationship
435501
436502 reg = registry()
437503
438504
439- @reg. mapped_as_dataclass
505+ @mapped_as_dataclass(reg)
440506 class Parent:
441507 __tablename__ = "parent"
442508 id: Mapped[int] = mapped_column(primary_key=True)
@@ -445,7 +511,7 @@ scalar object references may make use of
445511 )
446512
447513
448- @reg. mapped_as_dataclass
514+ @mapped_as_dataclass(reg)
449515 class Child:
450516 __tablename__ = "child"
451517 id: Mapped[int] = mapped_column(primary_key=True)
@@ -478,13 +544,14 @@ of the object, but will not be persisted by the ORM::
478544
479545
480546 from sqlalchemy.orm import Mapped
547+ from sqlalchemy.orm import mapped_as_dataclass
481548 from sqlalchemy.orm import mapped_column
482549 from sqlalchemy.orm import registry
483550
484551 reg = registry()
485552
486553
487- @reg. mapped_as_dataclass
554+ @mapped_as_dataclass(reg)
488555 class Data:
489556 __tablename__ = "data"
490557
@@ -513,13 +580,14 @@ function, such as `bcrypt <https://pypi.org/project/bcrypt/>`_ or
513580 from typing import Optional
514581
515582 from sqlalchemy.orm import Mapped
583+ from sqlalchemy.orm import mapped_as_dataclass
516584 from sqlalchemy.orm import mapped_column
517585 from sqlalchemy.orm import registry
518586
519587 reg = registry()
520588
521589
522- @reg. mapped_as_dataclass
590+ @mapped_as_dataclass(reg)
523591 class User:
524592 __tablename__ = "user_account"
525593
@@ -571,7 +639,8 @@ Integrating with Alternate Dataclass Providers such as Pydantic
571639 details which **explicitly resolve ** these incompatibilities.
572640
573641SQLAlchemy's :class: `_orm.MappedAsDataclass ` class
574- and :meth: `_orm.registry.mapped_as_dataclass ` method call directly into
642+ :meth: `_orm.registry.mapped_as_dataclass ` method, and
643+ :func: `_orm.mapped_as_dataclass ` functions call directly into
575644the Python standard library ``dataclasses.dataclass `` class decorator, after
576645the declarative mapping process has been applied to the class. This
577646function call may be swapped out for alternateive dataclasses providers,
0 commit comments