File: dataclasses.rst

package info (click to toggle)
sqlalchemy 1.4.46%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 22,444 kB
  • sloc: python: 341,434; ansic: 1,760; makefile: 226; xml: 17; sh: 7
file content (517 lines) | stat: -rw-r--r-- 18,346 bytes parent folder | download
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
.. _orm_dataclasses_toplevel:

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

SQLAlchemy 1.4 has limited support for ORM mappings that are established
against classes that have already been pre-instrumented using either Python's
built-in dataclasses_ library or the attrs_ third party integration library.

.. tip::  SQLAlchemy 2.0 will include a new dataclass integration feature which
   allows for a particular class to be mapped and converted into a Python
   dataclass simultaneously, with full support for SQLAlchemy's declarative
   syntax.  Within the scope of the 1.4 release, the ``@dataclass`` decorator
   is used separately as documented in this section.

.. _orm_declarative_dataclasses:

Applying ORM Mappings to an existing dataclass
----------------------------------------------

The dataclasses_ module, added in Python 3.7, provides a ``@dataclass`` class
decorator to automatically generate boilerplate definitions of common object
methods including ``__init__()``, ``__repr()__``, and other methods. SQLAlchemy
supports the application of ORM mappings to a class after it has been processed
with the ``@dataclass`` decorator, by using either the
:meth:`_orm.registry.mapped` class decorator, or the
:meth:`_orm.registry.map_imperatively` method to apply ORM mappings to the
class using Imperative.

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

* 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 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 :func:`_orm.mapper` construction function. An alternative to this
approach is in the next example.

.. _orm_declarative_dataclasses_declarative_table:

Mapping dataclasses using Declarative Mapping
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

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
: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 :func:`_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_dataclasses_mixin:

Using Declarative Mixins with 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_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 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]

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



.. _dataclasses: https://docs.python.org/3/library/dataclasses.html
.. _attrs: https://pypi.org/project/attrs/