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

Inheritance for foreign keys

Open thibault opened this issue 12 years ago • 21 comments

Hi,

I cannot find a way to use the wonderful (it saves my life) InheritanceManager through a ForeignKey. Here's some code, it will be clearer:

class Product(Model):
    objects = InheritanceManager()

class Shirt(Product):
    …

class Jacket(Product):
    …

class Box(Model):
    product = ForeignKey('Product')

boxes = Box.objects.all()
for box in boxes:
    box.product # How to get shirts and jackets here?

Is there any way to do this? Cannot find it in the doc. If not, that would be a great feature to add.

Regards, Thibault

thibault avatar Apr 13 '12 09:04 thibault

Currently I don't think there's any way to do this. I mean, the manager will be used if its the default manager, but select_subclasses will never be called.

The approach that first comes to mind would be a subclass of InheritanceManager that always overrides get() to return the polymorphic subclass; if that's the default manager on Product it should then work.

Closing this issue for now, though if you find a nice way to do this and want to propose it as an addition to model-utils, I'd be fine with a pull request.

carljm avatar Apr 13 '12 22:04 carljm

Never mind, not sure why I closed it, I'll leave it open since we don't have the feature and I'd accept it in principle (not sure what the API should be though).

carljm avatar Apr 13 '12 22:04 carljm

I followed your advice, and added a get method in my InheritanceQuerySet:

def get(self, *args, **kwargs):
    """
    Performs the query and returns a single object matching the given
    keyword arguments.
    """
    clone = self.filter(*args, **kwargs).select_subclasses()

    # HERE !
    if not hasattr(self, 'subclasses'):
        clone = clone.select_subclasses()

    if self.query.can_filter():
        clone = clone.order_by()
    num = len(clone)
    if num == 1:
        return clone._result_cache[0]
    if not num:
        raise self.model.DoesNotExist("%s matching query does not exist."
                % self.model._meta.object_name)
    raise self.model.MultipleObjectsReturned("get() returned more than one %s -- it returned %s! Lookup parameters were %s"
            % (self.model._meta.object_name, num, kwargs))

It works to a certain extent:

type(box.product) # 'Jacket'

But it won't performs the joins in the initial query. I don't know how we could make it better.

thibault avatar Apr 16 '12 14:04 thibault

Hey ! I was looking for the solution of this problem and found this.I was wondering, did you actually fix this issue ? The proposed feature addition didn't seem to have made it into the master branch...

AlexCid avatar Jan 10 '15 00:01 AlexCid

@AlexCid No, nobody has written any code to implement the proposed feature. If they had and it had been merged, this issue would be closed.

carljm avatar Jan 10 '15 00:01 carljm

too bad :( CrazyCasta's message seemed to imply that the code was available but it seems i misunderstood him. Thanks for your rapid answer anyways !

AlexCid avatar Jan 10 '15 00:01 AlexCid

I'm still beating my head against the ForeignKey field, trying to grok it, but...

Would it be possible to create a ForeignKey subclass that overrides however the hell it fetches its value, so that it goes through an InheritanceManager and calls select_subclasses()?

AndrewHows avatar Feb 02 '16 11:02 AndrewHows

@AndrewHows I'd think that would be possible, yeah. That's actually not a bad API for this; placing the decision on the FK side makes some sense.

carljm avatar Feb 02 '16 15:02 carljm

Ok. If I get it working, I'll send a pull request

AndrewHows avatar Feb 02 '16 21:02 AndrewHows

I started trying to pick this apart as well (the ForeignKey idea) ... I started by using the trace class in Python:

In [11]: tracer.run('m.foreignkeyfield') 
 --- modulename: related_descriptors, funcname: __get__
related_descriptors.py(153):         if instance is None:
related_descriptors.py(159):         try:
related_descriptors.py(160):             rel_obj = getattr(instance, self.cache_name)
related_descriptors.py(161):         except AttributeError:
related_descriptors.py(162):             val = self.field.get_local_related_value(instance)
 --- modulename: related, funcname: get_local_related_value
related.py(597):         return self.get_instance_value_for_fields(instance, self.local_related_fields)
 --- modulename: related, funcname: local_related_fields
related.py(590):         return tuple(lhs_field for lhs_field, rhs_field in self.related_fields)
 --- modulename: related, funcname: related_fields
related.py(580):         if not hasattr(self, '_related_fields'):
related.py(582):         return self._related_fields
 --- modulename: related, funcname: <genexpr>
related.py(590):         return tuple(lhs_field for lhs_field, rhs_field in self.related_fields)
 --- modulename: related, funcname: <genexpr>
related.py(590):         return tuple(lhs_field for lhs_field, rhs_field in self.related_fields)
 --- modulename: related, funcname: get_instance_value_for_fields
related.py(604):         ret = []
related.py(605):         opts = instance._meta
related.py(606):         for field in fields:
related.py(610):             if field.primary_key:
related.py(617):             ret.append(getattr(instance, field.attname))
related.py(606):         for field in fields:
related.py(618):         return tuple(ret)
related_descriptors.py(163):             if None in val:
related_descriptors.py(166):                 qs = self.get_queryset(instance=instance)
 --- modulename: related_descriptors, funcname: get_queryset
related_descriptors.py(105):         manager = self.field.remote_field.model._default_manager
related_descriptors.py(108):         if not getattr(manager, 'use_for_related_fields', False):
related_descriptors.py(110):         return manager.db_manager(hints=hints).all()
 --- modulename: manager, funcname: db_manager
manager.py(196):         obj = copy.copy(self)
 --- modulename: copy, funcname: copy
copy.py(72):     cls = type(x)

It looks like a descriptor is put into place ... ForwardManyToOneDescriptor

It looks like any modification to do select_subclasses might have to be in the get method.

rrauenza avatar Sep 20 '16 21:09 rrauenza

I'm wondering, is the problem easier to solve if the model that has the ForeignKey is required to have an InheritanceManager?

i.e.,

class Parent(Model):
      objects = InheritanceManager()  # Can adding this to the parent make the problem easier to solve?
      child = ForeignKey(Child)

class Child(Model):
       objects = InheritanceManager()

class AnotherChild(Child):
       .....

rrauenza avatar Sep 22 '16 17:09 rrauenza

@rrauenza, thanks for your comment.

I created a simple ForeignKey field which allows my models to return subclassed foreign key values.

class InheritanceForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
    def get_queryset(self, **hints):
        return self.field.remote_field.model.objects.db_manager(hints=hints).select_subclasses()


class InheritanceForeignKey(models.ForeignKey):
    forward_related_accessor_class = InheritanceForwardManyToOneDescriptor

update: It was tested only with Django-2.1/Python-3.6

renatgalimov avatar May 08 '18 22:05 renatgalimov

Have anyone tried @femdom's solution? Does it have any undesirable side effects?

moorchegue avatar Jul 26 '18 15:07 moorchegue

I found a way to do it, so maybe I can help a little bit. First, you need to create a custom manager that always look for the subclasses:

from model_utils.managers import InheritanceManager

class ProductManager(InheritanceManager):
	def get_queryset(self):
		return super(ProductManager, self).get_queryset().select_subclasses()

After that, you plug it into your model and set it as the base manager:

class Product(Model):
	objects = ProductManager()

	class Meta:
		base_manager_name = 'objects'

limaagabriel avatar Jan 09 '19 03:01 limaagabriel

Yes, this seems to be working.

I never understood why the InheritanceManager doesn't do that by default.

I mean in general why InheritanceManager doesn't just override the get and get_queryset functions and we rather have to use the special functions select_subclasses and get_subclass? What is the reasoning behind that?

pgeorgiadis avatar Jan 09 '19 08:01 pgeorgiadis

@pgeorgiadis I think it was built this way to preserve django's default manager interface. So, because of that, if you are using the InheritanceManager, you are still able to make queries as the default manager.

limaagabriel avatar Jan 14 '19 20:01 limaagabriel

@itsmealves and @pgeorgiadis

I tried the solution you provided, and it works to resolve for querying models

However on delete it creates an issue where the BaseModel remains in the database, but the InheritedModel is deleted.

This is the output I get

django.db.utils.IntegrityError: insert or update on table "piano_gym_api_flashcardmodel" violates foreign key constraint "piano_gym_api_flashc_flash_card_set_id_61025182_fk_piano_gym"
DETAIL:  Key (flash_card_set_id)=(4) is not present in table "piano_gym_api_flashcardsetmodel".

Do you have any guidance on what should be done?

loganknecht avatar May 01 '20 22:05 loganknecht

@loganknecht

I also had problems with the deletion of inherited models. I worked around by overloading their .delete() method to use a QuerySet instead, like this:

def delete(self):
    self.__class__.objects.filter(id=self.id).delete()

It works quite good in my case, even handling the cascading deletion of related models when needed.

Hyask avatar Jul 10 '20 09:07 Hyask

@rrauenza, thanks for your comment.

I created a simple ForeignKey field which allows my models to return subclassed foreign key values.

class InheritanceForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
    def get_queryset(self, **hints):
        return self.field.remote_field.model.objects.db_manager(hints=hints).select_subclasses()


class InheritanceForeignKey(models.ForeignKey):
    forward_related_accessor_class = InheritanceForwardManyToOneDescriptor

update: It was tested only with Django-2.1/Python-3.6

That works flawlessly for me. Thank you!

SteffRhes avatar Oct 01 '20 21:10 SteffRhes

@rrauenza, thanks for your comment. I created a simple ForeignKey field which allows my models to return subclassed foreign key values.

class InheritanceForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
    def get_queryset(self, **hints):
        return self.field.remote_field.model.objects.db_manager(hints=hints).select_subclasses()


class InheritanceForeignKey(models.ForeignKey):
    forward_related_accessor_class = InheritanceForwardManyToOneDescriptor

update: It was tested only with Django-2.1/Python-3.6

That works flawlessly for me. Thank you!

Yes, it works super. Unless the related objects are fetched in advance with select_related. Any ideas how to make it work with select_related? But I guess this is harder to solve as select_related works on a deeper level of the ORM and doing JOINs directly.

medihack avatar Oct 12 '20 17:10 medihack

Should we add this code to main package?

I can create PR, but we need to decide proper naming. Is InheritanceForeignKey good enough, or maybe it's better to use something like "PolymorphicForeignKey"?

last-partizan avatar May 30 '23 08:05 last-partizan