0

Say I have a set of models like this:

class Container(models.Model):
    pass

class Parent(models.Model):
    container = models.ForeignKey(Container, related_name='%(class)s_list')

class Child(Parent):
    pass

Without making Parent abstract, is it possible to make Child's container field have a related name of child_list instead and all Child's instances would be put in child_list for the Container model?

I've tried modifying Child to have an additional field:

class Child(Parent):
    container2 = models.ForeignKey(Container, related_name='%(class)s_list')

And then making the two fields be in sync'. However, since the real goal of this is to optimize a parent class with many inheritance-related children [1], using an aliased Container key doesn't populate the cache correctly when doing a prefetch.

Any solution would probably cause an aliasing issue with Child instance in list child_list into sample_list. However, I'm not intending to expose sample_list and I'd be happy to work around aliasing.

Community
  • 1
  • 1
Ross Rogers
  • 23,523
  • 27
  • 108
  • 164
  • Please confirm that you intend for `Parent` model to have public properties that the `Child` model should not have. If this is the case, are you aware that querying `Parent.objects` will include `Child` rows instantiated as `Parent`? – Robert Jørgensgaard Engdahl Mar 12 '15 at 19:02
  • Yeah, `Parent.objects` would have the `Child` instances as `Parent` objects. – Ross Rogers Mar 12 '15 at 21:04

1 Answers1

0

I found a wicked answer:

  • Create a Container pointer in the Parent and the Child and keep them in sync'.
  • Use select_subclasses() from django-model-utils to populate the parent_list with all the different Child objects.
  • wrapper QuerySet's _prefetch_related_objects so that after the original call, the child objects are split into their respective buckets into _prefetched_objects_cache
  • wrapper QuerySet's _clone function in order to re-wraper _prefetch_related_objects() each time the QuerySet is _clone()'d

# get all the subclasses of the `Parent` type
# from http://stackoverflow.com/a/408465/20712
def find_subclasses(module, clazz):
    return [
        cls
            for name, cls in inspect.getmembers(module)
                if inspect.isclass(cls) and issubclass(cls, clazz)
    ]
parent_subclasses = find_subclasses(app.models,Parent)
parent_subclass_2_related_name = {}
for c in parent_subclasses:
    try:
        parent_subclass_2_related_name[c.__name__] = c.container.field.rel.related_name
    except (AttributeError):
        pass

# define the query and selectively wrapper it    
qs = Container.objects.all()\
    .prefetch_related(Prefetch('parent_list',Parent.objects.select_subclasses()))

def attach_clone(obj):
    original_clone = obj._clone
    def new_clone(*args,**kwargs):
        result = original_clone(*args,**kwargs)
        attach_clone(result) # re-wrapper _clone
        old_prefetch = result._prefetch_related_objects
        def new_prefetch(*args):
            old_prefetch()
            for obj in result._result_cache:
                for s in obj._prefetched_objects_cache['parent_list']:
                    obj._prefetched_objects_cache.setdefault(parent_subclass_2_related_name[s.__class__.__name__],[]).append(s)
        result._prefetch_related_objects = new_prefetch
        return result
    obj._clone = new_clone
attach_clone(qs)

return qs
Ross Rogers
  • 23,523
  • 27
  • 108
  • 164