django-model-utils icon indicating copy to clipboard operation
django-model-utils copied to clipboard

InheritanceManager doesn't work with Proxy Model

Open chhantyal opened this issue 10 years ago • 9 comments
trafficstars

I created an app to demonstrate this bug here https://github.com/chhantyal/mptt-test

Please see the models here https://github.com/chhantyal/mptt-test/blob/master/mptt_test/app/models.py

When I use select_subclasses(), it doesn't give subclass object but superclass object (in this case ProxyPage object itself).

In [1]: from mptt_test.app.models import Blog, PageProxy

In [2]: Blog.objects.create(title='Title', quote='blog')
Out[2]: <Blog: Blog object>

In [3]: PageProxy.objects.all().select_subclasses()
Out[3]: [<PageProxy: PageProxy object>]

chhantyal avatar May 05 '15 10:05 chhantyal

Thanks for the report! This does look like a bug. I'm not sure if or when I'll have time to look into it, but I'd be happy to review a pull request.

carljm avatar May 05 '15 15:05 carljm

At least at a glance, the problem appears to stem from model._meta.get_all_related_objects(), which for a proxy model (Pageproxy) yields []; it is instead bound to the concrete parent (Page) as [<OneToOneRel: ...>]

kezabelle avatar May 06 '15 07:05 kezabelle

Further investigation, across both Django 1.7 and 1.8 (because both finally raise exceptions for select_related), given the following:

class InheritanceProxyConcrete(models.Model):
    pass

class InheritanceProxy(InheritanceProxyConcrete):
    class Meta:
        proxy = True

class InheritanceProxyChild(InheritanceProxy):
    pass

the following is given when trying to use a normal select_related (ie: before hitting our InheritanceManager magic):

>>> InheritanceProxyConcrete.objects.select_related('anything')
FieldError: Invalid field name(s) given in select_related: 'anything'. Choices are: inheritanceproxychild
>>> InheritanceProxy.objects.select_related('anything')
FieldError: Invalid field name(s) given in select_related: 'anything'. Choices are: (none)

which sort of implies to me that we couldn't support it currently anyway, even if we could resolve the correct model by doing something like:

if model._meta.proxy is True:
    relations = model._meta.proxy_for_model._meta.get_all_related_objects()
else:
    relations = model._meta.get_all_related_objects()

If either of you can replicate my findings, I'll open a ticket upstream and see what happens.

kezabelle avatar May 06 '15 10:05 kezabelle

Hi @kezabelle - I haven't explored it myself, but based on what you're seeing it certainly does look like this is an upstream bug. If select_related doesn't work on proxy models, then select_subclasses isn't going to work either. So the correct fix seems to be to make select_related work first.

carljm avatar May 06 '15 16:05 carljm

I've opened a ticket upstream reflecting my findings. If it comes back as my mistake, I can re-evaluate the problem in this ticket, possibly with a better understanding of what I got wrong ;)

kezabelle avatar May 07 '15 08:05 kezabelle

Hi @kezabelle !

The corresponding ticket has been fixed/closed.

@chhantyal you can try to upgrade to Django 1.10 and see if you still encounter this issue.

romgar avatar Nov 27 '16 14:11 romgar

Hi. I have the same issue.

Django==2.0.3 django-model-utils==3.0.0

class Offer(models.Model):
    objects = InheritanceQuerySet.as_manager()
    class Meta:
        verbose_name = _('offer')
        verbose_name_plural = _('offers')

class Order(Offer):
    class Meta:
        managed = False
        proxy = True
        verbose_name = _('order')
        verbose_name_plural = _('orders')

result = Offer.objects.select_subclasses()

All instances in result are instances of Offer.

Any ideas please?

eriktelepovsky avatar Apr 18 '18 15:04 eriktelepovsky

Same issue with:

  • Django 2.2
  • django-model-utils 3.1.2

Lorac avatar May 16 '19 06:05 Lorac

Hi,

This hit me as well.

However, looking a bit more in detail into it, I feel like model-utils does the right thing / the best it can.

Proxy models in django only work on the Python level - there is no information stored about the proxy in the database (the table is shared with the base model). So when you query objects from a certain model (class), you get instances of that class. This is by design.

In that situation, model-utils has no way to know which model is "the right one" - because both are, there's no difference on the db level! So it uses the non-proxy one in select_subclasses.

See also https://docs.djangoproject.com/en/3.1/topics/db/models/#querysets-still-return-the-model-that-was-requested.

(note: this is only my understanding of it, maybe I'm wrong)

EDIT:

To complete my point above, I would like to emphasize that proxy models in django are not subclasses in the MTI sense - they are other "views" of the same data. Accordingly, if you have a proxy model B to some model A, you can create instances of A or B and retrieve any of them later as A or B instance transparently.

In django, which model you want to retrieve instances of is explicit: from the model you take the manager of, from the definition of relation fields, from the arguments of select_related, etc.

In model-utils however the whole point of select_subclasses is to "guess" the return types, by scanning through possible subclasses in the db. So proxy models cannot be retrieved as they do not correspond to tables in the db on their own (only present as a row in django_content_type).

One case where model-utils could do better (or forbid this case altogether) is when one explicitly passes a proxy model as argument in select_subclasses:

class C(models.Model):
    objects = InheritanceManager()

class D(C):
    pass

class E(D):
    class Meta:
        proxy = True
>>> D().save() # or E().save(), doesn't make a difference

>>> C.objects.select_subclasses()
<InheritanceQuerySet [<D: D object (1)>]>
>>> C.objects.select_subclasses(D)
<InheritanceQuerySet [<D: D object (1)>]>
>>> D.objects.select_subclasses()
<InheritanceQuerySet [<D: D object (1)>]>
>>> E.objects.select_subclasses(E)
<InheritanceQuerySet [<E: E object (1)>]>
>>> C.objects.select_subclasses(E)
<InheritanceQuerySet [<D: D object (1)>]>
>>> D.objects.select_subclasses(E)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/matpi/.local/share/virtualenvs/tmp_django_mti/lib/python3.8/site-packages/model_utils/managers.py", line 219, in select_subclasses
    return self.get_queryset().select_subclasses(*subclasses)
  File "/home/matpi/.local/share/virtualenvs/tmp_django_mti/lib/python3.8/site-packages/model_utils/managers.py", line 70, in select_subclasses
    raise ValueError(
ValueError: '' is not in the discovered subclasses, tried: 

The first four calls to select_subclasses make sense. One could however argue that the last two should both succeed and return E instances.

qwenger avatar Oct 18 '20 10:10 qwenger