Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple object types references in Django

We are currently running with the following configuration to avoid other issues.
So for the question: let's assume that this is a must and we can not change the Models part.

At the beginning we had the following models:

class A(Model):
    b = ForeignKey(B)
    ... set of fields ...

class B(Model):
    ...

Then we added something like this:

class AVer2(Model):
    b = ForeignKey(B)
    ... ANOTHER set of fields ...

Assuming an object of type B can only be referenced by either A or AVer2 but never both:

Is there a way to run a query on B that will return, at runtime, the correct object type that references it, in the query result (and the query has both types in it)?

You can assume that an object of type B holds the information regarding who's referencing it.

I am trying to avoid costly whole-system code changes for this.

EDIT: Apparently, my question was not clear. So I will try to explain it better. The answers I got were great but apparently I missed a key point in my question so here it is. Assuming I have the model B from above, and I get some objects:

b_filter = B.objects.filter(some_of_them_have_this_true=True)

Now, I want to get a field that is in both A and AVer2 with one filter into one values list. So for example, I want to get a field named "MyVal" (both A and AVer2 have it) I don't care what is the actual type. So I want to write something like:

b_filter.values(['a__myval', 'aver2__myval'])

and get something like the following in return: [{'myval': }] Instead, I currently get [{'a__myval': , 'aver2__myval': None}]

I hope it is clearer.

Thanks!

like image 785
Tal Avatar asked Apr 23 '17 14:04

Tal


1 Answers

Short answer: You can not make your exact need.

Long answer: The first thing that came to my mind when I read your question is Content Types, Generic Foreign Keys and Generic Relations

Whether you will use "normal" foreign keys or "generic foreign keys" (combined with Generic Relation), Your B instances will have both A field and AVer2 field and this natural thing make life easier and make your goal (B instance has a single Field that may be A or Avr2) unreachable. And here you should also override the B model save method to force it to have only the A field and the Avr2 to be None or A to be None and Avr2 to be used. And if you do so, don't forget to add null=True, blank=True to A and Avr2 foreign key fields.

On the other hand, the opposite of your schema makes your goal reachable: B model references A and Avr2 that means that B model has ONE generic foreign key to both A and Avr2 like this: (this code is with Django 1.8, for Django 1.9 or higher the import of GenericRelation, GenericForeignKey has changed)

from django.db import models
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.contrib.contenttypes.models import ContentType


class B(models.Model):
    # Some of your fields here...
    content_type = models.ForeignKey(ContentType, null=True, blank=True)
    object_id = models.PositiveIntegerField(null=True, blank=True)
    # Generic relational field will be associed to diffrent models like A or Avr2
    content_object = GenericForeignKey('content_type', 'object_id')


class A(models.Model):
    # Some of your fields here...
    the_common_field = models.BooleanField()
    bbb = GenericRelation(B, related_query_name="a")  # since it is a foreign key, this may be one or many objects refernced (One-To-Many)


class Avr2(models.Model):
    # Some of your fields here...
    the_common_field = models.BooleanField()
    bbb = GenericRelation(B, related_query_name="avr2")  # since it is a foreign key, this may be one or many objects refernced (One-To-Many)

Now both A and Avr2 have "bbb" field which is a B instance.

a = A(some fields initializations)
a.save()
b = B(some fields initializations)
b.save()
a.bbb = [b]
a.save()

Now you can do a.bbb and you get the B instances

And get the A or Avr2 out of b like this:

b.content_object  # which will return an `A object` or an `Avr2 object`

Now let's return to your goals:

  • Is there a way to run a query on B that will return, at runtime, the correct object type that references it, in the query result (and the query has both types in it)?

Yes: like this:

B.objects.get(id=1).content_type  # will return A or Avr2
  • You wanna perform something like this: b_filter = B.objects.filter(some_of_them_have_this_true=True) :

    from django.db.models import Q

    filter = Q(a__common_field=True) | Q(avr2__common_field=True)

    B.objects.filter(filter)

  • Getting [{'a__myval': , 'aver2__myval': None}] is 100% normal since values is asked to provide two fields values. One way to overcome this, is by getting two clean queries and then chain them together like so:

    from itertools import chain

    c1 = B.objects.filter(content_type__model='a').values('a__common_field')

    c2 = B.objects.filter(content_type__model='avr2').values('avr2__common_field')

    result_list = list(chain(c1, c2))
    

Please notice that when we added related_query_name to the generic relation, a and avr2 has become accessible from B instances, which is not the default case.

And voilà ! I hope this helps !

like image 163
Yahya Yahyaoui Avatar answered Nov 08 '22 22:11

Yahya Yahyaoui