django-model-utils
django-model-utils copied to clipboard
Inheritance for foreign keys
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
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.
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).
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.
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 No, nobody has written any code to implement the proposed feature. If they had and it had been merged, this issue would be closed.
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 !
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 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.
Ok. If I get it working, I'll send a pull request
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.
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, 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
Have anyone tried @femdom's solution? Does it have any undesirable side effects?
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'
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 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.
@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
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.
@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!
@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.
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"?