summaryrefslogtreecommitdiff
path: root/doc/build/orm/dataclasses.rst
blob: bbd05fcd5e508d9404d3ceeb9fa2592b726c5763 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
.. _orm_dataclasses_toplevel:

======================================
Integration with dataclasses and attrs
======================================

SQLAlchemy as of version 2.0 features "native dataclass" integration where
an :ref:`Annotated Declarative Table <orm_declarative_mapped_column>`
mapping may be turned into a Python dataclass_ by adding a single mixin
or decorator to mapped classes.

.. versionadded:: 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.

.. _orm_declarative_native_dataclasses:

Declarative Dataclass Mapping
-------------------------------

SQLAlchemy :ref:`Annotated Declarative Table <orm_declarative_mapped_column>`
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 :term:`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 **April 4, 2023** is
   limited and is currently known to be supported by Pyright_ as well
   as Mypy_ as of **version 1.2**.  Note that Mypy 1.1.1 introduced
   :pep:`681` support but did not correctly accommodate Python descriptors
   which will lead to errors when using SQLAlhcemy's ORM mapping scheme.

   .. seealso::

      https://peps.python.org/pep-0681/#the-dataclass-transform-decorator - background
      on how libraries like SQLAlchemy enable :pep:`681` support


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

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


    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]

Or may be applied directly to classes that extend from the 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(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]

When using the decorator form, only the :meth:`_orm.registry.mapped_as_dataclass`
decorator is supported::

    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]

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 :class:`_orm.MappedAsDataclass`,
class configuration arguments are passed as class-level parameters::

    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]

When using the decorator form with :meth:`_orm.registry.mapped_as_dataclass`,
class configuration arguments are passed to the decorator directly::

    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]

For background on dataclass class options, see the dataclasses_ documentation
at `@dataclasses.dataclass <https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass>`_.

Attribute Configuration
^^^^^^^^^^^^^^^^^^^^^^^

SQLAlchemy native dataclasses differ from normal dataclasses in that
attributes to be mapped are described using the :class:`_orm.Mapped`
generic annotation container in all cases.    Mappings follow the same
forms as those documented at :ref:`orm_declarative_table`, and all
features of :func:`_orm.mapped_column` and :class:`_orm.Mapped` are supported.

Additionally, ORM attribute configuration constructs including
:func:`_orm.mapped_column`, :func:`_orm.relationship` and :func:`_orm.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:

* ``init``, as in :paramref:`_orm.mapped_column.init`,
  :paramref:`_orm.relationship.init`, if False indicates the field should
  not be part of the ``__init__()`` method
* ``default``, as in :paramref:`_orm.mapped_column.default`,
  :paramref:`_orm.relationship.default`
  indicates a default value for the field as given as a keyword argument
  in the ``__init__()`` method.
* ``default_factory``, as in :paramref:`_orm.mapped_column.default_factory`,
  :paramref:`_orm.relationship.default_factory`, indicates a callable function
  that will be invoked to generate a new default value for a parameter
  if not passed explicitly to the ``__init__()`` method.
* ``repr`` True by default, indicates the field should be part of the generated
  ``__repr__()`` method


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 :func:`_orm.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::

    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")

Column Defaults
~~~~~~~~~~~~~~~

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

    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
        )

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

.. sourcecode:: pycon+sql

    >>> 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



Integration with Annotated
~~~~~~~~~~~~~~~~~~~~~~~~~~

The approach introduced at :ref:`orm_declarative_mapped_column_pep593` illustrates
how to use :pep:`593` ``Annotated`` objects to package whole
:func:`_orm.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 :func:`_orm.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::

    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()

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

    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()

.. _orm_declarative_dc_mixins:

Using mixins and abstract superclasses
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Any mixins or base classes that are used in a :class:`_orm.MappedAsDataclass`
mapped class which include :class:`_orm.Mapped` attributes must themselves be
part of a :class:`_orm.MappedAsDataclass`
hierarchy, such as in the example below using a mixin::


    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 type checkers which support :pep:`681` will otherwise not consider
attributes from non-dataclass mixins to be part of the dataclass.

.. deprecated:: 2.0.8  Using mixins and abstract bases within
   :class:`_orm.MappedAsDataclass` or
   :meth:`_orm.registry.mapped_as_dataclass` hierarchies which are not
   themselves dataclasses is deprecated, as these fields are not supported
   by :pep:`681` as belonging to the dataclass.  A warning is emitted for this
   case which will later be an error.

   .. seealso::

       :ref:`error_dcmx` - background on rationale




Relationship Configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^

The :class:`_orm.Mapped` annotation in combination with
:func:`_orm.relationship` is used in the same way as described at
:ref:`relationship_patterns`.    When specifying a collection-based
:func:`_orm.relationship` as an optional keyword argument, the
:paramref:`_orm.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
:paramref:`_orm.relationship.default` if the default value is to be ``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)

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 :paramref:`_orm.relationship.default_factory` can be automatically
derived from the given collection class of the :func:`_orm.relationship`
itself, this would break compatibility with dataclasses, as the presence
of :paramref:`_orm.relationship.default_factory` or
:paramref:`_orm.relationship.default` is what determines if the parameter is
to be required or optional when rendered into the ``__init__()`` method.

.. _orm_declarative_native_dataclasses_non_mapped_fields:

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 :class:`.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::


    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

Instance of ``Data`` above can be created as::

    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 <https://pypi.org/project/bcrypt/>`_ or
`argon2-cffi <https://pypi.org/project/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)

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::

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

.. versionchanged:: 2.0.0rc1  When using :meth:`_orm.registry.mapped_as_dataclass`
   or :class:`.MappedAsDataclass`, fields that do not include the
   :class:`.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.

.. _dataclasses_pydantic:

Integrating with Alternate Dataclass Providers such as Pydantic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

SQLAlchemy's :class:`_orm.MappedAsDataclass` class
and :meth:`_orm.registry.mapped_as_dataclass` method call directly into
the Python standard library ``dataclasses.dataclass`` class decorator, after
the declarative mapping process has been applied to the class.  This
function call may be swapped out for alternateive dataclasses providers,
such as that of Pydantic, using the ``dataclass_callable`` parameter
accepted by :class:`_orm.MappedAsDataclass` as a class keyword argument
as well as by :meth:`_orm.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]

The above ``User`` class will be applied as a dataclass, using Pydantic's
``pydantic.dataclasses.dataclasses`` callable.     The process is available
both for mapped classes as well as mixins that extend from
:class:`_orm.MappedAsDataclass` or which have
:meth:`_orm.registry.mapped_as_dataclass` applied directly.

.. versionadded:: 2.0.4 Added the ``dataclass_callable`` class and method
   parameters for :class:`_orm.MappedAsDataclass` and
   :meth:`_orm.registry.mapped_as_dataclass`, and adjusted some of the
   dataclass internals to accommodate more strict dataclass functions such as
   that of Pydantic.


.. _orm_declarative_dataclasses:

Applying ORM Mappings to an existing dataclass (legacy dataclass use)
---------------------------------------------------------------------

.. legacy::

   The approaches described here are superseded by
   the :ref:`orm_declarative_native_dataclasses` feature new in the 2.0
   series of SQLAlchemy.  This newer version of the feature builds upon
   the dataclass support first added in version 1.4, which is described
   in this section.

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

* Using "Declarative with Imperative Table", the table / column to be mapped
  is defined using a :class:`_schema.Table` object assigned to the
  ``__table__`` attribute of the class; relationships are defined within
  ``__mapper_args__`` dictionary.  The class is mapped using the
  :meth:`_orm.registry.mapped` decorator.   An example is below at
  :ref:`orm_declarative_dataclasses_imperative_table`.

* Using full "Declarative", the Declarative-interpreted directives such as
  :class:`_schema.Column`, :func:`_orm.relationship` are added to the
  ``.metadata`` dictionary of the ``dataclasses.field()`` construct, where
  they are consumed by the declarative process.  The class is again
  mapped using the :meth:`_orm.registry.mapped` decorator.  See the example
  below at :ref:`orm_declarative_dataclasses_declarative_table`.

* An "Imperative" mapping can be applied to an existing dataclass using
  the :meth:`_orm.registry.map_imperatively` method to produce the mapping
  in exactly the same way as described at :ref:`orm_imperative_mapping`.
  This is illustrated below at :ref:`orm_imperative_dataclasses`.

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.

.. _orm_declarative_dataclasses_imperative_table:

Mapping pre-existing dataclasses using Declarative With Imperative Table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

An example of a mapping using ``@dataclass`` using
:ref:`orm_imperative_table_configuration` is below. A complete
:class:`_schema.Table` object is constructed explicitly and assigned to the
``__table__`` attribute. Instance fields are defined using normal dataclass
syntaxes. Additional :class:`.MapperProperty`
definitions such as :func:`.relationship`, are placed in the
:ref:`__mapper_args__ <orm_declarative_mapper_options>` class-level
dictionary underneath the ``properties`` key, corresponding to the
:paramref:`_orm.Mapper.properties` parameter::

    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

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
:class:`.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 :func:`_orm.relationship` to be declared separately, it needs to be
specified directly within the :paramref:`_orm.Mapper.properties` dictionary
which itself is specified within the ``__mapper_args__`` dictionary, so that it
is passed to the constructor for :class:`_orm.Mapper`. An alternative to this
approach is in the next example.

.. _orm_declarative_dataclasses_declarative_table:

Mapping pre-existing dataclasses using Declarative-style fields
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. legacy:: 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 :ref:`orm_declarative_native_dataclasses`.

   Note that **mapped_column() is not supported with this use**;
   the :class:`_schema.Column` construct should continue to be used to declare
   table metadata within the ``metadata`` field of ``dataclasses.field()``.

The fully declarative approach requires that :class:`_schema.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 :func:`_orm.relationship`
association::


    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))})

.. _orm_declarative_dataclasses_mixin:

Using Declarative Mixins with pre-existing dataclasses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In the section :ref:`orm_mixins_toplevel`, 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 :class:`_orm.declared_attr` decorator, such as in the
example at :ref:`orm_declarative_mixins_relationships`::

    class RefTargetMixin:
        @declared_attr
        def target_id(cls):
            return Column("target_id", ForeignKey("target.id"))

        @declared_attr
        def target(cls):
            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 :func:`_orm.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::

    @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

.. versionadded:: 1.4.2  Added support for "declared attr" style mixin attributes,
   namely :func:`_orm.relationship` constructs as well as :class:`_schema.Column`
   objects with foreign key declarations, to be used within "Dataclasses
   with Declarative Table" style mappings.



.. _orm_imperative_dataclasses:

Mapping pre-existing 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
:meth:`_orm.registry.mapped` decorator in order to apply declarative-style
mapping to the class. As an alternative to using the
:meth:`_orm.registry.mapped` decorator, we may also pass the class through the
:meth:`_orm.registry.map_imperatively` method instead, so that we may pass all
:class:`_schema.Table` and :class:`_orm.Mapper` configuration imperatively to
the function rather than having them defined on the class itself as class
variables::

    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_declarative_attrs_imperative_table:

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
:ref:`orm_declarative_dataclasses_declarative_table` and
:ref:`orm_declarative_dataclasses_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 :class:`_schema.Table`
object is declared inline with the declarative class.   The
``@define`` decorator is applied to the class first, then the
:meth:`_orm.registry.mapped` decorator second::

    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]]

.. note:: The ``attrs`` ``slots=True`` option, which enables ``__slots__`` on
   a mapped class, cannot be used with SQLAlchemy mappings without fully
   implementing alternative
   :ref:`attribute instrumentation <examples_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
:meth:`_orm.registry.map_imperatively` to map an existing ``attrs`` class
as well::

    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)

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



.. _dataclass: https://docs.python.org/3/library/dataclasses.html
.. _dataclasses: https://docs.python.org/3/library/dataclasses.html
.. _attrs: https://pypi.org/project/attrs/
.. _mypy: https://mypy.readthedocs.io/en/stable/
.. _pyright: https://github.com/microsoft/pyright