1
0
mirror of https://github.com/django/django.git synced 2025-07-05 10:19:20 +00:00

[soc2009/multidb] Updated content types to be multidb aware. Patch from Russell Keith-Magee.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11889 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Alex Gaynor 2009-12-17 16:12:06 +00:00
parent f9412b4d21
commit 2a99b2ba5b
6 changed files with 221 additions and 80 deletions

View File

@ -5,7 +5,7 @@ Classes allowing "generic" relations through ContentType and object-id fields.
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db.models import signals
from django.db import models
from django.db import models, DEFAULT_DB_ALIAS
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
from django.db.models.loading import get_model
from django.forms import ModelForm
@ -45,14 +45,14 @@ class GenericForeignKey(object):
kwargs[self.ct_field] = self.get_content_type(obj=value)
kwargs[self.fk_field] = value._get_pk_val()
def get_content_type(self, obj=None, id=None):
def get_content_type(self, obj=None, id=None, using=DEFAULT_DB_ALIAS):
# Convenience function using get_model avoids a circular import when
# using this model
ContentType = get_model("contenttypes", "contenttype")
if obj:
return ContentType.objects.get_for_model(obj)
return ContentType.objects.get_for_model(obj, using=obj._state.db)
elif id:
return ContentType.objects.get_for_id(id)
return ContentType.objects.get_for_id(id, using=using)
else:
# This should never happen. I love comments like this, don't you?
raise Exception("Impossible arguments to GFK.get_content_type!")
@ -73,7 +73,7 @@ class GenericForeignKey(object):
f = self.model._meta.get_field(self.ct_field)
ct_id = getattr(instance, f.get_attname(), None)
if ct_id:
ct = self.get_content_type(id=ct_id)
ct = self.get_content_type(id=ct_id, using=instance._state.db)
try:
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
except ObjectDoesNotExist:
@ -201,11 +201,10 @@ class ReverseGenericRelatedObjectsDescriptor(object):
join_table = qn(self.field.m2m_db_table()),
source_col_name = qn(self.field.m2m_column_name()),
target_col_name = qn(self.field.m2m_reverse_name()),
content_type = ContentType.objects.get_for_model(instance),
content_type = ContentType.objects.get_for_model(instance, using=instance._state.db),
content_type_field_name = self.field.content_type_field_name,
object_id_field_name = self.field.object_id_field_name
)
return manager
def __set__(self, instance, value):
@ -247,7 +246,7 @@ def create_generic_related_manager(superclass):
'%s__pk' % self.content_type_field_name : self.content_type.id,
'%s__exact' % self.object_id_field_name : self.pk_val,
}
return superclass.get_query_set(self).filter(**query)
return superclass.get_query_set(self).using(self.instance._state.db).filter(**query)
def add(self, *objs):
for obj in objs:
@ -255,17 +254,17 @@ def create_generic_related_manager(superclass):
raise TypeError, "'%s' instance expected" % self.model._meta.object_name
setattr(obj, self.content_type_field_name, self.content_type)
setattr(obj, self.object_id_field_name, self.pk_val)
obj.save()
obj.save(using=self.instance._state.db)
add.alters_data = True
def remove(self, *objs):
for obj in objs:
obj.delete()
obj.delete(using=self.instance._state.db)
remove.alters_data = True
def clear(self):
for obj in self.all():
obj.delete()
obj.delete(using=self.instance._state.db)
clear.alters_data = True
def create(self, **kwargs):

View File

@ -10,18 +10,19 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
ContentType.objects.clear_cache()
content_types = list(ContentType.objects.filter(app_label=app.__name__.split('.')[-2]))
app_models = get_models(app)
db = kwargs['db']
if not app_models:
return
for klass in app_models:
opts = klass._meta
try:
ct = ContentType.objects.get(app_label=opts.app_label,
model=opts.object_name.lower())
ct = ContentType.objects.using(db).get(app_label=opts.app_label,
model=opts.object_name.lower())
content_types.remove(ct)
except ContentType.DoesNotExist:
ct = ContentType(name=smart_unicode(opts.verbose_name_raw),
app_label=opts.app_label, model=opts.object_name.lower())
ct.save()
ct.save(using=db)
if verbosity >= 2:
print "Adding content type '%s | %s'" % (ct.app_label, ct.model)
# The presence of any remaining content types means the supplied app has an

View File

@ -1,4 +1,4 @@
from django.db import models
from django.db import models, DEFAULT_DB_ALIAS
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
@ -8,14 +8,14 @@ class ContentTypeManager(models.Manager):
# This cache is shared by all the get_for_* methods.
_cache = {}
def get_by_natural_key(self, app_label, model):
def get_by_natural_key(self, app_label, model, using=DEFAULT_DB_ALIAS):
try:
ct = self.__class__._cache[(app_label, model)]
ct = self.__class__._cache[using][(app_label, model)]
except KeyError:
ct = self.get(app_label=app_label, model=model)
ct = self.using(using).get(app_label=app_label, model=model)
return ct
def get_for_model(self, model):
def get_for_model(self, model, using=DEFAULT_DB_ALIAS):
"""
Returns the ContentType object for a given model, creating the
ContentType if necessary. Lookups are cached so that subsequent lookups
@ -27,32 +27,33 @@ class ContentTypeManager(models.Manager):
opts = model._meta
key = (opts.app_label, opts.object_name.lower())
try:
ct = self.__class__._cache[key]
ct = self.__class__._cache[using][key]
except KeyError:
# Load or create the ContentType entry. The smart_unicode() is
# needed around opts.verbose_name_raw because name_raw might be a
# django.utils.functional.__proxy__ object.
ct, created = self.get_or_create(
ct, created = self.using(using).get_or_create(
app_label = opts.app_label,
model = opts.object_name.lower(),
defaults = {'name': smart_unicode(opts.verbose_name_raw)},
)
self._add_to_cache(ct)
self._add_to_cache(using, ct)
return ct
def get_for_id(self, id):
def get_for_id(self, id, using=DEFAULT_DB_ALIAS):
"""
Lookup a ContentType by ID. Uses the same shared cache as get_for_model
(though ContentTypes are obviously not created on-the-fly by get_by_id).
"""
try:
ct = self.__class__._cache[id]
ct = self.__class__._cache[using][id]
except KeyError:
# This could raise a DoesNotExist; that's correct behavior and will
# make sure that only correct ctypes get stored in the cache dict.
ct = self.get(pk=id)
self._add_to_cache(ct)
ct = self.using(using).get(pk=id)
self._add_to_cache(using, ct)
return ct
def clear_cache(self):
@ -64,12 +65,12 @@ class ContentTypeManager(models.Manager):
"""
self.__class__._cache.clear()
def _add_to_cache(self, ct):
def _add_to_cache(self, using, ct):
"""Insert a ContentType into the cache."""
model = ct.model_class()
key = (model._meta.app_label, model._meta.object_name.lower())
self.__class__._cache[key] = ct
self.__class__._cache[ct.id] = ct
self.__class__._cache.setdefault(using, {})[key] = ct
self.__class__._cache.setdefault(using, {})[ct.id] = ct
class ContentType(models.Model):
name = models.CharField(max_length=100)
@ -99,7 +100,7 @@ class ContentType(models.Model):
method. The ObjectNotExist exception, if thrown, will not be caught,
so code that calls this method should catch it.
"""
return self.model_class()._default_manager.get(**kwargs)
return self.model_class()._default_manager.using(self._state.db).get(**kwargs)
def natural_key(self):
return (self.app_label, self.model)

View File

@ -183,12 +183,16 @@ The ``ContentTypeManager``
probably won't ever need to call this method yourself; Django will call
it automatically when it's needed.
.. method:: models.ContentTypeManager.get_for_model(model)
.. method:: models.ContentTypeManager.get_for_model(model, using=DEFAULT_DB_ALIAS)
Takes either a model class or an instance of a model, and returns the
:class:`~django.contrib.contenttypes.models.ContentType` instance
representing that model.
By default, this will find the content type on the default database.
You can load an instance from a different database by providing
a ``using`` argument.
The :meth:`~models.ContentTypeManager.get_for_model()` method is especially useful when you know you
need to work with a :class:`ContentType <django.contrib.contenttypes.models.ContentType>` but don't want to go to the
trouble of obtaining the model's metadata to perform a manual lookup::

View File

@ -1,10 +1,25 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db import models, DEFAULT_DB_ALIAS
class Review(models.Model):
source = models.CharField(max_length=100)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
def __unicode__(self):
return self.source
class Meta:
ordering = ('source',)
class Book(models.Model):
title = models.CharField(max_length=100)
published = models.DateField()
authors = models.ManyToManyField('Author')
reviews = generic.GenericRelation(Review)
def __unicode__(self):
return self.title

View File

@ -5,7 +5,7 @@ from django.conf import settings
from django.db import connections
from django.test import TestCase
from models import Book, Author
from models import Book, Author, Review
try:
# we only have these models if the user is using multi-db, it's safe the
@ -258,6 +258,53 @@ class QueryTestCase(TestCase):
self.assertEquals(list(Author.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)),
[u'Mark Pilgrim'])
def test_m2m_cross_database_protection(self):
"Operations that involve sharing M2M objects across databases raise an error"
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
marty = Author.objects.create(name="Marty Alchin")
# Create a book and author on the other database
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
mark = Author.objects.using('other').create(name="Mark Pilgrim")
# Set a foreign key set with an object from a different database
try:
marty.book_set = [pro, dive]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Add to an m2m with an object from a different database
try:
marty.book_set.add(dive)
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Set a m2m with an object from a different database
try:
marty.book_set = [pro, dive]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Add to a reverse m2m with an object from a different database
try:
dive.authors.add(marty)
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Set a reverse m2m with an object from a different database
try:
dive.authors = [mark, marty]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
def test_foreign_key_separation(self):
"FK fields are constrained to a single database"
@ -351,54 +398,6 @@ class QueryTestCase(TestCase):
self.assertEquals(list(Book.objects.using('other').filter(favourite_of__name='Jane Brown').values_list('title', flat=True)),
[u'Dive into Python'])
def test_m2m_cross_database_protection(self):
"Operations that involve sharing M2M objects across databases raise an error"
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
marty = Author.objects.create(name="Marty Alchin")
# Create a book and author on the other database
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
mark = Author.objects.using('other').create(name="Mark Pilgrim")
# Set a foreign key set with an object from a different database
try:
marty.book_set = [pro, dive]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Add to an m2m with an object from a different database
try:
marty.book_set.add(dive)
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Set a m2m with an object from a different database
try:
marty.book_set = [pro, dive]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Add to a reverse m2m with an object from a different database
try:
dive.authors.add(marty)
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Set a reverse m2m with an object from a different database
try:
dive.authors = [mark, marty]
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
def test_foreign_key_cross_database_protection(self):
"Operations that involve sharing FK objects across databases raise an error"
# Create a book and author on the default database
@ -469,6 +468,128 @@ class QueryTestCase(TestCase):
self.assertEquals(list(Author.objects.using('other').values_list('name',flat=True)),
[u'Jane Brown', u'John Smith', u'Mark Pilgrim'])
def test_generic_key_separation(self):
"Generic fields are constrained to a single database"
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
review1 = Review.objects.create(source="Python Monthly", content_object=pro)
# Create a book and author on the other database
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
review1 = Review.objects.using('default').get(source="Python Monthly")
self.assertEquals(review1.content_object.title, "Pro Django")
review2 = Review.objects.using('other').get(source="Python Weekly")
self.assertEquals(review2.content_object.title, "Dive into Python")
# Reget the objects to clear caches
dive = Book.objects.using('other').get(title="Dive into Python")
# Retrive related object by descriptor. Related objects should be database-bound
self.assertEquals(list(dive.reviews.all().values_list('source', flat=True)),
[u'Python Weekly'])
def test_generic_key_reverse_operations(self):
"Generic reverse manipulations are all constrained to a single DB"
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
temp = Book.objects.using('other').create(title="Temp",
published=datetime.date(2009, 5, 4))
review1 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
review2 = Review.objects.using('other').create(source="Python Monthly", content_object=temp)
self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[u'Python Weekly'])
# Add a second review
dive.reviews.add(review2)
self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[u'Python Monthly', u'Python Weekly'])
# Remove the second author
dive.reviews.remove(review1)
self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[u'Python Monthly'])
# Clear all reviews
dive.reviews.clear()
self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
# Create an author through the generic interface
dive.reviews.create(source='Python Daily')
self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)),
[])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[u'Python Daily'])
def test_generic_key_cross_database_protection(self):
"Operations that involve sharing FK objects across databases raise an error"
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
review1 = Review.objects.create(source="Python Monthly", content_object=pro)
# Create a book and author on the other database
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
# Set a foreign key with an object from a different database
try:
review1.content_object = dive
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# Add to a foreign key set with an object from a different database
try:
dive.reviews.add(review1)
self.fail("Shouldn't be able to assign across databases")
except ValueError:
pass
# BUT! if you assign a FK object when the base object hasn't
# been saved yet, you implicitly assign the database for the
# base object.
review3 = Review(source="Python Daily")
# initially, no db assigned
self.assertEquals(review3._state.db, None)
# Dive comes from 'other', so review3 is set to use 'other'...
review3.content_object = dive
self.assertEquals(review3._state.db, 'other')
# ... but it isn't saved yet
self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
[u'Python Monthly'])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
[u'Python Weekly'])
# When saved, John goes to 'other'
review3.save()
self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
[u'Python Monthly'])
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
[u'Python Daily', u'Python Weekly'])
class FixtureTestCase(TestCase):
multi_db = True