Search code examples
pythondjangoormdjango-orm

Django ORM JOIN of models that are related through JSONField


If I have 2 models related through ForeignKey I can easily get a queryset with both models joined using select_related

class Foo(Model):
    data = IntegerField()
    bar = ForeignKey('Bar', on_delete=CASCADE)

class Bar(Model):
    data = IntegerField()


foos_with_joined_bar = Foo.objects.select_related('bar')
for foo in foos_with_joined_bar:
    print(foo.data, foo.bar.data)  # this will not cause extra db queries

I want to do the same thing but in the case where Foo keeps its reference to bar in a JSONField

class Foo(Model):
    data = IntegerField()
    bar = JSONField()  # here can be something like {"bar": 1} where 1 is the ID of Bar

class Bar(Model):
    data = IntegerField()

foos_with_joined_bar = ???

Is it possible to get foos_with_joined_bar in this case using Django ORM?

P.S. We're not discussing the reasoning behind storing foreign keys in the JSONField, of course it's better to just use ForeignKey.


Solution

  • Finally I've found a bit tricky way to annotate Foo with complete Bar model.

    TLDR: use JSONObject to annotate Foo with dict representation of Bar and then manually create foo.bar using this dict

    managers.py

    from django.db.models import QuerySet, F
    from django.db.models.functions import JSONObject, Cast
    from django.db.models.fields import BigIntegerField
    
    
    class BarQS(QuerySet):
        def as_json(self):
            return self.values(json=JSONObject(**{f: f for f in self.model.json_fields}))
    
    
    class FooQS(QuerySet):
        def annotate_bar(self):
            from .models import Bar
    
            return (
                self
                .annotate(bar_id=Cast(F('bar__bar'), BigIntegerField()))
                .annotate(bar=Bar.objects.filter(pk=OuterRef('bar_id')).as_json())
            )
    
        def foobar_iter(self):
            from .models import Bar
    
            for foo in self:
                bar_model = Bar(**foo.bar)
                foo.bar = bar_model
                yield foo
    
    

    models.py

    from django.db.models import Model, IntegerField, JSONField
    from .managers import FooQS, BarQS
    
    
    class Foo(Model):
        data = IntegerField()
        bar = JSONField()
    
        objects = FooQS.as_manager()
    
    
    class Bar(Model):
        data = IntegerField()
        data2 = IntegerField()
    
        objects = BarQS.as_manager()
        json_fields = ('data', 'data2')  # here we define fields translated into JSON
    

    It's done, now we can do something like this

    foos_with_joined_bar = Foo.objects.annotate_bar().foobar_iter()
    
    for foo in foos_with_joined_bar:
        print(foo.data, foo.bar.data, foo.bar.data2)