Django ORM JOIN of models that are related through JSONField

Question:

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.

Asked By: Anton M.

||

Answers:

Import json

bar = Bar.objects.get(pk=json.loads(foo.bar)[“bar”])

You would have to parse the json.

Answered By: Arif Rasim

you can annotate the values from the Bar object to the Foo object

from django.db.models import OuterRef, Subquery
b = Bar.objects.filter(id=OuterRef('bar__bar'))
f = Foo.objects.all().annotate(barx=Subquery(b.values('data')[:1]))

for e in f:
    print(e.data, e.barx)

it extracts the value out of the JSON field and looks for a matching pk

Answered By: David Wenzel

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)

Answered By: Anton M.
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.