File: mapped_attributes.rst

package info (click to toggle)
sqlalchemy 2.0.40%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 26,404 kB
  • sloc: python: 410,002; makefile: 230; sh: 7
file content (369 lines) | stat: -rw-r--r-- 12,668 bytes parent folder | download | duplicates (2)
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
.. _mapping_attributes_toplevel:

.. currentmodule:: sqlalchemy.orm

Changing Attribute Behavior
===========================

This section will discuss features and techniques used to modify the
behavior of ORM mapped attributes, including those mapped with
:func:`_orm.mapped_column`, :func:`_orm.relationship`, and others.

.. _simple_validators:

Simple Validators
-----------------

A quick way to add a "validation" routine to an attribute is to use the
:func:`~sqlalchemy.orm.validates` decorator. An attribute validator can raise
an exception, halting the process of mutating the attribute's value, or can
change the given value into something different. Validators, like all
attribute extensions, are only called by normal userland code; they are not
issued when the ORM is populating the object::

    from sqlalchemy.orm import validates


    class EmailAddress(Base):
        __tablename__ = "address"

        id = mapped_column(Integer, primary_key=True)
        email = mapped_column(String)

        @validates("email")
        def validate_email(self, key, address):
            if "@" not in address:
                raise ValueError("failed simple email validation")
            return address

Validators also receive collection append events, when items are added to a
collection::

    from sqlalchemy.orm import validates


    class User(Base):
        # ...

        addresses = relationship("Address")

        @validates("addresses")
        def validate_address(self, key, address):
            if "@" not in address.email:
                raise ValueError("failed simplified email validation")
            return address

The validation function by default does not get emitted for collection
remove events, as the typical expectation is that a value being discarded
doesn't require validation.  However, :func:`.validates` supports reception
of these events by specifying ``include_removes=True`` to the decorator.  When
this flag is set, the validation function must receive an additional boolean
argument which if ``True`` indicates that the operation is a removal::

    from sqlalchemy.orm import validates


    class User(Base):
        # ...

        addresses = relationship("Address")

        @validates("addresses", include_removes=True)
        def validate_address(self, key, address, is_remove):
            if is_remove:
                raise ValueError("not allowed to remove items from the collection")
            else:
                if "@" not in address.email:
                    raise ValueError("failed simplified email validation")
                return address

The case where mutually dependent validators are linked via a backref
can also be tailored, using the ``include_backrefs=False`` option; this option,
when set to ``False``, prevents a validation function from emitting if the
event occurs as a result of a backref::

    from sqlalchemy.orm import validates


    class User(Base):
        # ...

        addresses = relationship("Address", backref="user")

        @validates("addresses", include_backrefs=False)
        def validate_address(self, key, address):
            if "@" not in address:
                raise ValueError("failed simplified email validation")
            return address

Above, if we were to assign to ``Address.user`` as in ``some_address.user = some_user``,
the ``validate_address()`` function would *not* be emitted, even though an append
occurs to ``some_user.addresses`` - the event is caused by a backref.

Note that the :func:`~.validates` decorator is a convenience function built on
top of attribute events.   An application that requires more control over
configuration of attribute change behavior can make use of this system,
described at :class:`~.AttributeEvents`.

.. autofunction:: validates

Using Custom Datatypes at the Core Level
-----------------------------------------

A non-ORM means of affecting the value of a column in a way that suits
converting data between how it is represented in Python, vs. how it is
represented in the database, can be achieved by using a custom datatype that is
applied to the mapped :class:`_schema.Table` metadata.     This is more common in the
case of some style of encoding / decoding that occurs both as data goes to the
database and as it is returned; read more about this in the Core documentation
at :ref:`types_typedecorator`.


.. _mapper_hybrids:

Using Descriptors and Hybrids
-----------------------------

A more comprehensive way to produce modified behavior for an attribute is to
use :term:`descriptors`.  These are commonly used in Python using the ``property()``
function. The standard SQLAlchemy technique for descriptors is to create a
plain descriptor, and to have it read/write from a mapped attribute with a
different name. Below we illustrate this using Python 2.6-style properties::

    class EmailAddress(Base):
        __tablename__ = "email_address"

        id = mapped_column(Integer, primary_key=True)

        # name the attribute with an underscore,
        # different from the column name
        _email = mapped_column("email", String)

        # then create an ".email" attribute
        # to get/set "._email"
        @property
        def email(self):
            return self._email

        @email.setter
        def email(self, email):
            self._email = email

The approach above will work, but there's more we can add. While our
``EmailAddress`` object will shuttle the value through the ``email``
descriptor and into the ``_email`` mapped attribute, the class level
``EmailAddress.email`` attribute does not have the usual expression semantics
usable with :class:`_sql.Select`. To provide these, we instead use the
:mod:`~sqlalchemy.ext.hybrid` extension as follows::

    from sqlalchemy.ext.hybrid import hybrid_property


    class EmailAddress(Base):
        __tablename__ = "email_address"

        id = mapped_column(Integer, primary_key=True)

        _email = mapped_column("email", String)

        @hybrid_property
        def email(self):
            return self._email

        @email.setter
        def email(self, email):
            self._email = email

The ``.email`` attribute, in addition to providing getter/setter behavior when we have an
instance of ``EmailAddress``, also provides a SQL expression when used at the class level,
that is, from the ``EmailAddress`` class directly:

.. sourcecode:: python+sql

    from sqlalchemy.orm import Session
    from sqlalchemy import select

    session = Session()

    address = session.scalars(
        select(EmailAddress).where(EmailAddress.email == "address@example.com")
    ).one()
    {execsql}SELECT address.email AS address_email, address.id AS address_id
    FROM address
    WHERE address.email = ?
    ('address@example.com',)
    {stop}

    address.email = "otheraddress@example.com"
    session.commit()
    {execsql}UPDATE address SET email=? WHERE address.id = ?
    ('otheraddress@example.com', 1)
    COMMIT
    {stop}

The :class:`~.hybrid_property` also allows us to change the behavior of the
attribute, including defining separate behaviors when the attribute is
accessed at the instance level versus at the class/expression level, using the
:meth:`.hybrid_property.expression` modifier. Such as, if we wanted to add a
host name automatically, we might define two sets of string manipulation
logic::

    class EmailAddress(Base):
        __tablename__ = "email_address"

        id = mapped_column(Integer, primary_key=True)

        _email = mapped_column("email", String)

        @hybrid_property
        def email(self):
            """Return the value of _email up until the last twelve
            characters."""

            return self._email[:-12]

        @email.setter
        def email(self, email):
            """Set the value of _email, tacking on the twelve character
            value @example.com."""

            self._email = email + "@example.com"

        @email.expression
        def email(cls):
            """Produce a SQL expression that represents the value
            of the _email column, minus the last twelve characters."""

            return func.substr(cls._email, 0, func.length(cls._email) - 12)

Above, accessing the ``email`` property of an instance of ``EmailAddress``
will return the value of the ``_email`` attribute, removing or adding the
hostname ``@example.com`` from the value. When we query against the ``email``
attribute, a SQL function is rendered which produces the same effect:

.. sourcecode:: python+sql

    address = session.scalars(
        select(EmailAddress).where(EmailAddress.email == "address")
    ).one()
    {execsql}SELECT address.email AS address_email, address.id AS address_id
    FROM address
    WHERE substr(address.email, ?, length(address.email) - ?) = ?
    (0, 12, 'address')
    {stop}

Read more about Hybrids at :ref:`hybrids_toplevel`.

.. _synonyms:

Synonyms
--------

Synonyms are a mapper-level construct that allow any attribute on a class
to "mirror" another attribute that is mapped.

In the most basic sense, the synonym is an easy way to make a certain
attribute available by an additional name::

    from sqlalchemy.orm import synonym


    class MyClass(Base):
        __tablename__ = "my_table"

        id = mapped_column(Integer, primary_key=True)
        job_status = mapped_column(String(50))

        status = synonym("job_status")

The above class ``MyClass`` has two attributes, ``.job_status`` and
``.status`` that will behave as one attribute, both at the expression
level:

.. sourcecode:: pycon+sql

    >>> print(MyClass.job_status == "some_status")
    {printsql}my_table.job_status = :job_status_1{stop}

    >>> print(MyClass.status == "some_status")
    {printsql}my_table.job_status = :job_status_1{stop}

and at the instance level::

    >>> m1 = MyClass(status="x")
    >>> m1.status, m1.job_status
    ('x', 'x')

    >>> m1.job_status = "y"
    >>> m1.status, m1.job_status
    ('y', 'y')

The :func:`.synonym` can be used for any kind of mapped attribute that
subclasses :class:`.MapperProperty`, including mapped columns and relationships,
as well as synonyms themselves.

Beyond a simple mirror, :func:`.synonym` can also be made to reference
a user-defined :term:`descriptor`.  We can supply our
``status`` synonym with a ``@property``::

    class MyClass(Base):
        __tablename__ = "my_table"

        id = mapped_column(Integer, primary_key=True)
        status = mapped_column(String(50))

        @property
        def job_status(self):
            return "Status: " + self.status

        job_status = synonym("status", descriptor=job_status)

When using Declarative, the above pattern can be expressed more succinctly
using the :func:`.synonym_for` decorator::

    from sqlalchemy.ext.declarative import synonym_for


    class MyClass(Base):
        __tablename__ = "my_table"

        id = mapped_column(Integer, primary_key=True)
        status = mapped_column(String(50))

        @synonym_for("status")
        @property
        def job_status(self):
            return "Status: " + self.status

While the :func:`.synonym` is useful for simple mirroring, the use case
of augmenting attribute behavior with descriptors is better handled in modern
usage using the :ref:`hybrid attribute <mapper_hybrids>` feature, which
is more oriented towards Python descriptors.   Technically, a :func:`.synonym`
can do everything that a :class:`.hybrid_property` can do, as it also supports
injection of custom SQL capabilities, but the hybrid is more straightforward
to use in more complex situations.

.. autofunction:: synonym

.. _custom_comparators:

Operator Customization
----------------------

The "operators" used by the SQLAlchemy ORM and Core expression language
are fully customizable.  For example, the comparison expression
``User.name == 'ed'`` makes usage of an operator built into Python
itself called ``operator.eq`` - the actual SQL construct which SQLAlchemy
associates with such an operator can be modified.  New
operations can be associated with column expressions as well.   The operators
which take place for column expressions are most directly redefined at the
type level -  see the
section :ref:`types_operators` for a description.

ORM level functions like :func:`.column_property`, :func:`_orm.relationship`,
and :func:`.composite` also provide for operator redefinition at the ORM
level, by passing a :class:`.PropComparator` subclass to the ``comparator_factory``
argument of each function.  Customization of operators at this level is a
rare use case.  See the documentation at :class:`.PropComparator`
for an overview.