Author: Alex Date: 2011-10-12 13:51:59 -0700 (Wed, 12 Oct 2011) New Revision: 16963
Modified: django/trunk/django/contrib/auth/management/__init__.py django/trunk/django/contrib/contenttypes/models.py django/trunk/django/contrib/contenttypes/tests.py django/trunk/docs/ref/contrib/contenttypes.txt Log: 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. Modified: django/trunk/django/contrib/auth/management/__init__.py =================================================================== --- django/trunk/django/contrib/auth/management/__init__.py 2011-10-12 19:34:34 UTC (rev 16962) +++ django/trunk/django/contrib/auth/management/__init__.py 2011-10-12 20:51:59 UTC (rev 16963) @@ -31,8 +31,8 @@ searched_perms = list() # The codenames and ctypes that should exist. ctypes = set() - for klass in app_models: - ctype = ContentType.objects.get_for_model(klass) + ctypes_for_models = ContentType.objects.get_for_models(*app_models) + for klass, ctype in ctypes_for_models.iteritems(): ctypes.add(ctype) for perm in _get_all_permissions(klass._meta): searched_perms.append((ctype, perm)) Modified: django/trunk/django/contrib/contenttypes/models.py =================================================================== --- django/trunk/django/contrib/contenttypes/models.py 2011-10-12 19:34:34 UTC (rev 16962) +++ django/trunk/django/contrib/contenttypes/models.py 2011-10-12 20:51:59 UTC (rev 16963) @@ -15,19 +15,26 @@ ct = self.get(app_label=app_label, model=model) 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): """ Returns the ContentType object for a given model, creating the ContentType if necessary. Lookups are cached so that subsequent lookups for the same model don't hit the database. """ - opts = model._meta - while opts.proxy: - model = opts.proxy_for_model - opts = model._meta - key = (opts.app_label, opts.object_name.lower()) + opts = self._get_opts(model) try: - ct = self.__class__._cache[self.db][key] + ct = self._get_from_cache(opts) except KeyError: # Load or create the ContentType entry. The smart_unicode() is # needed around opts.verbose_name_raw because name_raw might be a @@ -41,6 +48,48 @@ 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): """ Lookup a ContentType by ID. Uses the same shared cache as get_for_model Modified: django/trunk/django/contrib/contenttypes/tests.py =================================================================== --- django/trunk/django/contrib/contenttypes/tests.py 2011-10-12 19:34:34 UTC (rev 16962) +++ django/trunk/django/contrib/contenttypes/tests.py 2011-10-12 20:51:59 UTC (rev 16963) @@ -63,6 +63,36 @@ with self.assertNumQueries(1): 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): """ Check that the shortcut view (used for the admin "view on site" Modified: django/trunk/docs/ref/contrib/contenttypes.txt =================================================================== --- django/trunk/docs/ref/contrib/contenttypes.txt 2011-10-12 19:34:34 UTC (rev 16962) +++ django/trunk/docs/ref/contrib/contenttypes.txt 2011-10-12 20:51:59 UTC (rev 16963) @@ -193,6 +193,13 @@ :class:`~django.contrib.contenttypes.models.ContentType` instance 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) Returns the :class:`~django.contrib.contenttypes.models.ContentType` @@ -319,8 +326,8 @@ Due to the way :class:`~django.contrib.contenttypes.generic.GenericForeignKey` is implemented, you cannot use such fields directly with filters (``filter()`` -and ``exclude()``, for example) via the database API. Because a -:class:`~django.contrib.contenttypes.generic.GenericForeignKey` isn't a +and ``exclude()``, for example) via the database API. Because a +:class:`~django.contrib.contenttypes.generic.GenericForeignKey` isn't a normal field objects, these examples will *not* work:: # This will fail -- You received this message because you are subscribed to the Google Groups "Django updates" group. To post to this group, send email to django-updates@googlegroups.com. To unsubscribe from this group, send email to django-updates+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-updates?hl=en.