Search code examples
pythondjangodjango-queryset

update_or_create with django JSONField


I use Django JSONField to save custom objects (that are callable) and i am confused by update_or_create. My custom objects could be realized by nesting enough dictionarys but that is a pain to work with. Thats why i want to use custom objects. I didn't want to use a Django Model for that object, as i worry about the amount of database requests.

My Minimum example:models.py

from django.db import models
import json
# Create your models here.
class F:
    def __init__(self, val) -> None:
        self.val = val
    def __call__(self):
        return "call_return_string"
    def to_serializable(self) -> dict:
        return {"class": "F", "val": self.val}

    @classmethod
    def from_serialized(classtype, dic):
        assert "class" in dic.keys()
        assert dic["class"] == "F"
        return F(dic["val"])

class FEncoder(json.JSONEncoder):
    def default(self, f: F) -> dict:
        return f.to_serializable()

class FDecoder(json.JSONDecoder):
    def F_obj_hook(self,dic:dict):
        if "class" in dic.keys():
            if dic["class"] == "F":
                return F.from_serialized(dic)
        return dic
    def __init__(self,*args,**kwargs):
        super().__init__(object_hook=self.F_obj_hook,*args,**kwargs)


class Tester(models.Model):
    id = models.BigAutoField(primary_key=True)
    data= models.JSONField(default=dict,encoder=FEncoder,decoder=FDecoder)

Let f = F("1"). Now when i run any of these:

#Tester.objects.create(data=f) #works as expected
#Tester.objects.filter(id=1).update(data=f)#works as expected
#Tester.objects.get(data=f)#works as expected
#Tester.objects.update_or_create(data=f,defaults={"data":f}) #not as expected

The first will create a database entry where the data column is filled with {"class": "F", "val": "1"}.

The second would update an existing entry, so that the data column is filled with {"class": "F", "val": "1"}.

The third gets all the database entrys where the data column is filled with {"class": "F", "val": "1"}.

But the fourth will always write a database entry where the data column is filled with "call_return_string".

It is finding the correct entrys (it will throw an Exception if there are atleast 2 rows with data column {"class": "F", "val": "1"}). But it seems like it is creating a new entry where it gets the value for the data column by evaluating f?

How can i use update_or_create so that the expected will happen (update the data colum to {"class": "F", "val": "1"} but without having to manually put in F.to_serializable())


Solution

  • It says in the Api reference Link that

    For a detailed description of how names passed in kwargs are resolved, see get_or_create()
    

    There some pseudocode is given for get_or_create()

    params = {k: v for k, v in kwargs.items() if "__" not in k}
    params.update({k: v() if callable(v) else v for k, v in defaults.items()})
    obj = self.model(**params)
    obj.save()
    

    We see that v is called and there seems no way to prevent this. We can use something like this to write our own update_or_create via using the "create" function which doesn't seem to call. Use filter in combination with Tester.objects.create() and the update() function with save() to mimic the behaviour you want