Join three models with foreign keys B to A & C to A

Hi!

Using Django 4.2, I have three models looking as the following:

class A(models.Model):
    # [...]

class B(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    a = models.ForeignKey(A, on_delete=models.CASCADE, null=True)
    # [...]
    class Meta:
        unique_together = (("user", "a"), )

class C(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    a = models.ForeignKey(A, on_delete=models.CASCADE, null=True)
    # [...]
    class Meta:
        unique_together = (("user", "a"), )

B.user and C.user are related.

What I want is to join those three models based on the a foreign keys and the user foreign keys so that I can access A, B and C fields with a single QuerySet in my templates.

If possible, I would also want to have B fields equal to null if user exists in C but not in B.

I have tried using prefetch_related in the following way, but it doesn’t seem to let me access B and C fields:

result = A.objects.prefetch_related("b_set", "c_set").filter(b__user=user, c__user=user)

I might need to use a subquery or an annotate(), but I really don’t understand how they work.

Would anyone, please, be willing to help me?

Writing a query on A automatically gives you access to both sets of related objects.

If you have:
all_a = A.objects.all()

Then, for any element of all_a which we’ll name an_a, an_a.b_set.all() is the set of all B related to an_a. Likewise, an_a.c_set.all() is the set of all C related to an_a.

The prefetch_related clause is not required for access to those collections, it’s only a performance enhancement. If you iterate over both all_a and subsequently reference the b_set and c_set collections in a tempate, you’re going to end up in a 2N+1 query situation - which is what prefetch_related prevents.

Now, when it comes to accessing those fields in b_set or c_set, keep in mind that you may have many B (or C) related to a single A.

Side note: If that’s not the case - if there’s only 0 or 1 B that can be related to A - then you’ve designed your model incorrectly - it would be a OneToOneField that is needed, not a Foreign Key.

That means that an_a.b_set.all() is a collection, and you would need to iterate over that collection to access the individual fields of the individual members of b_set.

The other way you may want to look at this is to realize that what you have effectively created are two separate ManyToMany relationships between User and A, with two through models. (I don’t know whether that helps you visualize what’s going on here or not, but sometimes changing your perspective on an issue helps.)

Thank you for your reply.
I think that I understand all of it.

To bring more context, there can be many B and many C related to a single A. I am sure not to need OneToOneField instead of my ForeignKey fields.

I can see that I have access to the set of all B and to the set of all C related to each element of A. But that is not what I am looking for.

I do not want to iterate more than once in my template. That is why I am looking for a solution to obtain a QuerySet with the fields of all three models joined on A and User (which, I forgot to mention, are unique_together in models B and C).

Depending upon what exactly you’re trying to generate, that may not be possible. (It would be helpful if you could provide a minimal example showing just a handful of rows of data along with what you’re trying to achieve. I’m not seeing what the output is that you’re trying to create here.)
Keep in mind that regardless of the unique_together clause, something like B.objects.filter(user=some_user, a=an_a) is going to return a queryset and not an individual instance of B.

I basically want to do a left join and access values of A, B and C through the same QuerySet.

For example, if my models were to be looking like this:

class A(models.Model):
    name = ...
    moderator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    start_date = ...
    end_date = ...

class B(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    a = models.ForeignKey(A, on_delete=models.CASCADE, null=True)
    first_date = ...
    last_date = ...

    class Meta:
        unique_together = (("user", "a"), )

class C(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    a = models.ForeignKey(A, on_delete=models.CASCADE, null=True)

    class Meta:
        unique_together = (("user", "a"), )

With values for these models looking like this:

A id name moderator start_date end_date
1 name_1 55 sdate_1 edate_1
2 name_2 88 sdate_2 edate_2
3 name_3 55 sdate_3 edate_3
B id user a first_date last_date
1 133 1 fdate_1 ldate_1
2 133 2 fdate_2 ldate_2
3 177 1 fdate_3 ldate_3
C id user a
1 133 1
2 133 2
3 177 1
4 177 2
5 177 3

I would want the resulting QuerySet to be structured as if it was retrieving its values from the following table:

ABC id name moderator start_date end_date user a first_date last_date
1 name_1 55 sdate_1 edate_1 133 1 fdate_1 ldate_1
2 name_2 88 sdate_2 edate_2 133 2 fdate_2 ldate_2
3 name_1 55 sdate_1 edate_1 177 1 fdate_3 ldate_3
4 name_2 88 sdate_2 edate_2 177 2 null null
5 name_3 55 sdate_3 edate_3 177 3 null null

Unfortunately, I’m not in a position where I can test this right now, but I’d be trying something like:

all_c = C.objects.all().select_related('a').prefetch_related('a__b')

If you iterate over all_c, then for each c the related fields of A would be c.a.name, c.a.moderator, etc.

The instance of B that you are then looking for would be c.a.b_set.get(user=c.user), allowing you to access the fields as c.a.b_set.get(user=c.user).first_date and c.a.b_set.get(user=c.user).last_date

Rendering this in a template is going to require that you iterate over all_c in the view to collect the data into a single data structure before calling render. You’re not going to be able to do this directly in the template because of the need to pass a parameter to the get method.

Another approach you could take would be to annotate the fields of B on C as a subquery - either as a JSONField or as two separate subqueries, each annotating an individual field. (There’ve been a couple of different discussions here about this. Searching for Subquery is likely going to find some of them.)