Fixed #10356 -- Added pure-Python inheritance for models (a.k.a proxy models).

Large portions of this are needed for #5420, so I implemented it fully.
Thanks to Ryan Kelly for an initial patch to get this started.

Refs #5420.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10083 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2009-03-18 09:47:08 +00:00
parent c0b6e23eb3
commit 61a2708c41
10 changed files with 486 additions and 93 deletions

View File

@ -220,6 +220,7 @@ answer newbie questions, and generally made Django that much better:
Erik Karulf <erik@karulf.com>
Ben Dean Kawamura <ben.dean.kawamura@gmail.com>
Ian G. Kelly <ian.g.kelly@gmail.com>
Ryan Kelly <ryan@rfk.id.au>
Thomas Kerpe <thomas@kerpe.net>
Ossama M. Khayat <okhayat@yahoo.com>
Ben Khoo <khoobks@westnet.com.au>

View File

@ -67,9 +67,19 @@ class ModelBase(type):
if not hasattr(meta, 'get_latest_by'):
new_class._meta.get_latest_by = base_meta.get_latest_by
is_proxy = new_class._meta.proxy
if getattr(new_class, '_default_manager', None):
if not is_proxy:
# Multi-table inheritance doesn't inherit default manager from
# parents.
new_class._default_manager = None
new_class._base_manager = None
else:
# Proxy classes do inherit parent's default manager, if none is
# set explicitly.
new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
# Bail out early if we have already created this class.
m = get_model(new_class._meta.app_label, name, False)
@ -80,21 +90,43 @@ class ModelBase(type):
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
# Do the appropriate setup for any model parents.
o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
if isinstance(f, OneToOneField)])
for base in parents:
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
# All the fields of any type declared on this model
new_fields = new_class._meta.local_fields + \
new_class._meta.local_many_to_many + \
new_class._meta.virtual_fields
field_names = set([f.name for f in new_fields])
# Basic setup for proxy models.
if is_proxy:
base = None
for parent in [cls for cls in parents if hasattr(cls, '_meta')]:
if parent._meta.abstract:
if parent._meta.fields:
raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name)
else:
continue
if base is not None:
raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name)
else:
base = parent
if base is None:
raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
if (new_class._meta.local_fields or
new_class._meta.local_many_to_many):
raise FieldError("Proxy model '%s' contains model fields."
% name)
new_class._meta.setup_proxy(base)
# Do the appropriate setup for any model parents.
o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
if isinstance(f, OneToOneField)])
for base in parents:
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
# Check for clashes between locally declared fields and those
# on the base classes (we cannot handle shadowed fields at the
@ -107,15 +139,19 @@ class ModelBase(type):
(field.name, name, base.__name__))
if not base._meta.abstract:
# Concrete classes...
while base._meta.proxy:
# Skip over a proxy class to the "real" base it proxies.
base = base._meta.proxy_for_model
if base in o2o_map:
field = o2o_map[base]
else:
elif not is_proxy:
attr_name = '%s_ptr' % base._meta.module_name
field = OneToOneField(base, name=attr_name,
auto_created=True, parent_link=True)
new_class.add_to_class(attr_name, field)
else:
field = None
new_class._meta.parents[base] = field
else:
# .. and abstract ones.
for field in parent_fields:
@ -125,13 +161,12 @@ class ModelBase(type):
new_class._meta.parents.update(base._meta.parents)
# Inherit managers from the abstract base classes.
base_managers = base._meta.abstract_managers
base_managers.sort()
for _, mgr_name, manager in base_managers:
val = getattr(new_class, mgr_name, None)
if not val or val is manager:
new_manager = manager._copy_to_model(new_class)
new_class.add_to_class(mgr_name, new_manager)
new_class.copy_managers(base._meta.abstract_managers)
# Proxy models inherit the non-abstract managers from their base,
# unless they have redefined any of them.
if is_proxy:
new_class.copy_managers(base._meta.concrete_managers)
# Inherit virtual fields (like GenericForeignKey) from the parent
# class
@ -160,6 +195,15 @@ class ModelBase(type):
# registered version.
return get_model(new_class._meta.app_label, name, False)
def copy_managers(cls, base_managers):
# This is in-place sorting of an Options attribute, but that's fine.
base_managers.sort()
for _, mgr_name, manager in base_managers:
val = getattr(cls, mgr_name, None)
if not val or val is manager:
new_manager = manager._copy_to_model(cls)
cls.add_to_class(mgr_name, new_manager)
def add_to_class(cls, name, value):
if hasattr(value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
@ -358,12 +402,16 @@ class Model(object):
# At this point, parent's primary key field may be unknown
# (for example, from administration form which doesn't fill
# this field). If so, fill it.
if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
self.save_base(raw, parent)
self.save_base(cls=parent)
if field:
setattr(self, field.attname, self._get_pk_val(parent._meta))
if meta.proxy:
return
if not meta.proxy:
non_pks = [f for f in meta.local_fields if not f.primary_key]
# First, try an UPDATE. If that doesn't update anything, do an INSERT.

View File

@ -60,6 +60,9 @@ class Manager(object):
if model._meta.abstract or self._inherited:
model._meta.abstract_managers.append((self.creation_counter, name,
self))
else:
model._meta.concrete_managers.append((self.creation_counter, name,
self))
def _set_creation_counter(self):
"""

View File

@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace',
'abstract', 'managed')
'abstract', 'managed', 'proxy')
class Options(object):
def __init__(self, meta, app_label=None):
@ -43,11 +43,15 @@ class Options(object):
self.has_auto_field, self.auto_field = False, None
self.abstract = False
self.managed = True
self.proxy = False
self.proxy_for_model = None
self.parents = SortedDict()
self.duplicate_targets = {}
# Managers that have been inherited from abstract base classes. These
# are passed onto any children.
# To handle various inheritance situations, we need to track where
# managers came from (concrete or abstract base classes).
self.abstract_managers = []
self.concrete_managers = []
def contribute_to_class(self, cls, name):
from django.db import connection
@ -164,6 +168,15 @@ class Options(object):
self.pk = field
field.serialize = False
def setup_proxy(self, target):
"""
Does the internal setup so that the current model is a proxy for
"target".
"""
self.pk = target._meta.pk
self.proxy_for_model = target
self.db_table = target._meta.db_table
def __repr__(self):
return '<Options for %s>' % self.object_name

View File

@ -641,6 +641,7 @@ class BaseQuery(object):
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
aliases = set()
proxied_model = opts.proxy and opts.proxy_for_model or 0
if start_alias:
seen = {None: start_alias}
for field, model in opts.get_fields_with_model():
@ -648,6 +649,9 @@ class BaseQuery(object):
try:
alias = seen[model]
except KeyError:
if model is proxied_model:
alias = start_alias
else:
link_field = opts.get_ancestor_link(model)
alias = self.join((start_alias, model._meta.db_table,
link_field.column, model._meta.pk.column))
@ -1158,8 +1162,12 @@ class BaseQuery(object):
opts = self.model._meta
root_alias = self.tables[0]
seen = {None: root_alias}
proxied_model = opts.proxy and opts.proxy_for_model or 0
for field, model in opts.get_fields_with_model():
if model not in seen:
if model is proxied_model:
seen[model] = root_alias
else:
link_field = opts.get_ancestor_link(model)
seen[model] = self.join((root_alias, model._meta.db_table,
link_field.column, model._meta.pk.column))
@ -1559,7 +1567,11 @@ class BaseQuery(object):
raise MultiJoin(pos + 1)
if model:
# The field lives on a base class of the current model.
proxied_model = opts.proxy and opts.proxy_for_model or 0
for int_model in opts.get_base_chain(model):
if int_model is proxied_model:
opts = int_model._meta
else:
lhs_col = opts.parents[int_model].column
dedupe = lhs_col in opts.duplicate_targets
if dedupe:
@ -1572,7 +1584,8 @@ class BaseQuery(object):
joins.append(alias)
exclusions.add(alias)
for (dupe_opts, dupe_col) in dupe_set:
self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
self.update_dupe_avoidance(dupe_opts, dupe_col,
alias)
cached_data = opts._join_cache.get(name)
orig_opts = opts
dupe_col = direct and field.column or field.field.column

View File

@ -162,6 +162,16 @@ that has ``admin`` set. This example specifies an extra permission,
This is a list or tuple of 2-tuples in the format ``(permission_code,
human_readable_permission_name)``.
``proxy``
---------
.. attribute:: Options.proxy
.. versionadded: 1.1
If set to ``True``, a model which subclasses another model will be treated as
a :ref:`proxy model <proxy-models>`.
``unique_together``
-------------------

View File

@ -195,6 +195,8 @@ attribute on the manager class. This is documented fully below_.
.. _below: manager-types_
.. _custom-managers-and-inheritance:
Custom managers and model inheritance
-------------------------------------

View File

@ -773,13 +773,18 @@ is whether you want the parent models to be models in their own right
of common information that will only be visible through the child
models.
Often, you will just want to use the parent class to hold information
that you don't want to have to type out for each child model. This
class isn't going to ever be used in isolation, so
:ref:`abstract-base-classes` are what you're after. However, if you're
subclassing an existing model (perhaps something from another
application entirely), or want each model to have its own database
table, :ref:`multi-table-inheritance` is the way to go.
There are three styles of inheritance that are possible in Django.
1. Often, you will just want to use the parent class to hold information that
you don't want to have to type out for each child model. This class isn't
going to ever be used in isolation, so :ref:`abstract-base-classes` are
what you're after.
2. If you're subclassing an existing model (perhaps something from another
application entirely) and want each model to have its own database table,
:ref:`multi-table-inheritance` is the way to go.
3. Finally, if you only want to modify the Python-level behaviour of a model,
without changing the models fields in any way, you can use
:ref:`proxy-models`.
.. _abstract-base-classes:
@ -937,14 +942,16 @@ referring to ``p.restaurant`` would raise a Restaurant.DoesNotExist exception.
In the multi-table inheritance situation, it doesn't make sense for a child
class to inherit from its parent's :ref:`Meta <meta-options>` class. All the :ref:`Meta <meta-options>` options
have already been applied to the parent class and applying them again would
normally only lead to contradictory behaviour (this is in contrast with the
normally only lead to contradictory behavior (this is in contrast with the
abstract base class case, where the base class doesn't exist in its own
right).
So a child model does not have access to its parent's :ref:`Meta <meta-options>` class. However,
there are a few limited cases where the child inherits behaviour from the
parent: if the child does not specify an :attr:`django.db.models.Options.ordering` attribute or a
:attr:`django.db.models.Options.get_latest_by` attribute, it will inherit these from its parent.
So a child model does not have access to its parent's :ref:`Meta
<meta-options>` class. However, there are a few limited cases where the child
inherits behavior from the parent: if the child does not specify an
:attr:`django.db.models.Options.ordering` attribute or a
:attr:`django.db.models.Options.get_latest_by` attribute, it will inherit
these from its parent.
If the parent has an ordering and you don't want the child to have any natural
ordering, you can explicitly disable it::
@ -990,6 +997,126 @@ own :class:`~django.db.models.fields.OneToOneField` and set
:attr:`parent_link=True <django.db.models.fields.OneToOneField.parent_link>`
to indicate that your field is the link back to the parent class.
.. _proxy-models:
Proxy models
------------
.. versionadded:: 1.1
When using :ref:`multi-table inheritance <multi-table-inheritance>`, a new
database table is created for each subclass of a model. This is usually the
desired behavior, since the subclass needs a place to store any additional
data fields that are not present on the base class. Sometimes, however, you
only want to change the Python behavior of a model -- perhaps to change the
default manager, or add a new method.
This is what proxy model inheritance is for: creating a *proxy* for the
original model. You can create, delete and update instances of the proxy model
and all the data will be saved as if you were using the original (non-proxied)
model. The difference is that you can change things like the default model
ordering or the default manager in the proxy, without having to alter the
original.
Proxy models are declared like normal models. You tell Django that it's a
proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute to of the ``Meta`` class to ``True``.
For example, suppose you want to add a method to the standard ``User`` model
that will make be used in your templates. You can do it like this::
from django.contrib.auth.models import User
class MyUser(User):
class Meta:
proxy = True
def do_something(self):
...
The ``MyUser`` class operates on the same database table as its parent
``User`` class. In particular, any new instances of ``User`` will also be
accessible through ``MyUser``, and vice-versa::
>>> u = User.objects.create(username="foobar")
>>> MyUser.objects.get(username="foobar")
<MyUser: foobar>
You could also use a proxy model to define a different default ordering on a
model. The standard ``User`` model has no ordering defined on it
(intentionally; sorting is expensive and we don't want to do it all the time
when we fetch users). You might want to regularly order by the ``username``
attribute when you use the proxy. This is easy::
class OrderedUser(User):
class Meta:
ordering = ["username"]
proxy = True
Now normal ``User`` queries will be unorderd and ``OrderedUser`` queries will
be ordered by ``username``.
Querysets still return the model that was requested
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There is no way to have Django return, say, a ``MyUser`` object whenever you
query for ``User`` objects. A queryset for ``User`` objects will return those
types of objects. The whole point of proxy objects is that code relying on the
original ``User`` will use those and your own code can use the extensions you
included (that no other code is relying on anyway). It is not a way to replace
the ``User`` (or any other) model everywhere with something of your own
creation.
Base class restrictions
~~~~~~~~~~~~~~~~~~~~~~~
A proxy model must inherit from exactly one non-abstract model class. You
can't inherit from multiple non-abstract models as the proxy model doesn't
provide any connection between the rows in the different database tables. A
proxy model can inherit from any number of abstract model classes, providing
they do *not* define any model fields.
Proxy models inherit any ``Meta`` options that they don't define from their
non-abstract model parent (the model they are proxying for).
Proxy model managers
~~~~~~~~~~~~~~~~~~~~
If you don't specify any model managers on a proxy model, it inherits the
managers from its model parents. If you define a manager on the proxy model,
it will become the default, although any managers defined on the parent
classes will still be available.
Continuing our example from above, you could change the default manager used
when you query the ``User`` model like this::
class NewManager(models.Manager):
...
class MyUser(User):
objects = NewManager()
class Meta:
proxy = True
If you wanted to add a new manager to the Proxy, without replacing the
existing default, you can use the techniques described in the :ref:`custom
manager <custom-managers-and-inheritance>` documentation: create a base class
containing the new managers and inherit that after the primary base class::
# Create an abstract class for the new manager.
class ExtraManagers:
secondary = NewManager()
class Meta:
abstract = True
class MyUser(User, ExtraManagers):
class Meta:
proxy = True
You probably won't need to do this very often, but, when you do, it's
possible.
Multiple inheritance
--------------------

View File

@ -0,0 +1,176 @@
"""
By specifying the 'proxy' Meta attribute, model subclasses can specify that
they will take data directly from the table of their base class table rather
than using a new table of their own. This allows them to act as simple proxies,
providing a modified interface to the data from the base class.
"""
from django.db import models
# A couple of managers for testing managing overriding in proxy model cases.
class PersonManager(models.Manager):
def get_query_set(self):
return super(PersonManager, self).get_query_set().exclude(name="fred")
class SubManager(models.Manager):
def get_query_set(self):
return super(SubManager, self).get_query_set().exclude(name="wilma")
class Person(models.Model):
"""
A simple concrete base class.
"""
name = models.CharField(max_length=50)
objects = PersonManager()
def __unicode__(self):
return self.name
class Abstract(models.Model):
"""
A simple abstract base class, to be used for error checking.
"""
data = models.CharField(max_length=10)
class Meta:
abstract = True
class MyPerson(Person):
"""
A proxy subclass, this should not get a new table. Overrides the default
manager.
"""
class Meta:
proxy = True
ordering = ["name"]
objects = SubManager()
other = PersonManager()
def has_special_name(self):
return self.name.lower() == "special"
class ManagerMixin(models.Model):
excluder = SubManager()
class Meta:
abstract = True
class OtherPerson(Person, ManagerMixin):
"""
A class with the default manager from Person, plus an secondary manager.
"""
class Meta:
proxy = True
ordering = ["name"]
class StatusPerson(MyPerson):
"""
A non-proxy subclass of a proxy, it should get a new table.
"""
status = models.CharField(max_length=80)
# We can even have proxies of proxies (and subclass of those).
class MyPersonProxy(MyPerson):
class Meta:
proxy = True
class LowerStatusPerson(MyPersonProxy):
status = models.CharField(max_length=80)
__test__ = {'API_TESTS' : """
# The MyPerson model should be generating the same database queries as the
# Person model (when the same manager is used in each case).
>>> MyPerson.other.all().query.as_sql() == Person.objects.order_by("name").query.as_sql()
True
# The StatusPerson models should have its own table (it's using ORM-level
# inheritance).
>>> StatusPerson.objects.all().query.as_sql() == Person.objects.all().query.as_sql()
False
# Creating a Person makes them accessible through the MyPerson proxy.
>>> _ = Person.objects.create(name="Foo McBar")
>>> len(Person.objects.all())
1
>>> len(MyPerson.objects.all())
1
>>> MyPerson.objects.get(name="Foo McBar").id
1
>>> MyPerson.objects.get(id=1).has_special_name()
False
# Person is not proxied by StatusPerson subclass, however.
>>> StatusPerson.objects.all()
[]
# A new MyPerson also shows up as a standard Person
>>> _ = MyPerson.objects.create(name="Bazza del Frob")
>>> len(MyPerson.objects.all())
2
>>> len(Person.objects.all())
2
>>> _ = LowerStatusPerson.objects.create(status="low", name="homer")
>>> LowerStatusPerson.objects.all()
[<LowerStatusPerson: homer>]
# And now for some things that shouldn't work...
#
# All base classes must be non-abstract
>>> class NoAbstract(Abstract):
... class Meta:
... proxy = True
Traceback (most recent call last):
....
TypeError: Abstract base class containing model fields not permitted for proxy model 'NoAbstract'.
# The proxy must actually have one concrete base class
>>> class TooManyBases(Person, Abstract):
... class Meta:
... proxy = True
Traceback (most recent call last):
....
TypeError: Abstract base class containing model fields not permitted for proxy model 'TooManyBases'.
>>> class NoBaseClasses(models.Model):
... class Meta:
... proxy = True
Traceback (most recent call last):
....
TypeError: Proxy model 'NoBaseClasses' has no non-abstract model base class.
# A proxy cannot introduce any new fields
>>> class NoNewFields(Person):
... newfield = models.BooleanField()
... class Meta:
... proxy = True
Traceback (most recent call last):
....
FieldError: Proxy model 'NoNewFields' contains model fields.
# Manager tests.
>>> Person.objects.all().delete()
>>> _ = Person.objects.create(name="fred")
>>> _ = Person.objects.create(name="wilma")
>>> _ = Person.objects.create(name="barney")
>>> MyPerson.objects.all()
[<MyPerson: barney>, <MyPerson: fred>]
>>> MyPerson._default_manager.all()
[<MyPerson: barney>, <MyPerson: fred>]
>>> OtherPerson.objects.all()
[<OtherPerson: barney>, <OtherPerson: wilma>]
>>> OtherPerson.excluder.all()
[<OtherPerson: barney>, <OtherPerson: fred>]
>>> OtherPerson._default_manager.all()
[<OtherPerson: barney>, <OtherPerson: wilma>]
"""}