edelooff / sqlalchemy-json

Full-featured JSON type with mutation tracking for SQLAlchemy
http://variable-scope.com/posts/mutation-tracking-in-nested-json-structures-using-sqlalchemy
BSD 2-Clause "Simplified" License
189 stars 34 forks source link

sqlalchemy-json ###############

SQLAlchemy-JSON provides mutation-tracked JSON types to SQLAlchemy_:

Examples

Basic change tracking

This is essentially the SQLAlchemy mutable JSON recipe_. We define a simple author model which list the author's name and a property handles for various social media handles used:

.. code-block:: python

class Author(Base):
    name = Column(Text)
    handles = Column(MutableJson)

Or, using the declarative mapping style:

.. code-block:: python

class Category(Base):
    __tablename__ = "categories"

    id = mapped_column(Integer, primary_key=True)
    created_at: Mapped[DateTime] = mapped_column(DateTime, default=datetime.now)
    updated_at: Mapped[DateTime] = mapped_column(
        DateTime, default=datetime.now, onupdate=datetime.now
    )
    keywords: Mapped[list[str]] = mapped_column(MutableJson)

The example below loads one of the existing authors and retrieves the mapping of social media handles. The error in the twitter handle is then corrected and committed. The change is detected by SQLAlchemy and the appropriate UPDATE statement is generated.

.. code-block:: python

>>> author = session.query(Author).first()
>>> author.handles
{'twitter': '@JohnDoe', 'facebook': 'JohnDoe'}
>>> author.handles['twitter'] = '@JDoe'
>>> session.commit()
>>> author.handles
{'twitter': '@JDoe', 'facebook': 'JohnDoe'}

Nested change tracking

The example below defines a simple model for articles. One of the properties on this model is a mutable JSON structure called references which includes a count of links that the article contains, grouped by domain:

.. code-block:: python

class Article(Base):
    author = Column(ForeignKey('author.name'))
    content = Column(Text)
    references = Column(NestedMutableJson)

With this in place, an existing article is loaded and its current references inspected. Following that, the count for one of these is increased by ten, and the session is committed:

.. code-block:: python

>>> article = session.query(Article).first()
>>> article.references
{'github.com': {'edelooff/sqlalchemy-json': 4, 'zzzeek/sqlalchemy': 7}}
>>> article.references['github.com']['edelooff/sqlalchemy-json'] += 10
>>> session.commit()
>>> article.references
{'github.com': {'edelooff/sqlalchemy-json': 14, 'zzzeek/sqlalchemy': 7}}

Had the articles model used MutableJson like in the previous example this code would have failed. This is because the top level dictionary is never altered directly. The nested mutable ensures the change happening at the lower level bubbles up to the outermost container.

Non-native JSON / other serialization types

By default, sqlalchemy-json uses the JSON column type provided by SQLAlchemy (specifically sqlalchemy.types.JSON.) If you wish to use another type (e.g. PostgreSQL's JSONB), your database does not natively support JSON (e.g. versions of SQLite before 3.37.2/), or you wish to serialize to a format other than JSON, you'll need to provide a different backing type.

This is done by using the utility function mutable_json_type. This type creator function accepts two parameters:

.. code-block:: python

import json

from sqlalchemy import JSON, String, TypeDecorator
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy_json import mutable_json_type

class JsonString(TypeDecorator):
    """Enables JSON storage by encoding and decoding on the fly."""

    impl = String

    def process_bind_param(self, value, dialect):
        return json.dumps(value)

    def process_result_value(self, value, dialect):
        return json.loads(value)

postgres_jsonb_mutable = mutable_json_type(dbtype=JSONB)
string_backed_nested_mutable = mutable_json_type(dbtype=JsonString, nested=True)

Dependencies

Development

Here's how to setup your development environment:

.. code-block:: shell

python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
# run tests
pytest

Changelog

0.7.0

0.6.0

0.5.0

0.4.0

0.3.0

0.2.2

0.2.1

0.2.0 (unreleased)

0.1.0 (unreleased)

Initial version. This initially carried a 1.0.0 version number but has never been released on PyPI.

.. _augmented type: https://docs.sqlalchemy.org/en/13/core/custom_types.html#augmenting-existing-types .. _mutable json recipe: http://docs.sqlalchemy.org/en/latest/core/custom_types.html#marshal-json-strings .. _sqlalchemy: https://www.sqlalchemy.org/ .. _sqlalchemy-utils: https://sqlalchemy-utils.readthedocs.io/