From 6bbc16f3c1668fd3d571816b7ee7774960d50be8 Mon Sep 17 00:00:00 2001 From: Shafiya Adzhani Date: Sat, 24 Aug 2024 23:18:24 +0700 Subject: [PATCH] Added topic guide for updating JSONField. --- docs/ref/models/database-functions.txt | 3 + docs/ref/models/fields.txt | 3 +- docs/topics/db/queries.txt | 186 +++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index b8c49b1864..7593634e68 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -955,6 +955,9 @@ Usage example: >>> print(user_preferences.settings) {'font': {'name': 'Arial'}} +For more information on how to use the ``JSONSet`` and ``JSONRemove`` +functions, see :ref:`updating-jsonfield`. + .. _math-functions: Math Functions diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 5b0f127c6f..9f8075d8aa 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1443,7 +1443,8 @@ Python native format: dictionaries, lists, strings, numbers, booleans and Defaults to ``json.JSONDecoder``. -To query ``JSONField`` in the database, see :ref:`querying-jsonfield`. +To query and update ``JSONField`` in the database, see :ref:`querying-jsonfield` +and :ref:`updating-jsonfield`. .. admonition:: Default value diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 7e3338eaea..0db1600b53 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -1333,6 +1333,192 @@ For example: >>> Dog.objects.filter(data__has_any_keys=["owner", "breed"]) , ]> +.. _updating-jsonfield: + +Updating ``JSONField`` +====================== + +Updating :class:`~django.db.models.JSONField` can be achieved through various +steps: updating by treating it like dictionary, making use +:class:`~django.db.models.functions.JSONSet` and +:class:`~django.db.models.functions.JSONRemove` functions and saving them +through model instance, and using +:class:`~django.db.models.functions.JSONSet` and +:class:`~django.db.models.functions.JSONRemove` inside +:meth:`~django.db.models.query.QuerySet.update`. + +To demonstrate, we will use the following example model:: + + from django.db import models + + + class UserPreferences(models.Model): + settings = models.JSONField() + +To update a :class:`~django.db.models.JSONField` you can modify it directly, +treating it like a dictionary. Once the desired changes are made, you can save +the updated data by saving the model instance. + +Consider the following example: + +.. code-block:: pycon + + >>> user_preferences = UserPreferences.objects.create( + ... settings={"theme": "dark", "notifications": True} + ... ) + >>> user_preferences.settings["theme"] = "light" + >>> user_preferences.save() + >>> print(user_preferences.settings) + {'theme': 'light', 'notifications': True} + +The process can be made more secure by using +:class:`~django.db.models.functions.JSONSet` to insert or update +specific keys within a :class:`~django.db.models.JSONField`, avoiding race +conditions. Using :class:`~django.db.models.functions.JSONSet`, the previous +example is expressed as: + +.. code-block:: pycon + + >>> from django.db.models.functions import JSONSet + >>> user_preferences = UserPreferences.objects.create( + ... settings={"theme": "dark", "notifications": True} + ... ) + >>> user_preferences.settings = JSONSet("settings", theme="light") + >>> user_preferences.save() + >>> UserPreferences.objects.filter(settings__theme="light") + ]> + +We can also update multiple keys at once. For example, instead of updating +each key individually, we can update several keys at once by passing them +as key-value pairs to the :class:`~django.db.models.functions.JSONSet` +function. + +.. code-block:: pycon + + >>> user_preferences.settings = JSONSet("settings", theme="dark", notifications=False) + >>> user_preferences.save() + >>> UserPreferences.objects.filter( + ... settings__theme="dark", + ... settings__notifications=False, + ... ) + ]> + +To insert or update nested keys, we can use key transforms inside +:class:`~django.db.models.functions.JSONSet`, allowing us to update values at +different levels of the structure without modifying other parts of the field. + +.. code-block:: pycon + + >>> user_preferences.settings = JSONSet( + ... "settings", + ... font__name="Arial", + ... font__size=10, + ... ) # {'theme': 'dark', 'notifications': True, 'font': {'name': 'Arial', 'size': 10}} + >>> user_preferences.save() + >>> UserPreferences.objects.filter( + ... settings__font__name="Arial", + ... settings__font__size=10, + ... ) + ]> + +You can pass ``None`` value inside +:class:`~django.db.models.functions.JSONSet` key to make JSON ``null``. +Keep in mind that this will not remove the key; it will only set its value +to ``null``. + +.. code-block:: pycon + + >>> user_preferences.settings = JSONSet( + ... "settings", + ... font__name="Arial", + ... font__size=None, + ... ) # {'theme': 'dark', 'notifications': True, 'font': {'name': 'Arial', 'size': null}} + >>> user_preferences.save() + >>> UserPreferences.objects.filter( + ... settings__font__name="Arial", + ... settings__font__size=None, + ... ) + ]> + +Django provides :class:`~django.db.models.functions.JSONRemove` to remove +specific key from a :class:`~django.db.models.JSONField`. +Here is an example where we use :class:`~django.db.models.functions.JSONRemove` +to remove data and saving it using the model instance. + +.. code-block:: pycon + + >>> from django.db.models.functions import JSONRemove + >>> user_preferences = UserPreferences.objects.create( + ... settings={ + ... "theme": "dark", + ... "notifications": True, + ... "font": {"size": 10, "name": "Arial"}, + ... } + ... ) + >>> user_preferences.settings = JSONRemove("settings", "theme") + >>> user_preferences.save() + >>> UserPreferences.objects.filter(settings__theme__isnull=True) + ]> + +We can also remove multiple keys from the :class:`~django.db.models.JSONField` +at once, simplifying the process of modifying the JSON structure. +This eliminates the need to remove each key individually, +allowing for more convenient updates when dealing with multiple keys at once. + +.. code-block:: pycon + + >>> user_preferences.settings = JSONRemove("settings", "notifications", "font__size") + >>> user_preferences.save() + >>> UserPreferences.objects.filter( + ... settings__notifications__isnull=True, + ... settings__font__size__isnull=True, + ... ) + ]> + +We can use :class:`~django.db.models.functions.JSONSet` +and :class:`~django.db.models.functions.JSONRemove` inside +:meth:`~django.db.models.query.QuerySet.update` to update +:class:`~django.db.models.JSONField`. To demonstrate using +:class:`~django.db.models.functions.JSONSet` inside +:meth:`~django.db.models.query.QuerySet.update`, take a look at this example. + +.. code-block:: pycon + + >>> from django.db.models.functions import JSONSet + >>> user_preferences = UserPreferences.objects.create( + ... settings={ + ... "font": {"name": "Arial", "size": 10}, + ... "notifications": True, + ... } + ... ) + >>> UserPreferences.objects.update( + ... settings=JSONSet("settings", font__size=20, notifications=False, theme="dark") + ... ) + 1 + >>> user_preferences = UserPreferences.objects.get(pk=user_preferences.pk) + >>> user_preferences.settings + {'font': {'name': 'Arial', 'size': 20}, 'notifications': False, 'theme': 'dark'} + +We can also use :class:`~django.db.models.functions.JSONRemove` inside +:meth:`~django.db.models.query.QuerySet.update`. + +.. code-block:: pycon + + >>> from django.db.models.functions import JSONRemove + >>> user_preferences = UserPreferences.objects.create( + ... settings={ + ... "font": {"name": "Arial", "size": 10}, + ... "notifications": True, + ... } + ... ) + >>> UserPreferences.objects.update( + ... settings=JSONRemove("settings", "font__size", "notifications") + ... ) + 1 + >>> user_preferences = UserPreferences.objects.get(pk=user_preferences.pk) + >>> print(user_preferences.settings) + {'font': {'name': 'Arial'}} + .. _complex-lookups-with-q: Complex lookups with ``Q`` objects