Introduce `ContentType.objects.get_for_models(*models)` and use it in the the auth permissions code. This is a solid performance gain on the test suite. Thanks to ptone for the profiling to find this hotspot, and carl for the review.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16963 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Alex Gaynor 2011-10-12 20:51:59 +00:00
parent a5e691e5de
commit f04af7080b
4 changed files with 96 additions and 10 deletions

View File

@ -31,8 +31,8 @@ def create_permissions(app, created_models, verbosity, **kwargs):
searched_perms = list() searched_perms = list()
# The codenames and ctypes that should exist. # The codenames and ctypes that should exist.
ctypes = set() ctypes = set()
for klass in app_models: ctypes_for_models = ContentType.objects.get_for_models(*app_models)
ctype = ContentType.objects.get_for_model(klass) for klass, ctype in ctypes_for_models.iteritems():
ctypes.add(ctype) ctypes.add(ctype)
for perm in _get_all_permissions(klass._meta): for perm in _get_all_permissions(klass._meta):
searched_perms.append((ctype, perm)) searched_perms.append((ctype, perm))

View File

@ -15,19 +15,26 @@ class ContentTypeManager(models.Manager):
ct = self.get(app_label=app_label, model=model) ct = self.get(app_label=app_label, model=model)
return ct return ct
def _get_opts(self, model):
opts = model._meta
while opts.proxy:
model = opts.proxy_for_model
opts = model._meta
return opts
def _get_from_cache(self, opts):
key = (opts.app_label, opts.object_name.lower())
return self.__class__._cache[self.db][key]
def get_for_model(self, model): def get_for_model(self, model):
""" """
Returns the ContentType object for a given model, creating the Returns the ContentType object for a given model, creating the
ContentType if necessary. Lookups are cached so that subsequent lookups ContentType if necessary. Lookups are cached so that subsequent lookups
for the same model don't hit the database. for the same model don't hit the database.
""" """
opts = model._meta opts = self._get_opts(model)
while opts.proxy:
model = opts.proxy_for_model
opts = model._meta
key = (opts.app_label, opts.object_name.lower())
try: try:
ct = self.__class__._cache[self.db][key] ct = self._get_from_cache(opts)
except KeyError: except KeyError:
# Load or create the ContentType entry. The smart_unicode() is # Load or create the ContentType entry. The smart_unicode() is
# needed around opts.verbose_name_raw because name_raw might be a # needed around opts.verbose_name_raw because name_raw might be a
@ -41,6 +48,48 @@ class ContentTypeManager(models.Manager):
return ct return ct
def get_for_models(self, *models):
"""
Given *models, returns a dictionary mapping {model: content_type}.
"""
# Final results
results = {}
# models that aren't already in the cache
needed_app_labels = set()
needed_models = set()
needed_opts = set()
for model in models:
opts = self._get_opts(model)
try:
ct = self._get_from_cache(opts)
except KeyError:
needed_app_labels.add(opts.app_label)
needed_models.add(opts.object_name.lower())
needed_opts.add(opts)
else:
results[model] = ct
if needed_opts:
cts = self.filter(
app_label__in=needed_app_labels,
model__in=needed_models
)
for ct in cts:
model = ct.model_class()
if model._meta in needed_opts:
results[model] = ct
needed_opts.remove(model._meta)
self._add_to_cache(self.db, ct)
for opts in needed_opts:
# These weren't in the cache, or the DB, create them.
ct = self.create(
app_label=opts.app_label,
model=opts.object_name.lower(),
name=smart_unicode(opts.verbose_name_raw),
)
self._add_to_cache(self.db, ct)
results[ct.model_class()] = ct
return results
def get_for_id(self, id): def get_for_id(self, id):
""" """
Lookup a ContentType by ID. Uses the same shared cache as get_for_model Lookup a ContentType by ID. Uses the same shared cache as get_for_model

View File

@ -63,6 +63,36 @@ class ContentTypesTests(TestCase):
with self.assertNumQueries(1): with self.assertNumQueries(1):
ContentType.objects.get_for_model(ContentType) ContentType.objects.get_for_model(ContentType)
def test_get_for_models_empty_cache(self):
# Empty cache.
with self.assertNumQueries(1):
cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
self.assertEqual(cts, {
ContentType: ContentType.objects.get_for_model(ContentType),
FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
})
def test_get_for_models_partial_cache(self):
# Partial cache
ContentType.objects.get_for_model(ContentType)
with self.assertNumQueries(1):
cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
self.assertEqual(cts, {
ContentType: ContentType.objects.get_for_model(ContentType),
FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
})
def test_get_for_models_full_cache(self):
# Full cache
ContentType.objects.get_for_model(ContentType)
ContentType.objects.get_for_model(FooWithUrl)
with self.assertNumQueries(0):
cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
self.assertEqual(cts, {
ContentType: ContentType.objects.get_for_model(ContentType),
FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
})
def test_shortcut_view(self): def test_shortcut_view(self):
""" """
Check that the shortcut view (used for the admin "view on site" Check that the shortcut view (used for the admin "view on site"

View File

@ -193,6 +193,13 @@ The ``ContentTypeManager``
:class:`~django.contrib.contenttypes.models.ContentType` instance :class:`~django.contrib.contenttypes.models.ContentType` instance
representing that model. representing that model.
.. method:: get_for_models(*models)
Takes a variadic number of model classes, and returns a dictionary
mapping the model classes to the
:class:`~django.contrib.contenttypes.models.ContentType` instances
representing them.
.. method:: get_by_natural_key(app_label, model) .. method:: get_by_natural_key(app_label, model)
Returns the :class:`~django.contrib.contenttypes.models.ContentType` Returns the :class:`~django.contrib.contenttypes.models.ContentType`