1
0
mirror of https://github.com/django/django.git synced 2025-07-05 18:29:11 +00:00

queryset-refactor: Model inheritance support.

This adds both types of model inheritance: abstract base classes (ABCs) and
multi-table inheritance. See the documentation and tests / examples for details.

Still a few known bugs here, so don't file tickets (I know about them). Not
quite ready for prime-time usage, but it mostly works as expected.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7126 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-02-17 18:47:57 +00:00
parent 2d0588548e
commit da6570bf08
10 changed files with 607 additions and 138 deletions

View File

@ -26,7 +26,7 @@ def django_table_list(only_existing=False):
for app in models.get_apps():
for model in models.get_models(app):
tables.append(model._meta.db_table)
tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
tables.extend([f.m2m_db_table() for f in model._meta.local_many_to_many])
if only_existing:
existing = table_list()
tables = [t for t in tables if t in existing]
@ -54,12 +54,12 @@ def sequence_list():
for app in apps:
for model in models.get_models(app):
for f in model._meta.fields:
for f in model._meta.local_fields:
if isinstance(f, models.AutoField):
sequence_list.append({'table': model._meta.db_table, 'column': f.column})
break # Only one AutoField is allowed per model, so don't bother continuing.
for f in model._meta.many_to_many:
for f in model._meta.local_many_to_many:
sequence_list.append({'table': f.m2m_db_table(), 'column': None})
return sequence_list
@ -147,7 +147,7 @@ def sql_delete(app, style):
if cursor and table_name_converter(model._meta.db_table) in table_names:
# The table exists, so it needs to be dropped
opts = model._meta
for f in opts.fields:
for f in opts.local_fields:
if f.rel and f.rel.to not in to_delete:
references_to_delete.setdefault(f.rel.to, []).append( (model, f) )
@ -179,7 +179,7 @@ def sql_delete(app, style):
# Output DROP TABLE statements for many-to-many tables.
for model in app_models:
opts = model._meta
for f in opts.many_to_many:
for f in opts.local_many_to_many:
if isinstance(f.rel, generic.GenericRel):
continue
if cursor and table_name_converter(f.m2m_db_table()) in table_names:
@ -256,7 +256,7 @@ def sql_model_create(model, style, known_models=set()):
pending_references = {}
qn = connection.ops.quote_name
inline_references = connection.features.inline_fk_references
for f in opts.fields:
for f in opts.local_fields:
col_type = f.db_type()
tablespace = f.db_tablespace or opts.db_tablespace
if col_type is None:
@ -351,7 +351,7 @@ def many_to_many_sql_for_model(model, style):
final_output = []
qn = connection.ops.quote_name
inline_references = connection.features.inline_fk_references
for f in opts.many_to_many:
for f in opts.local_many_to_many:
if not isinstance(f.rel, generic.GenericRel):
tablespace = f.db_tablespace or opts.db_tablespace
if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
@ -458,7 +458,7 @@ def sql_indexes_for_model(model, style):
output = []
qn = connection.ops.quote_name
for f in model._meta.fields:
for f in model._meta.local_fields:
if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
unique = f.unique and 'UNIQUE ' or ''
tablespace = f.db_tablespace or model._meta.db_tablespace

View File

@ -32,7 +32,7 @@ def get_validation_errors(outfile, app=None):
opts = cls._meta
# Do field-specific validation.
for f in opts.fields:
for f in opts.local_fields:
if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name)
if f.name.endswith('_'):
@ -69,8 +69,8 @@ def get_validation_errors(outfile, app=None):
if db_version < (5, 0, 3) and isinstance(f, (models.CharField, models.CommaSeparatedIntegerField, models.SlugField)) and f.max_length > 255:
e.add(opts, '"%s": %s cannot have a "max_length" greater than 255 when you are using a version of MySQL prior to 5.0.3 (you are using %s).' % (f.name, f.__class__.__name__, '.'.join([str(n) for n in db_version[:3]])))
# Check to see if the related field will clash with any
# existing fields, m2m fields, m2m related objects or related objects
# Check to see if the related field will clash with any existing
# fields, m2m fields, m2m related objects or related objects
if f.rel:
if f.rel.to not in models.get_models():
e.add(opts, "'%s' has relation with model %s, which has not been installed" % (f.name, f.rel.to))
@ -87,7 +87,7 @@ def get_validation_errors(outfile, app=None):
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
if r.name == rel_query_name:
e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
for r in rel_opts.many_to_many:
for r in rel_opts.local_many_to_many:
if r.name == rel_name:
e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
if r.name == rel_query_name:
@ -104,9 +104,10 @@ def get_validation_errors(outfile, app=None):
if r.get_accessor_name() == rel_query_name:
e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
for i, f in enumerate(opts.many_to_many):
for i, f in enumerate(opts.local_many_to_many):
# Check to see if the related m2m field will clash with any
# existing fields, m2m fields, m2m related objects or related objects
# existing fields, m2m fields, m2m related objects or related
# objects
if f.rel.to not in models.get_models():
e.add(opts, "'%s' has m2m relation with model %s, which has not been installed" % (f.name, f.rel.to))
# it is a string and we could not find the model it refers to
@ -117,17 +118,17 @@ def get_validation_errors(outfile, app=None):
rel_opts = f.rel.to._meta
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
rel_query_name = f.related_query_name()
# If rel_name is none, there is no reverse accessor.
# (This only occurs for symmetrical m2m relations to self).
# If this is the case, there are no clashes to check for this field, as
# there are no reverse descriptors for this field.
# If rel_name is none, there is no reverse accessor (this only
# occurs for symmetrical m2m relations to self). If this is the
# case, there are no clashes to check for this field, as there are
# no reverse descriptors for this field.
if rel_name is not None:
for r in rel_opts.fields:
if r.name == rel_name:
e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
if r.name == rel_query_name:
e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
for r in rel_opts.many_to_many:
for r in rel_opts.local_many_to_many:
if r.name == rel_name:
e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
if r.name == rel_query_name:

View File

@ -1,9 +1,14 @@
import types
import sys
import os
from itertools import izip
import django.db.models.manipulators
import django.db.models.manager
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q
from django.db.models.options import Options, AdminOptions
from django.db import connection, transaction
@ -14,10 +19,6 @@ from django.utils.datastructures import SortedDict
from django.utils.functional import curry
from django.utils.encoding import smart_str, force_unicode, smart_unicode
from django.conf import settings
from itertools import izip
import types
import sys
import os
class ModelBase(type):
"Metaclass for all models"
@ -25,29 +26,46 @@ class ModelBase(type):
# If this isn't a subclass of Model, don't do anything special.
try:
parents = [b for b in bases if issubclass(b, Model)]
if not parents:
return super(ModelBase, cls).__new__(cls, name, bases, attrs)
except NameError:
# 'Model' isn't defined yet, meaning we're looking at Django's own
# Model class, defined below.
parents = []
if not parents:
return super(ModelBase, cls).__new__(cls, name, bases, attrs)
# Create the class.
new_class = type.__new__(cls, name, bases, {'__module__': attrs.pop('__module__')})
new_class.add_to_class('_meta', Options(attrs.pop('Meta', None)))
meta = attrs.pop('Meta', None)
# FIXME: Promote Meta to a newstyle class before attaching it to the
# model.
## if meta:
## new_class.Meta = meta
new_class.add_to_class('_meta', Options(meta))
# FIXME: Need to be smarter here. Exception is an old-style class in
# Python <= 2.4, new-style in Python 2.5+. This construction is only
# really correct for old-style classes.
new_class.add_to_class('DoesNotExist', types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {}))
new_class.add_to_class('MultipleObjectsReturned',
types.ClassType('MultipleObjectsReturned', (MultipleObjectsReturned, ), {}))
new_class.add_to_class('MultipleObjectsReturned', types.ClassType('MultipleObjectsReturned', (MultipleObjectsReturned, ), {}))
# Build complete list of parents
# Do the appropriate setup for any model parents.
abstract_parents = []
for base in parents:
# Things without _meta aren't functional models, so they're
# uninteresting parents.
if hasattr(base, '_meta'):
new_class._meta.parents.append(base)
new_class._meta.parents.extend(base._meta.parents)
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
if not base._meta.abstract:
attr_name = '%s_ptr' % base._meta.module_name
field = OneToOneField(base, name=attr_name, auto_created=True)
new_class.add_to_class(attr_name, field)
new_class._meta.parents[base] = field
else:
abstract_parents.append(base)
if getattr(new_class, '_default_manager', None) is not None:
# We have a parent who set the default manager. We need to override
# this.
new_class._default_manager = None
if getattr(new_class._meta, 'app_label', None) is None:
# Figure out the app_label by looking one level up.
# For 'django.contrib.sites.models', this would be 'sites'.
@ -63,21 +81,26 @@ class ModelBase(type):
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
# Add Fields inherited from parents
for parent in new_class._meta.parents:
for field in parent._meta.fields:
# Only add parent fields if they aren't defined for this class.
try:
new_class._meta.get_field(field.name)
except FieldDoesNotExist:
field.contribute_to_class(new_class, field.name)
for parent in abstract_parents:
names = [f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many]
for field in parent._meta.local_fields:
if field.name in names:
raise TypeError('Local field %r in class %r clashes with field of similar name from abstract base class %r'
% (field.name, name, parent.__name__))
new_class.add_to_class(field.name, field)
if new_class._meta.abstract:
# Abstract base models can't be instantiated and don't appear in
# the list of models for an app. We do the final setup for them a
# little differently from normal models.
return new_class
new_class._prepare()
register_models(new_class._meta.app_label, new_class)
# Because of the way imports happen (recursively), we may or may not be
# the first class for this model to register with the framework. There
# should only be one class for each model, so we must always return the
# the first time this model tries to register with the framework. There
# should only be one class for each model, so we always return the
# registered version.
return get_model(new_class._meta.app_label, name, False)
@ -113,8 +136,10 @@ class ModelBase(type):
class Model(object):
__metaclass__ = ModelBase
def _get_pk_val(self):
return getattr(self, self._meta.pk.attname)
def _get_pk_val(self, meta=None):
if not meta:
meta = self._meta
return getattr(self, meta.pk.attname)
def _set_pk_val(self, value):
return setattr(self, self._meta.pk.attname, value)
@ -207,19 +232,30 @@ class Model(object):
raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]
dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self)
def save(self, raw=False):
dispatcher.send(signal=signals.pre_save, sender=self.__class__,
instance=self, raw=raw)
def save(self, raw=False, cls=None):
if not cls:
dispatcher.send(signal=signals.pre_save, sender=self.__class__,
instance=self, raw=raw)
cls = self.__class__
meta = self._meta
signal = True
else:
meta = cls._meta
signal = False
non_pks = [f for f in self._meta.fields if not f.primary_key]
for parent, field in meta.parents.items():
self.save(raw, parent)
setattr(self, field.attname, self._get_pk_val(parent._meta))
non_pks = [f for f in self._meta.local_fields if not f.primary_key]
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
pk_val = self._get_pk_val()
pk_val = self._get_pk_val(meta)
# Note: the comparison with '' is required for compatibility with
# oldforms-style model creation.
pk_set = pk_val is not None and smart_unicode(pk_val) != u''
record_exists = True
manager = self.__class__._default_manager
manager = cls._default_manager
if pk_set:
# Determine whether a record with the primary key already exists.
if manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by():
@ -231,16 +267,16 @@ class Model(object):
record_exists = False
if not pk_set or not record_exists:
if not pk_set:
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in self._meta.fields if not isinstance(f, AutoField)]
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)]
else:
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in self._meta.fields]
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields]
if self._meta.order_with_respect_to:
field = self._meta.order_with_respect_to
if meta.order_with_respect_to:
field = meta.order_with_respect_to
values.append(('_order', manager.filter(**{field.name: getattr(self, field.attname)}).count()))
record_exists = False
update_pk = bool(self._meta.has_auto_field and not pk_set)
update_pk = bool(meta.has_auto_field and not pk_set)
if values:
# Create a new record.
result = manager._insert(_return_id=update_pk, **dict(values))
@ -250,12 +286,13 @@ class Model(object):
_raw_values=True, pk=connection.ops.pk_default_value())
if update_pk:
setattr(self, self._meta.pk.attname, result)
setattr(self, meta.pk.attname, result)
transaction.commit_unless_managed()
# Run any post-save hooks.
dispatcher.send(signal=signals.post_save, sender=self.__class__,
instance=self, created=(not record_exists), raw=raw)
if signal:
# Run any post-save hooks.
dispatcher.send(signal=signals.post_save, sender=self.__class__,
instance=self, created=(not record_exists), raw=raw)
save.alters_data = True

View File

@ -75,15 +75,19 @@ class Field(object):
# database level.
empty_strings_allowed = True
# Tracks each time a Field instance is created. Used to retain order.
# These track each time a Field instance is created. Used to retain order.
# The auto_creation_counter is used for fields that Django implicitly
# creates, creation_counter is used for all user-specified fields.
creation_counter = 0
auto_creation_counter = -1
def __init__(self, verbose_name=None, name=None, primary_key=False,
max_length=None, unique=False, blank=False, null=False, db_index=False,
core=False, rel=None, default=NOT_PROVIDED, editable=True, serialize=True,
prepopulate_from=None, unique_for_date=None, unique_for_month=None,
unique_for_year=None, validator_list=None, choices=None, radio_admin=None,
help_text='', db_column=None, db_tablespace=None):
max_length=None, unique=False, blank=False, null=False,
db_index=False, core=False, rel=None, default=NOT_PROVIDED,
editable=True, serialize=True, prepopulate_from=None,
unique_for_date=None, unique_for_month=None, unique_for_year=None,
validator_list=None, choices=None, radio_admin=None, help_text='',
db_column=None, db_tablespace=None, auto_created=False):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@ -109,9 +113,13 @@ class Field(object):
# Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
self.db_index = db_index
# Increase the creation counter, and save our local copy.
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
# Adjust the appropriate creation counter, and save our local copy.
if auto_created:
self.creation_counter = Field.auto_creation_counter
Field.auto_creation_counter -= 1
else:
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
def __cmp__(self, other):
# This is needed because bisect does not take a comparison function.

View File

@ -494,8 +494,9 @@ class OneToOneRel(ManyToOneRel):
# ignored here. We accept them as parameters only to match the calling
# signature of ManyToOneRel.__init__().
super(OneToOneRel, self).__init__(to, field_name, num_in_admin,
edit_inline, related_name, limit_choices_to, lookup_overrides,
raw_id_admin)
edit_inline=edit_inline, related_name=related_name,
limit_choices_to=limit_choices_to,
lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin)
self.multiple = False
class ManyToManyRel(object):
@ -754,7 +755,7 @@ class ManyToManyField(RelatedField, Field):
def save_form_data(self, instance, data):
setattr(instance, self.attname, data)
def formfield(self, **kwargs):
defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()}
defaults.update(kwargs)

View File

@ -5,7 +5,7 @@ from django.db.models.fields import FieldDoesNotExist
def ensure_default_manager(sender):
cls = sender
if not hasattr(cls, '_default_manager'):
if not hasattr(cls, '_default_manager') or cls._default_manager is None:
# Create the default manager, if needed.
try:
cls._meta.get_field('objects')
@ -31,7 +31,7 @@ class Manager(object):
# TODO: Use weakref because of possible memory leak / circular reference.
self.model = model
setattr(model, name, ManagerDescriptor(self))
if not hasattr(model, '_default_manager') or self.creation_counter < model._default_manager.creation_counter:
if not hasattr(model, '_default_manager') or model._default_manager is None or self.creation_counter < model._default_manager.creation_counter:
model._default_manager = self
#######################

View File

@ -1,3 +1,6 @@
import re
from bisect import bisect
from django.conf import settings
from django.db.models.related import RelatedObject
from django.db.models.fields.related import ManyToManyRel
@ -7,19 +10,19 @@ from django.db.models.loading import get_models, app_cache_ready
from django.db.models import Manager
from django.utils.translation import activate, deactivate_all, get_language, string_concat
from django.utils.encoding import force_unicode, smart_str
from bisect import bisect
import re
from django.utils.datastructures import SortedDict
# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace')
'order_with_respect_to', 'app_label', 'db_tablespace',
'abstract')
class Options(object):
def __init__(self, meta):
self.fields, self.many_to_many = [], []
self.local_fields, self.local_many_to_many = [], []
self.module_name, self.verbose_name = None, None
self.verbose_name_plural = None
self.db_table = ''
@ -35,7 +38,8 @@ class Options(object):
self.pk = None
self.has_auto_field, self.auto_field = False, None
self.one_to_one_field = None
self.parents = []
self.abstract = False
self.parents = SortedDict()
def contribute_to_class(self, cls, name):
cls._meta = self
@ -82,9 +86,16 @@ class Options(object):
self.order_with_respect_to = None
if self.pk is None:
auto = AutoField(verbose_name='ID', primary_key=True)
auto.creation_counter = -1
model.add_to_class('id', auto)
if self.parents:
# Promote the first parent link in lieu of adding yet another
# field.
field = self.parents.value_for_index(0)
field.primary_key = True
self.pk = field
else:
auto = AutoField(verbose_name='ID', primary_key=True,
auto_created=True)
model.add_to_class('id', auto)
# If the db_table wasn't provided, use the app_label + module_name.
if not self.db_table:
@ -97,13 +108,23 @@ class Options(object):
# Move many-to-many related fields from self.fields into
# self.many_to_many.
if field.rel and isinstance(field.rel, ManyToManyRel):
self.many_to_many.insert(bisect(self.many_to_many, field), field)
self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
else:
self.fields.insert(bisect(self.fields, field), field)
self.local_fields.insert(bisect(self.local_fields, field), field)
if not self.pk and field.primary_key:
self.pk = field
field.serialize = False
# All of these internal caches need to be updated the next time they
# are used.
# TODO: Do this more neatly. (Also, use less caches!)
if hasattr(self, '_field_cache'):
del self._field_cache
if hasattr(self, '_m2m_cache'):
del self._m2m_cache
if hasattr(self, '_name_map'):
del self._name_map
def __repr__(self):
return '<Options for %s>' % self.object_name
@ -123,8 +144,76 @@ class Options(object):
return raw
verbose_name_raw = property(verbose_name_raw)
def _fields(self):
"""
The getter for self.fields. This returns the list of field objects
available to this model (including through parent models).
"""
try:
self._field_cache
except AttributeError:
self._fill_fields_cache()
return self._field_cache.keys()
fields = property(_fields)
def get_fields_with_model(self):
"""
Returns a list of (field, model) pairs for all fields. The "model"
element is None for fields on the current model. Mostly of use when
constructing queries so that we know which model a field belongs to.
"""
try:
self._field_cache
except AttributeError:
self._fill_fields_cache()
return self._field_cache.items()
def _fill_fields_cache(self):
cache = SortedDict()
for parent in self.parents:
for field, model in parent._meta.get_fields_with_model():
if model:
cache[field] = model
else:
cache[field] = parent
for field in self.local_fields:
cache[field] = None
self._field_cache = cache
def _many_to_many(self):
try:
self._m2m_cache
except AttributeError:
self._fill_m2m_cache()
return self._m2m_cache.keys()
many_to_many = property(_many_to_many)
def get_m2m_with_model(self):
"""
The many-to-many version of get_fields_with_model().
"""
try:
self._m2m_cache
except AttributeError:
self._fill_m2m_cache()
return self._m2m_cache.items()
def _fill_m2m_cache(self):
cache = SortedDict()
for parent in self.parents:
for field, model in parent._meta.get_m2m_with_model():
if model:
cache[field] = model
else:
cache[field] = parent
for field in self.local_many_to_many:
cache[field] = None
self._m2m_cache = cache
def get_field(self, name, many_to_many=True):
"Returns the requested field by name. Raises FieldDoesNotExist on error."
"""
Returns the requested field by name. Raises FieldDoesNotExist on error.
"""
to_search = many_to_many and (self.fields + self.many_to_many) or self.fields
for f in to_search:
if f.name == name:
@ -133,8 +222,9 @@ class Options(object):
def get_field_by_name(self, name, only_direct=False):
"""
Returns the (field_object, direct, m2m), where field_object is the
Field instance for the given name, direct is True if the field exists
Returns the (field_object, model, direct, m2m), where field_object is
the Field instance for the given name, model is the model containing
this field (None for local fields), direct is True if the field exists
on this model, and m2m is True for many-to-many relations. When
'direct' is False, 'field_object' is the corresponding RelatedObject
for this field (since the field doesn't have an instance associated
@ -151,7 +241,7 @@ class Options(object):
cache = self.init_name_map()
result = cache.get(name)
if not result or (not result[1] and only_direct):
if not result or (only_direct and not result[2]):
raise FieldDoesNotExist('%s has no field named %r'
% (self.object_name, name))
return result
@ -173,15 +263,16 @@ class Options(object):
"""
Initialises the field name -> field object mapping.
"""
cache = dict([(f.name, (f, True, False)) for f in self.fields])
for f in self.many_to_many:
cache[f.name] = (f, True, True)
for f in self.get_all_related_many_to_many_objects():
cache[f.field.related_query_name()] = (f, False, True)
for f in self.get_all_related_objects():
cache[f.field.related_query_name()] = (f, False, False)
cache = dict([(f.name, (f, m, True, False)) for f, m in
self.get_fields_with_model()])
for f, model in self.get_m2m_with_model():
cache[f.name] = (f, model, True, True)
for f, model in self.get_all_related_m2m_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, True)
for f, model in self.get_all_related_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, False)
if self.order_with_respect_to:
cache['_order'] = OrderWrt(), True, False
cache['_order'] = OrderWrt(), None, True, False
if app_cache_ready():
self._name_map = cache
return cache
@ -195,17 +286,81 @@ class Options(object):
def get_delete_permission(self):
return 'delete_%s' % self.object_name.lower()
def get_all_related_objects(self):
try: # Try the cache first.
return self._all_related_objects
def get_all_related_objects(self, local_only=False):
try:
self._related_objects_cache
except AttributeError:
rel_objs = []
for klass in get_models():
for f in klass._meta.fields:
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
rel_objs.append(RelatedObject(f.rel.to, klass, f))
self._all_related_objects = rel_objs
return rel_objs
self._fill_related_objects_cache()
if local_only:
return [k for k, v in self._related_objects_cache.items() if not v]
return self._related_objects_cache.keys()
def get_all_related_objects_with_model(self):
"""
Returns a list of (related-object, model) pairs. Similar to
get_fields_with_model().
"""
try:
self._related_objects_cache
except AttributeError:
self._fill_related_objects_cache()
return self._related_objects_cache.items()
def _fill_related_objects_cache(self):
cache = SortedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_objects_with_model():
if obj.field.creation_counter < 0 and obj.model not in parent_list:
continue
if not model:
cache[obj] = parent
else:
cache[obj] = model
for klass in get_models():
for f in klass._meta.local_fields:
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
cache[RelatedObject(f.rel.to, klass, f)] = None
self._related_objects_cache = cache
def get_all_related_many_to_many_objects(self, local_only=False):
try:
cache = self._related_many_to_many_cache
except AttributeError:
cache = self._fill_related_many_to_many_cache()
if local_only:
return [k for k, v in cache.items() if not v]
return cache.keys()
def get_all_related_m2m_objects_with_model(self):
"""
Returns a list of (related-m2m-object, model) pairs. Similar to
get_fields_with_model().
"""
try:
cache = self._related_many_to_many_cache
except AttributeError:
cache = self._fill_related_many_to_many_cache()
return cache.items()
def _fill_related_many_to_many_cache(self):
cache = SortedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
if obj.field.creation_counter < 0 and obj.model not in parent_list:
continue
if not model:
cache[obj] = parent
else:
cache[obj] = model
for klass in get_models():
for f in klass._meta.local_many_to_many:
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
cache[RelatedObject(f.rel.to, klass, f)] = None
if app_cache_ready():
self._related_many_to_many_cache = cache
return cache
def get_followed_related_objects(self, follow=None):
if follow == None:
@ -229,18 +384,35 @@ class Options(object):
follow[f.name] = fol
return follow
def get_all_related_many_to_many_objects(self):
try: # Try the cache first.
return self._all_related_many_to_many_objects
except AttributeError:
rel_objs = []
for klass in get_models():
for f in klass._meta.many_to_many:
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
rel_objs.append(RelatedObject(f.rel.to, klass, f))
if app_cache_ready():
self._all_related_many_to_many_objects = rel_objs
return rel_objs
def get_base_chain(self, model):
"""
Returns a list of parent classes leading to 'model' (order from closet
to most distant ancestor). This has to handle the case were 'model' is
a granparent or even more distant relation.
"""
if not self.parents:
return
if model in self.parents:
return [model]
for parent in self.parents:
res = parent._meta.get_base_chain(model)
if res:
res.insert(0, parent)
return res
raise TypeError('%r is not an ancestor of this model'
% model._meta.module_name)
def get_parent_list(self):
"""
Returns a list of all the ancestor of this model as a list. Useful for
determining if something is an ancestor, regardless of lineage.
"""
# FIXME: Fix model hashing and then use a Set here.
result = []
for parent in self.parents:
result.append(parent)
result.extend(parent._meta.get_parent_list())
return result
def get_ordered_objects(self):
"Returns a list of Options objects that are ordered with respect to this object."

View File

@ -389,8 +389,13 @@ class Query(object):
aliases.append(col.alias)
elif self.default_cols:
table_alias = self.tables[0]
result = ['%s.%s' % (qn(table_alias), qn(f.column))
for f in self.model._meta.fields]
root_pk = self.model._meta.pk.column
seen = {None: table_alias}
for field, model in self.model._meta.get_fields_with_model():
if model not in seen:
seen[model] = self.join((table_alias, model._meta.db_table,
root_pk, model._meta.pk.column))
result.append('%s.%s' % (qn(seen[model]), qn(field.column)))
aliases = result[:]
result.extend(['(%s) AS %s' % (col, alias)
@ -742,7 +747,7 @@ class Query(object):
opts = self.model._meta
alias = self.join((None, opts.db_table, None, None))
field, target, unused, join_list, = self.setup_joins(parts, opts,
field, target, opts, join_list, = self.setup_joins(parts, opts,
alias, (connector == AND))
col = target.column
alias = join_list[-1][-1]
@ -850,11 +855,21 @@ class Query(object):
name = opts.pk.name
try:
field, direct, m2m = opts.get_field_by_name(name)
field, model, direct, m2m = opts.get_field_by_name(name)
except FieldDoesNotExist:
names = opts.get_all_field_names()
raise TypeError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (name, ", ".join(names)))
if model:
# The field lives on a base class of the current model.
alias_list = []
for int_model in opts.get_base_chain(model):
lhs_col = opts.parents[int_model].column
opts = int_model._meta
alias = self.join((alias, opts.db_table, lhs_col,
opts.pk.column))
alias_list.append(alias)
joins.append(alias_list)
cached_data = opts._join_cache.get(name)
orig_opts = opts
@ -899,6 +914,7 @@ class Query(object):
nullable=field.null)
joins.append([alias])
else:
# Non-relation fields.
target = field
break
else:
@ -1242,7 +1258,7 @@ class UpdateQuery(Query):
def add_update_values(self, values):
from django.db.models.base import Model
for name, val in values.items():
field, direct, m2m = self.model._meta.get_field_by_name(name)
field, model, direct, m2m = self.model._meta.get_field_by_name(name)
if not direct or m2m:
# Can only update non-relation fields and foreign keys.
raise TypeError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field)

View File

@ -2028,6 +2028,105 @@ You can also prevent saving::
.. _database API docs: ../db-api/
Model inheritance
=================
Abstract base classes
---------------------
Abstract base classes are useful when you want to put some common information
into a number of other models. You write your base class and put
``abstract=True`` in the ``Meta`` class. This model will then not be used to
create any database table. Instead, when it is used as a base class for other
models, its fields will be added to those of the child class. It is an error
to have fields in the abstract base class with the same name as those in the
child (and Django will raise an exception).
An example::
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class Student(CommonInfo):
home_group = models.CharField(max_length=5)
The ``Student`` model will have three fields: ``name``, ``age`` and
``home_group``. The ``CommonInfo`` model cannot be used as a normal Django
model, since it is an abstract base class. It does not generate a database
table or have a manager or anything like that.
For many uses, this type of model inheritance will be exactly what you want.
It provides a way to factor out common information at the Python level, whilst
still only creating one database table per child model at the database level.
Multi-table inheritance
-----------------------
The second type of model inheritance supported by Django is when each model in
the hierarchy is a model all by itself. Each model corresponds to its own
database table and can be queried and created indvidually. The inheritance
relationship introduces links between the child model and each of its parents
(via an automatically created ``OneToOneField``). For example::
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField()
All of the fields of ``Place`` will also be available in ``Restaurant``,
although the data will reside in a different database table. So these are both
possible::
>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")
If you have a ``Place`` that is also a ``Restaurant``, you can get from the
``Place`` object to the ``Restaurant`` object by using the lower-case version
of the model name::
>>> p = Place.objects.filter(name="Bob's Cafe")
# If Bob's Cafe is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>
However, if ``p`` in the above example was *not* a ``Restaurant`` (it had been
created directly as a ``Place`` object or was the parent of some other class),
referring to ``p.restaurant`` would give an error.
Normally you won't need to worry too much about how model inheritance works.
It will behave similarly to Python class inheritance.
Inheritance and reverse relations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Because multi-table inheritance uses an implicit ``OneToOneField`` to link the
child and the parent, it's possible to move from the parent down to the child,
as in the above example. However, this uses up the name that is the default
``related_name`` value for ``ForeignKey`` and ``ManyToManyField`` relations.
If you are putting those type of relations on a subclass of another model, you
**must** specify the ``related_name`` attribute on each such field. If you
forget, Django will raise an error when you run ``manage.py validate`` or try
to syncdb.
For example, using the above ``Place`` class again, let's create another
subclass with a ``ManyToManyField``::
class Supplier(Place):
# Must specify related_name on all relations.
customers = models.ManyToManyField(Restaurant,
related_name='provider')
For more information about reverse relations, refer to the `Database API
reference`_ . For now, just remember to run ``manage.py validate`` when
you're writing your models and pay attention to the error messages.
Models across files
===================

View File

@ -1,11 +1,35 @@
"""
XX. Model inheritance
Model inheritance isn't yet supported.
Model inheritance exists in two varieties:
- abstract base classes which are a way of specifying common
information inherited by the subclasses. They don't exist as a separate
model.
- non-abstract base classes (the default), which are models in their own
right with their own database tables and everything. Their subclasses
have references back to them, created automatically.
Both styles are demonstrated here.
"""
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=50)
age = models.PositiveIntegerField()
class Meta:
abstract = True
def __unicode__(self):
return u'%s %s' % (self.__class__.__name__, self.name)
class Worker(CommonInfo):
job = models.CharField(max_length=50)
class Student(CommonInfo):
school_class = models.CharField(max_length=10)
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
@ -26,16 +50,44 @@ class ItalianRestaurant(Restaurant):
def __unicode__(self):
return u"%s the italian restaurant" % self.name
# XFAIL: Recent changes to model saving mean these now fail catastrophically.
# They'll be re-enabled when the porting is a bit further along.
not__test__ = {'API_TESTS':"""
# Make sure Restaurant has the right fields in the right order.
>>> [f.name for f in Restaurant._meta.fields]
['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza']
class Supplier(Place):
customers = models.ManyToManyField(Restaurant, related_name='provider')
# Make sure ItalianRestaurant has the right fields in the right order.
>>> [f.name for f in ItalianRestaurant._meta.fields]
['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza', 'serves_gnocchi']
def __unicode__(self):
return u"%s the supplier" % self.name
class ParkingLot(Place):
main_site = models.ForeignKey(Place, related_name='lot')
def __unicode__(self):
return u"%s the parking lot" % self.name
__test__ = {'API_TESTS':"""
# The Student and Worker models both have 'name' and 'age' fields on them and
# inherit the __unicode__() method, just as with normal Python subclassing.
# This is useful if you want to factor out common information for programming
# purposes, but still completely independent separate models at the database
# level.
>>> w = Worker(name='Fred', age=35, job='Quarry worker')
>>> w.save()
>>> s = Student(name='Pebbles', age=5, school_class='1B')
>>> s.save()
>>> unicode(w)
u'Worker Fred'
>>> unicode(s)
u'Student Pebbles'
# However, the CommonInfo class cannot be used as a normal model (it doesn't
# exist as a model).
>>> CommonInfo.objects.all()
Traceback (most recent call last):
...
AttributeError: type object 'CommonInfo' has no attribute 'objects'
# The Place/Restaurant/ItalianRestaurant models, on the other hand, all exist
# as independent models. However, the subclasses also have transparent access
# to the fields of their ancestors.
# Create a couple of Places.
>>> p1 = Place(name='Master Shakes', address='666 W. Jersey')
@ -43,7 +95,7 @@ not__test__ = {'API_TESTS':"""
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
# Test constructor for Restaurant.
Test constructor for Restaurant.
>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
@ -51,5 +103,88 @@ not__test__ = {'API_TESTS':"""
>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Elm', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True)
>>> ir.save()
# Make sure Restaurant and ItalianRestaurant have the right fields in the right
# order.
>>> [f.name for f in Restaurant._meta.fields]
['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza']
>>> [f.name for f in ItalianRestaurant._meta.fields]
['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi']
# Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant
# object cannot access that reverse relation, since it's not part of the
# Place-Supplier Hierarchy.
>>> Place.objects.filter(supplier__name='foo')
[]
>>> Restaurant.objects.filter(supplier__name='foo')
Traceback (most recent call last):
...
TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, serves_hot_dogs, serves_pizza
# Parent fields can be used directly in filters on the child model.
>>> Restaurant.objects.filter(name='Demon Dogs')
[<Restaurant: Demon Dogs the restaurant>]
>>> ItalianRestaurant.objects.filter(address='1234 W. Elm')
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
# Filters against the parent model return objects of the parent's type.
>>> Place.objects.filter(name='Demon Dogs')
[<Place: Demon Dogs the place>]
# Since the parent and child are linked by an automatically created
# OneToOneField, you can get from the parent to the child by using the child's
# name.
>>> place = Place.objects.get(name='Demon Dogs')
>>> place.restaurant
<Restaurant: Demon Dogs the restaurant>
>>> Place.objects.get(name='Ristorante Miron').restaurant.italianrestaurant
<ItalianRestaurant: Ristorante Miron the italian restaurant>
>>> Restaurant.objects.get(name='Ristorante Miron').italianrestaurant
<ItalianRestaurant: Ristorante Miron the italian restaurant>
# This won't work because the Demon Dogs restaurant is not an Italian
# restaurant.
>>> place.restaurant.italianrestaurant
Traceback (most recent call last):
...
DoesNotExist: ItalianRestaurant matching query does not exist.
# Related objects work just as they normally do.
>>> s1 = Supplier(name="Joe's Chickens", address='123 Sesame St')
>>> s1.save()
>>> s1.customers = [r, ir]
>>> s2 = Supplier(name="Luigi's Pasta", address='456 Sesame St')
>>> s2.save()
>>> s2.customers = [ir]
# This won't work because the Place we select is not a Restaurant (it's a
# Supplier).
>>> p = Place.objects.get(name="Joe's Chickens")
>>> p.restaurant
Traceback (most recent call last):
...
DoesNotExist: Restaurant matching query does not exist.
# But we can descend from p to the Supplier child, as expected.
>>> p.supplier
<Supplier: Joe's Chickens the supplier>
>>> ir.provider.order_by('-name')
[<Supplier: Luigi's Pasta the supplier>, <Supplier: Joe's Chickens the supplier>]
>>> Restaurant.objects.filter(provider__name__contains="Chickens")
[<Restaurant: Demon Dogs the restaurant>, <Restaurant: Ristorante Miron the restaurant>]
>>> ItalianRestaurant.objects.filter(provider__name__contains="Chickens")
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
>>> park1 = ParkingLot(name='Main St', address='111 Main St', main_site=s1)
>>> park1.save()
>>> park2 = ParkingLot(name='Well Lit', address='124 Sesame St', main_site=ir)
>>> park2.save()
>>> Restaurant.objects.get(lot__name='Well Lit')
<Restaurant: Ristorante Miron the restaurant>
"""}