File: recipes.rst

package info (click to toggle)
python-marshmallow-sqlalchemy 1.4.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 364 kB
  • sloc: python: 1,927; makefile: 13; sh: 8
file content (426 lines) | stat: -rw-r--r-- 12,802 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
.. _recipes:

*******
Recipes
*******


Base ``Schema`` I
=================

A common pattern with marshmallow is to define a base `Schema <marshmallow.Schema>` class which has common configuration and behavior for your application's schemas.

You may want to define a common session object, e.g. a `scoped_session <sqlalchemy.orm.scoping.scoped_session>` to use for all `Schemas <marshmallow.Schema>`.


.. code-block:: python

    # myproject/db.py
    import sqlalchemy as sa
    from sqlalchemy import orm

    Session = orm.scoped_session(orm.sessionmaker())
    Session.configure(bind=engine)

.. code-block:: python

    # myproject/schemas.py

    from marshmallow_sqlalchemy import SQLAlchemySchema

    from .db import Session


    class BaseSchema(SQLAlchemySchema):
        class Meta:
            sqla_session = Session


.. code-block:: python
    :emphasize-lines: 9

    # myproject/users/schemas.py

    from ..schemas import BaseSchema
    from .models import User


    class UserSchema(BaseSchema):
        # Inherit BaseSchema's options
        class Meta(BaseSchema.Meta):
            model = User

Base ``Schema`` II
==================

Here is an alternative way to define a BaseSchema class with a common `Session <sqlalchemy.orm.Session>` object.

.. code-block:: python

    # myproject/schemas.py

    from marshmallow_sqlalchemy import SQLAlchemySchemaOpts, SQLAlchemySchema
    from .db import Session


    class BaseOpts(SQLAlchemySchemaOpts):
        def __init__(self, meta, ordered=False):
            if not hasattr(meta, "sqla_session"):
                meta.sqla_session = Session
            super(BaseOpts, self).__init__(meta, ordered=ordered)


    class BaseSchema(SQLAlchemySchema):
        OPTIONS_CLASS = BaseOpts


This allows you to define class Meta options without having to subclass ``BaseSchema.Meta``.

.. code-block:: python
    :emphasize-lines: 8

    # myproject/users/schemas.py

    from ..schemas import BaseSchema
    from .models import User


    class UserSchema(BaseSchema):
        class Meta:
            model = User

Using `Related <marshmallow_sqlalchemy.fields.Related>` to serialize relationships
==================================================================================

The `Related <marshmallow_sqlalchemy.fields.Related>` field can be used to serialize a
SQLAlchemy `relationship <sqlalchemy.orm.relationship>` as a nested dictionary.

.. code-block:: python
    :emphasize-lines: 34

    import sqlalchemy as sa
    from sqlalchemy.orm import DeclarativeBase, relationship

    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field
    from marshmallow_sqlalchemy.fields import Related


    class Base(DeclarativeBase):
        pass


    class User(Base):
        __tablename__ = "user"
        id = sa.Column(sa.Integer, primary_key=True)
        full_name = sa.Column(sa.String(255))


    class BlogPost(Base):
        __tablename__ = "blog_post"
        id = sa.Column(sa.Integer, primary_key=True)
        title = sa.Column(sa.String(255), nullable=False)

        author_id = sa.Column(sa.Integer, sa.ForeignKey(User.id), nullable=False)
        author = relationship(User)


    class BlogPostSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = BlogPost

        id = auto_field()
        # Blog's author will be serialized as a dictionary with
        # `id` and `name` pulled from the related User.
        author = Related(["id", "full_name"])

Serialization will look like this:

.. code-block:: python

    from pprint import pprint

    from sqlalchemy.orm import sessionmaker

    engine = sa.create_engine("sqlite:///:memory:")
    Session = sessionmaker(engine)

    Base.metadata.create_all(engine)

    with Session() as session:
        user = User(full_name="Freddie Mercury")
        post = BlogPost(title="Bohemian Rhapsody Revisited", author=user)

        session.add_all([user, post])
        session.commit()

        blog_post_schema = BlogPostSchema()
        data = blog_post_schema.dump(post)
        pprint(data, indent=2)
        # { 'author': {'full_name': 'Freddie Mercury', 'id': 1},
        #   'id': 1,
        #   'title': 'Bohemian Rhapsody Revisited'}

Introspecting generated fields
==============================

It is often useful to introspect what fields are generated for a `SQLAlchemyAutoSchema <marshmallow_sqlalchemy.SQLAlchemyAutoSchema>`.

Generated fields are added to a `Schema's` ``_declared_fields`` attribute.

.. code-block:: python

    AuthorSchema._declared_fields["books"]
    # <fields.RelatedList(default=<marshmallow.missing>, ...>


You can also use marshmallow-sqlalchemy's conversion functions directly.


.. code-block:: python

    from marshmallow_sqlalchemy import property2field

    id_prop = Author.__mapper__.attrs.get("id")

    property2field(id_prop)
    # <fields.Integer(default=<marshmallow.missing>, ...>

Overriding generated fields
===========================

Any field generated by a `SQLAlchemyAutoSchema <marshmallow_sqlalchemy.SQLAlchemyAutoSchema>` can be overridden.

.. code-block:: python

    from marshmallow import fields
    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
    from marshmallow_sqlalchemy.fields import Nested


    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author

        # Override books field to use a nested representation rather than pks
        books = Nested(BookSchema, many=True, exclude=("author",))

You can use the `auto_field <marshmallow_sqlalchemy.auto_field>` function to generate a marshmallow `Field <marshmallow.fields.Field>` based on single model property. This is useful for passing additional keyword arguments to the generated field.

.. code-block:: python

    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, field_for


    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author

        # Generate a field, passing in an additional dump_only argument
        date_created = auto_field(dump_only=True)

If a field's external data key differs from the model's column name, you can pass a column name to `auto_field <marshmallow_sqlalchemy.auto_field>`.

.. code-block:: python

    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author
            # Exclude date_created because we're aliasing it below
            exclude = ("date_created",)

        # Generate "created_date" field from "date_created" column
        created_date = auto_field("date_created", dump_only=True)

Automatically generating schemas for SQLAlchemy models
======================================================

It can be tedious to implement a large number of schemas if not overriding any of the generated fields as detailed above. SQLAlchemy has a hook that can be used to trigger the creation of the schemas, assigning them to ``Model.__marshmallow__``.

.. code-block:: python

    from marshmallow_sqlalchemy import ModelConversionError, SQLAlchemyAutoSchema


    def setup_schema(Base, session):
        # Create a function which incorporates the Base and session information
        def setup_schema_fn():
            for class_ in Base._decl_class_registry.values():
                if hasattr(class_, "__tablename__"):
                    if class_.__name__.endswith("Schema"):
                        raise ModelConversionError(
                            "For safety, setup_schema can not be used when a"
                            "Model class ends with 'Schema'"
                        )

                    class Meta(object):
                        model = class_
                        sqla_session = session

                    schema_class_name = "%sSchema" % class_.__name__

                    schema_class = type(
                        schema_class_name, (SQLAlchemyAutoSchema,), {"Meta": Meta}
                    )

                    setattr(class_, "__marshmallow__", schema_class)

        return setup_schema_fn

Usage:

.. code-block:: python

    import sqlalchemy as sa
    from sqlalchemy.orm import declarative_base, sessionmaker
    from sqlalchemy import event
    from sqlalchemy.orm import mapper

    # Either import or declare setup_schema here

    engine = sa.create_engine("sqlite:///:memory:")
    Session = sessionmaker(engine)
    Base = declarative_base()


    class Author(Base):
        __tablename__ = "authors"
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.String)

        def __repr__(self):
            return "<Author(name={self.name!r})>".format(self=self)


    # Listen for the SQLAlchemy event and run setup_schema.
    # Note: This has to be done after Base and session are setup
    event.listen(mapper, "after_configured", setup_schema(Base, session))

    Base.metadata.create_all(engine)

    with Session() as session:
        author = Author(name="Chuck Paluhniuk")
        session.add(author)
        session.commit()

        # Model.__marshmallow__ returns the Class not an instance of the schema
        # so remember to instantiate it
        author_schema = Author.__marshmallow__()

        print(author_schema.dump(author))

This is inspired by functionality from `ColanderAlchemy <https://colanderalchemy.readthedocs.io/en/latest/>`_.

Smart ``Nested`` field
======================

To serialize nested attributes to primary keys unless they are already loaded, you can use this custom field.

.. code-block:: python

    from marshmallow_sqlalchemy.fields import Nested


    class SmartNested(Nested):
        def serialize(self, attr, obj, accessor=None):
            if attr not in obj.__dict__:
                return {"id": int(getattr(obj, attr + "_id"))}
            return super().serialize(attr, obj, accessor)

An example of then using this:

.. code-block:: python

    from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field


    class BookSchema(SQLAlchemySchema):
        id = auto_field()
        author = SmartNested(AuthorSchema)

        class Meta:
            model = Book
            sqla_session = Session


    book = Book(id=1)
    book.author = Author(name="Chuck Paluhniuk")
    session.add(book)
    session.commit()

    book = Book.query.get(1)
    print(BookSchema().dump(book)["author"])
    # {'id': 1}

    book = Book.query.options(joinedload("author")).get(1)
    print(BookSchema().dump(book)["author"])
    # {'id': 1, 'name': 'Chuck Paluhniuk'}

Transient object creation
=========================

Sometimes it might be desirable to deserialize instances that are transient (not attached to a session). In these cases you can specify the `transient` option in the `Meta <marshmallow_sqlalchemy.SQLAlchemySchemaOpts>` class of a `SQLAlchemySchema <marshmallow_sqlalchemy.SQLAlchemySchema>`.


.. code-block:: python

    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema


    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author
            load_instance = True
            transient = True


    dump_data = {"id": 1, "name": "John Steinbeck"}
    print(AuthorSchema().load(dump_data))
    # <Author(name='John Steinbeck')>

You may also explicitly specify an override by passing the same argument to `load <marshmallow_sqlalchemy.SQLAlchemySchema.load>`.

.. code-block:: python

    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema


    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author
            sqla_session = Session
            load_instance = True


    dump_data = {"id": 1, "name": "John Steinbeck"}
    print(AuthorSchema().load(dump_data, transient=True))
    # <Author(name='John Steinbeck')>

Note that transience propagates to relationships (i.e. auto-generated schemas for nested items will also be transient).


.. seealso::

    See `State Management <https://docs.sqlalchemy.org/en/latest/orm/session_state_management.html>`_ to understand session state management.

Controlling instance loading
============================

You can override the schema ``load_instance`` flag by passing in a ``load_instance`` argument when creating the schema instance. Use this to switch between loading to a dictionary or to a model instance:

.. code-block:: python

    from marshmallow_sqlalchemy import SQLAlchemyAutoSchema


    class AuthorSchema(SQLAlchemyAutoSchema):
        class Meta:
            model = Author
            sqla_session = Session
            load_instance = True


    dump_data = {"id": 1, "name": "John Steinbeck"}
    print(AuthorSchema().load(dump_data))  # loading an instance
    # <Author(name='John Steinbeck')>
    print(AuthorSchema(load_instance=False).load(dump_data))  # loading a dict
    # {"id": 1, "name": "John Steinbeck"}