Wow, amazing job!

This addresses the big disappointment I felt when I found out django
Model Inheritance didn't do this by default. I used some ugly
workarounds to make it happen and this seems like the perfect
solution. Main problem is that it requires django 1.1, I'm working
with trunk/1.2, but I'm going to give it a try anyway.

I'll keep tracking the developments on github, keep up the good work!

- Robin

On Jan 15, 8:55 pm, Bert Constantin <bert.constan...@gmx.de> wrote:
> Hello everyone!
>
> I just uploaded a simple but mostly complete implementation of model
> and queryset polymorphism onto github and bitbucket.
>
> For enabled models, this implementation simply makes all references
> to the model's objects polymorphic (be it managers/querysets or
> relationship fields).
>
> The prototype might be useful as a tool for concretely exploring the
> concept of polymorphic models within the Django framework (and
> perhaps might find use in real projects as well).
>
> My impression is that fully polymorphic models and querysets
> integrate very well into the Django ORM and database API. Also, with
> Django's ORM and model inheritance system, all the heavy lifting
> has already been done; only a rather thin layer above seems
> to be required to get to a complete implementation of polymorphic
> inheritance.
>
> Below I copied the docstring of the module.
> Near the top are a number of short examples that are good as a quick
> overview.
>
> The docstring can be read in a much better format 
> here:http://bserve.webhop.org/wiki/django_polymorphic
>
> Suggestions and criticism are very welcome.
>
> Kind Regards,
> Bert Constantin
>
> GitHub:http://github.com/bconstantin/django_polymorphic
> Bitbucket:http://bitbucket.org/bconstantin/django_polymorphic
> Tar:http://github.com/bconstantin/django_polymorphic/tarball/master
>
> +++++++
>
> Defining Polymorphic Models
> ===========================
>
> To make models polymorphic, use PolymorphicModel instead of Django's
> models.Model as the superclass of your base model. All models
> inheriting from your base class will be polymorphic as well::
>
>     from polymorphic import PolymorphicModel
>
>     class ModelA(PolymorphicModel):
>         field1 = models.CharField(max_length=10)
>
>     class ModelB(ModelA):
>         field2 = models.CharField(max_length=10)
>
>     class ModelC(ModelB):
>         field3 = models.CharField(max_length=10)
>
> Using Polymorphic Models
> ========================
>
> Most of Django's standard ORM functionality is available
> and works as expected:
>
> Create some objects
> -------------------
>
>     >>> ModelA.objects.create(field1='A1')
>     >>> ModelB.objects.create(field1='B1', field2='B2')
>     >>> ModelC.objects.create(field1='C1', field2='C2', field3='C3')
>
> Query results are polymorphic
> -----------------------------
>
>     >>> ModelA.objects.all()
>     .
>     [ <ModelA: id 1, field1 (CharField)>,
>       <ModelB: id 2, field1 (CharField), field2 (CharField)>,
>       <ModelC: id 3, field1 (CharField), field2 (CharField), field3
> (CharField)> ]
>
> Filtering for classes (equivalent to python's isinstance() ):
> -------------------------------------------------------------
>
>     >>> ModelA.objects.instance_of(ModelB)
>     .
>     [ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
>       <ModelC: id 3, field1 (CharField), field2 (CharField), field3
> (CharField)> ]
>
>     In general, including or excluding parts of the inheritance tree::
>
>         ModelA.objects.instance_of(ModelB [, ModelC ...])
>         ModelA.objects.not_instance_of(ModelB [, ModelC ...])
>
> Polymorphic filtering (for fields in derived classes)
> -----------------------------------------------------
>
>     For example, cherrypicking objects from multiple derived classes
>     anywhere in the inheritance tree, using Q objects (with the
>     slightly enhanced syntax: exact model name + three _ + field
> name):
>
>     >>> ModelA.objects.filter(  Q( ModelB___field2 = 'B2' )  |  Q
> ( ModelC___field3 = 'C3' )  )
>     .
>     [ <ModelB: id 2, field1 (CharField), field2 (CharField)>,
>       <ModelC: id 3, field1 (CharField), field2 (CharField), field3
> (CharField)> ]
>
> Combining Querysets of different types/models
> ---------------------------------------------
>
>     Querysets may now be regarded as object containers that allow the
>     aggregation of  different object types - very similar to python
>     lists (as long as the objects are accessed through the manager of
>     a common base class):
>
>     >>> Base.objects.instance_of(ModelX) | Base.objects.instance_of
> (ModelY)
>     .
>     [ <ModelX: id 1, field_x (CharField)>,
>       <ModelY: id 2, field_y (CharField)> ]
>
> Using Third Party Models (without modifying them)
> -------------------------------------------------
>
>     Third party models can be used as polymorphic models without any
>     restrictions by simply subclassing them. E.g. using a third party
>     model as the root of a polymorphic inheritance tree::
>
>         from thirdparty import ThirdPartyModel
>
>         class MyThirdPartyModel(PolymorhpicModel, ThirdPartyModel):
>             pass    # or add fields
>
>     Or instead integrating the third party model anywhere into an
>     existing polymorphic inheritance tree::
>
>         class MyModel(SomePolymorphicModel):
>             my_field = models.CharField(max_length=10)
>
>         class MyModelWithThirdParty(MyModel, ThirdPartyModel):
>             pass    # or add fields
>
> ManyToManyField, ForeignKey, OneToOneField
> ------------------------------------------
>
>     Relationship fields referring to polymorphic models work as
>     expected: like polymorphic querysets they now always return the
>     referred objects with the same type/class these were created and
>     saved as.
>
>     E.g., if in your model you define::
>
>         field1 = OneToOneField(ModelA)
>
>     then field1 may now also refer to objects of type ModelB or
> ModelC.
>
>     A ManyToManyField example::
>
>         # The model holding the relation may be any kind of model,
> polymorphic or not
>         class RelatingModel(models.Model):
>             many2many = models.ManyToManyField('ModelA')  # ManyToMany
> relation to a polymorphic model
>
>         >>> o=RelatingModel.objects.create()
>         >>> o.many2many.add(ModelA.objects.get(id=1))
>         >>> o.many2many.add(ModelB.objects.get(id=2))
>         >>> o.many2many.add(ModelC.objects.get(id=3))
>
>         >>> o.many2many.all()
>         [ <ModelA: id 1, field1 (CharField)>,
>           <ModelB: id 2, field1 (CharField), field2 (CharField)>,
>           <ModelC: id 3, field1 (CharField), field2 (CharField),
> field3 (CharField)> ]
>
> Non-Polymorphic Queries
> -----------------------
>
>     >>> ModelA.base_objects.all()
>     .
>     [ <ModelA: id 1, field1 (CharField)>,
>       <ModelA: id 2, field1 (CharField)>,
>       <ModelA: id 3, field1 (CharField)> ]
>
>     Each polymorphic model has 'base_objects' defined as a normal
>     Django manager. Of course, arbitrary custom managers may be
>     added to the models as well.
>
> Custom Managers, Querysets & Inheritance
> ========================================
>
> Using a Custom Manager
> ----------------------
>
> For creating a custom polymorphic manager class, derive your manager
> from PolymorphicManager instead of models.Manager. In your model
> class, explicitly add the default manager first, and then your
> custom manager::
>
>         class MyOrderedManager(PolymorphicManager):
>             def get_query_set(self):
>                 return super(MyOrderedManager,self).get_query_set
> ().order_by('some_field')
>
>         class MyModel(PolymorphicModel):
>             objects = PolymorphicManager()    # add the default
> polymorphic manager first
>             ordered_objects = MyOrderedManager()    # then add your
> own manager
>
> The first manager defined ('objects' in the example) is used by
> Django as automatic manager for several purposes, including accessing
> related objects. It must not filter objects and it's safest to use
> the plain PolymorphicManager here.
>
> Manager Inheritance
> -------------------
>
> The current polymorphic models implementation unconditionally
> inherits all managers from the base models. An example::
>
>     class MyModel2(MyModel):
>         pass
>
>     # Managers inherited from MyModel
>     >>> MyModel2.objects.all()
>     >>> MyModel2.ordered_objects.all()
>
> Manager inheritance is a somewhat complex topic that needs more
> thought and more actual experience with real-world use-cases.
>
> Using a Custom Queryset Class
> -----------------------------
>
> The PolymorphicManager class accepts one initialization argument,
> which is the queryset class the manager should use. A custom
> custom queryset class can be defined and used like this::
>
>         class MyQuerySet(PolymorphicQuerySet):
>             def my_queryset_method(...):
>                 ...
>
>         class MyModel(PolymorphicModel):
>             my_objects=PolymorphicManager(MyQuerySet)
>             ...
>
> Performance Considerations
> ==========================
>
> The current implementation is pretty simple and does not use any
> custom sql - it is purely based on the Django ORM. Right now the
> query ::
>
>     result_objects = list( ModelA.objects.filter(...) )
>
> performs one sql query to retrieve ModelA objects and one additional
> query for each unique derived class occurring in result_objects.
> The best case for retrieving 100 objects is 1 db query if all are
> class ModelA. If 50 objects are ModelA and 50 are ModelB, then two
> queries are executed. If result_objects contains only the base model
> type (ModelA), the polymorphic models are just as efficient as plain
> Django models (in terms of executed queries). The pathological worst
> case is 101 db queries if result_objects contains 100 different
> object types (with all of them subclasses of ModelA).
>
> Performance ist relative: when Django users create their own
> polymorphic ad-hoc solution (without a module like polymorphic.py),
> they will tend to use a variation of ::
>
>     result_objects = [ o.get_real_instance() for o in
> BaseModel.objects.filter(...) ]
>
> which of course has really bad performance. Relative to this, the
> performance of the current polymorphic.py is rather good.
> It's well possible that the current implementation is already
> efficient enough for the majority of use cases.
>
> Chunking: The implementation always requests objects in chunks of
> size Polymorphic_QuerySet_objects_per_request. This limits the
> complexity/duration for each query, including the pathological cases.
>
> Possible Optimizations
> ======================
>
> PolymorphicQuerySet can be optimized to require only one sql query
> for the queryset evaluation and retrieval of all objects.
>
> Basically, what ist needed is a possibility to pull in the fields
> from all relevant sub-models with one sql query. In order to do this
> on top of the Django ORM, some kind of enhhancement would be needed.
>
> At first, it looks like a reverse select_related for OneToOne
> relations might offer a solution 
> (seehttp://code.djangoproject.com/ticket/7270)::
>
>     ModelA.objects.filter(...).select_related
> ('modelb','modelb__modelc')
>
> This approach has a number of problems, but nevertheless would
> already execute the correct sql query and receive all the model
> fields required from the db.
>
> A kind of "select_related for values" might be a better solution::
>
>     ModelA.objects.filter(...).values_related(
>         [ base field name list ],  {
>             'modelb' : [field name list ],
>             'modelb__modelc' : [ field name list ]
>         })
>
> Django's lower level db API in QuerySet.query (see BaseQuery in
> django.db.models.sql.query) might still allow other, better or easier
> ways to implement the needed functionality.
>
> SQL Complexity
> --------------
>
> Regardless how these queries would be created, their complexity is
> the same in any case:
>
> With only one sql query, one sql join for each possible subclass
> would be needed (BaseModel.__subclasses__(), recursively).
> With two sql queries, the number of joins could be reduced to the
> number of actuallly occurring subclasses in the result. A final
> implementation might want to use one query only if the number of
> possible subclasses (and therefore joins) is not too large, and
> two queries otherwise (using the first query to determine the
> actually occurring subclasses, reducing the number of joins for
> the second).
>
> A relatively large number of joins may be needed in both cases,
> which raises concerns regarding the efficiency of these database
> queries. It is currently unclear however, how many model classes
> will actually be involved in typical use cases - the total number
> of classes in the inheritance tree as well as the number of distinct
> classes in query results. It may well turn out that the increased
> number of joins is no problem for the DBMS in all realistic use
> cases. Alternatively, if the sql query execution time is
> significantly longer even in common use cases, this may still be
> acceptable in exchange for the added functionality.
>
> Let's not forget that all of the above is just about optimizations.
> The current simplistic implementation already works well - perhaps
> well enough for the majority of applications.
>
> Restrictions, Caveats, Loose Ends
> =================================
>
> Unsupported Queryset Methods
> ----------------------------
>
> +   aggregate() probably makes only sense in a purely non-OO/
> relational
>     way. So it seems an implementation would just fall back to the
>     Django vanilla equivalent.
>
> +   annotate(): The current '_get_real_instances' would need minor
>     enhancement.
>
> +   defer() and only(): Full support, including slight polymorphism
>     enhancements, seems to be straighforward
>     (depends on '_get_real_instances').
>
> +   extra(): Does not really work with the current implementation of
>     '_get_real_instances'. It's unclear if it should be supported.
>
> +   select_related(): This would probably need Django core support
>     for traversing the reverse model inheritance OneToOne relations
>     with Django's select_related(), e.g.:
>     *select_related('modela__modelb__foreignkeyfield')*.
>     Also needs more thought/investigation.
>
> +   distinct() needs more thought and investigation as well
>
> +   values() & values_list(): Implementation seems to be mostly
>     straighforward
>
> Restrictions & Caveats
> ----------------------
>
> +   Diamond shaped inheritance: There seems to be a general problem
>     with diamond shaped multiple model inheritance with Django models
>     (tested with V1.1).
>     An example is here:http://code.djangoproject.com/ticket/10808.
>     This problem is aggravated when trying to enhance models.Model
>     by subclassing it instead of modifying Django core (as we do here
>     with PolymorphicModel).
>
> +   The name and appname of the leaf model is stored in the base model
>     (the base model directly inheriting from PolymorphicModel).
>     If a model or an app is renamed, then these fields need to be
>     corrected too, if the db content should stay usable after the
> rename.
>     Aside from this, these two fields should probably be combined into
>     one field (more db/sql efficiency)
>
> +   For all objects that are not instances of the base class type, but
>     instances of a subclass, the base class fields are currently
>     transferred twice from the database (an artefact of the current
>     implementation's simplicity).
>
> +   __getattribute__ hack: For base model inheritance back relation
>     fields (like basemodel_ptr), as well as implicit model inheritance
>     forward relation fields, Django internally tries to use our
>     polymorphic manager/queryset in some places, which of course it
>     should not. Currently this is solved with hackish __getattribute__
>     in PolymorphicModel. A minor patch to Django core would probably
>     get rid of that.
>
> +   "instance_of" and "not_instance_of" may need some optimization.
>
> More Investigation Needed
> -------------------------
>
> There are a number of subtleties that have not yet been fully
> evaluated
> or resolved, for example (among others) the exact implications of
> 'use_for_related_fields' in the polymorphic manager.
>
> There may also well be larger issues of conceptual or technical nature
> that might basically be showstoppers (but have not yet been found).
>
> In General
> ----------
>
> It is important to consider that this code is very experimental
> and very insufficiently tested. A number of test cases are included
> but they need to be expanded. This implementation is currently more
> a tool for exploring the concept of polymorphism within the Django
> framework. After careful testing and consideration it may perhaps be
> useful for actual projects, but it might be too early for this
> right now.
>
> Links
> =====
>
> -http://code.djangoproject.com/wiki/ModelInheritance
> -http://lazypython.blogspot.com/2009/02/second-look-at-inheritance-and...
> -http://www.djangosnippets.org/snippets/1031/
> -http://www.djangosnippets.org/snippets/1034/
> -http://groups.google.com/group/django-developers/browse_frm/thread/7d...
> -http://groups.google.com/group/django-developers/browse_thread/thread...
> -http://groups.google.com/group/django-developers/browse_thread/thread...
> -http://groups.google.com/group/django-users/browse_thread/thread/52f7...
> -http://peterbraden.co.uk/article/django-inheritance
> -http://www.hopelessgeek.com/2009/11/25/a-hack-for-multi-table-inherit...
> -http://stackoverflow.com/questions/929029/how-do-i-access-the-child-c...
> -http://stackoverflow.com/questions/1581024/django-inheritance-how-to-...
> -http://groups.google.com/group/django-users/browse_thread/thread/cbda...
> -http://groups.google.com/group/django-users/browse_thread/thread/52f7...
> -http://code.djangoproject.com/ticket/10808
> -http://code.djangoproject.com/ticket/7270

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-develop...@googlegroups.com.
To unsubscribe from this group, send email to 
django-developers+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en.

Reply via email to