Fixed #12540, #12541 -- Added database routers, allowing for configurable database use behavior in a multi-db setup, and improved error checking for cross-database joins.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12272 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
acc095c333
commit
1b3dc8ad9a
|
@ -128,6 +128,7 @@ SERVER_EMAIL = 'root@localhost'
|
||||||
SEND_BROKEN_LINK_EMAILS = False
|
SEND_BROKEN_LINK_EMAILS = False
|
||||||
|
|
||||||
# Database connection info.
|
# Database connection info.
|
||||||
|
# Legacy format
|
||||||
DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||||
DATABASE_NAME = '' # Or path to database file if using sqlite3.
|
DATABASE_NAME = '' # Or path to database file if using sqlite3.
|
||||||
DATABASE_USER = '' # Not used with sqlite3.
|
DATABASE_USER = '' # Not used with sqlite3.
|
||||||
|
@ -136,9 +137,13 @@ DATABASE_HOST = '' # Set to empty string for localhost. Not used wit
|
||||||
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
|
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
|
||||||
DATABASE_OPTIONS = {} # Set to empty dictionary for default.
|
DATABASE_OPTIONS = {} # Set to empty dictionary for default.
|
||||||
|
|
||||||
|
# New format
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Classes used to implement db routing behaviour
|
||||||
|
DATABASE_ROUTERS = []
|
||||||
|
|
||||||
# The email backend to use. For possible shortcuts see django.core.mail.
|
# The email backend to use. For possible shortcuts see django.core.mail.
|
||||||
# The default is to use the SMTP backend.
|
# The default is to use the SMTP backend.
|
||||||
# Third-party backends can be specified by providing a Python path
|
# Third-party backends can be specified by providing a Python path
|
||||||
|
|
|
@ -3,7 +3,7 @@ import urllib
|
||||||
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models, DEFAULT_DB_ALIAS
|
from django.db import models
|
||||||
from django.db.models.manager import EmptyManager
|
from django.db.models.manager import EmptyManager
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
|
|
@ -5,7 +5,7 @@ Classes allowing "generic" relations through ContentType and object-id fields.
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db import models, DEFAULT_DB_ALIAS
|
from django.db import models
|
||||||
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
|
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
|
||||||
from django.db.models.loading import get_model
|
from django.db.models.loading import get_model
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
@ -255,7 +255,7 @@ def create_generic_related_manager(superclass):
|
||||||
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
||||||
setattr(obj, self.content_type_field_name, self.content_type)
|
setattr(obj, self.content_type_field_name, self.content_type)
|
||||||
setattr(obj, self.object_id_field_name, self.pk_val)
|
setattr(obj, self.object_id_field_name, self.pk_val)
|
||||||
obj.save(using=self.instance._state.db)
|
obj.save()
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def remove(self, *objs):
|
def remove(self, *objs):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.db import models, DEFAULT_DB_ALIAS
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_unicode
|
from django.utils.encoding import smart_unicode
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.db import connections, DEFAULT_DB_ALIAS
|
from django.db import connections
|
||||||
from django.db.models.query import sql
|
from django.db.models.query import sql
|
||||||
|
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import signals
|
from django.core import signals
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.utils import ConnectionHandler, load_backend
|
from django.db.utils import ConnectionHandler, ConnectionRouter, load_backend, DEFAULT_DB_ALIAS
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
|
|
||||||
__all__ = ('backend', 'connection', 'connections', 'DatabaseError',
|
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
|
||||||
'IntegrityError', 'DEFAULT_DB_ALIAS')
|
'IntegrityError', 'DEFAULT_DB_ALIAS')
|
||||||
|
|
||||||
DEFAULT_DB_ALIAS = 'default'
|
|
||||||
|
|
||||||
# For backwards compatibility - Port any old database settings over to
|
# For backwards compatibility - Port any old database settings over to
|
||||||
# the new values.
|
# the new values.
|
||||||
|
@ -61,6 +60,7 @@ for alias, database in settings.DATABASES.items():
|
||||||
|
|
||||||
connections = ConnectionHandler(settings.DATABASES)
|
connections = ConnectionHandler(settings.DATABASES)
|
||||||
|
|
||||||
|
router = ConnectionRouter(settings.DATABASE_ROUTERS)
|
||||||
|
|
||||||
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
|
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
|
||||||
# for backend bits.
|
# for backend bits.
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneF
|
||||||
from django.db.models.query import delete_objects, Q
|
from django.db.models.query import delete_objects, Q
|
||||||
from django.db.models.query_utils import CollectedObjects, DeferredAttribute
|
from django.db.models.query_utils import CollectedObjects, DeferredAttribute
|
||||||
from django.db.models.options import Options
|
from django.db.models.options import Options
|
||||||
from django.db import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS
|
from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.loading import register_models, get_model
|
from django.db.models.loading import register_models, get_model
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -439,7 +439,7 @@ class Model(object):
|
||||||
need for overrides of save() to pass around internal-only parameters
|
need for overrides of save() to pass around internal-only parameters
|
||||||
('raw', 'cls', and 'origin').
|
('raw', 'cls', and 'origin').
|
||||||
"""
|
"""
|
||||||
using = using or self._state.db or DEFAULT_DB_ALIAS
|
using = using or router.db_for_write(self.__class__, instance=self)
|
||||||
connection = connections[using]
|
connection = connections[using]
|
||||||
assert not (force_insert and force_update)
|
assert not (force_insert and force_update)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
|
@ -592,7 +592,7 @@ class Model(object):
|
||||||
parent_obj._collect_sub_objects(seen_objs)
|
parent_obj._collect_sub_objects(seen_objs)
|
||||||
|
|
||||||
def delete(self, using=None):
|
def delete(self, using=None):
|
||||||
using = using or self._state.db or DEFAULT_DB_ALIAS
|
using = using or router.db_for_write(self.__class__, instance=self)
|
||||||
connection = connections[using]
|
connection = connections[using]
|
||||||
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
|
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
|
||||||
|
|
||||||
|
@ -719,7 +719,7 @@ class Model(object):
|
||||||
# no value, skip the lookup
|
# no value, skip the lookup
|
||||||
continue
|
continue
|
||||||
if f.primary_key and not getattr(self, '_adding', False):
|
if f.primary_key and not getattr(self, '_adding', False):
|
||||||
# no need to check for unique primary key when editting
|
# no need to check for unique primary key when editing
|
||||||
continue
|
continue
|
||||||
lookup_kwargs[str(field_name)] = lookup_value
|
lookup_kwargs[str(field_name)] = lookup_value
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db import connection, transaction, DEFAULT_DB_ALIAS
|
from django.conf import settings
|
||||||
|
from django.db import connection, router, transaction
|
||||||
from django.db.backends import util
|
from django.db.backends import util
|
||||||
from django.db.models import signals, get_model
|
from django.db.models import signals, get_model
|
||||||
from django.db.models.fields import (AutoField, Field, IntegerField,
|
from django.db.models.fields import (AutoField, Field, IntegerField,
|
||||||
|
@ -197,7 +198,8 @@ class SingleRelatedObjectDescriptor(object):
|
||||||
return getattr(instance, self.cache_name)
|
return getattr(instance, self.cache_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
params = {'%s__pk' % self.related.field.name: instance._get_pk_val()}
|
params = {'%s__pk' % self.related.field.name: instance._get_pk_val()}
|
||||||
rel_obj = self.related.model._base_manager.using(instance._state.db).get(**params)
|
db = router.db_for_read(instance.__class__, instance=instance)
|
||||||
|
rel_obj = self.related.model._base_manager.using(db).get(**params)
|
||||||
setattr(instance, self.cache_name, rel_obj)
|
setattr(instance, self.cache_name, rel_obj)
|
||||||
return rel_obj
|
return rel_obj
|
||||||
|
|
||||||
|
@ -218,6 +220,15 @@ class SingleRelatedObjectDescriptor(object):
|
||||||
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
|
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
|
||||||
(value, instance._meta.object_name,
|
(value, instance._meta.object_name,
|
||||||
self.related.get_accessor_name(), self.related.opts.object_name))
|
self.related.get_accessor_name(), self.related.opts.object_name))
|
||||||
|
elif value is not None:
|
||||||
|
if instance._state.db is None:
|
||||||
|
instance._state.db = router.db_for_write(instance.__class__, instance=value)
|
||||||
|
elif value._state.db is None:
|
||||||
|
value._state.db = router.db_for_write(value.__class__, instance=instance)
|
||||||
|
elif value._state.db is not None and instance._state.db is not None:
|
||||||
|
if not router.allow_relation(value, instance):
|
||||||
|
raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
|
||||||
|
(value, instance._state.db, value._state.db))
|
||||||
|
|
||||||
# Set the value of the related field to the value of the related object's related field
|
# Set the value of the related field to the value of the related object's related field
|
||||||
setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname))
|
setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname))
|
||||||
|
@ -260,11 +271,11 @@ class ReverseSingleRelatedObjectDescriptor(object):
|
||||||
# If the related manager indicates that it should be used for
|
# If the related manager indicates that it should be used for
|
||||||
# related fields, respect that.
|
# related fields, respect that.
|
||||||
rel_mgr = self.field.rel.to._default_manager
|
rel_mgr = self.field.rel.to._default_manager
|
||||||
using = instance._state.db or DEFAULT_DB_ALIAS
|
db = router.db_for_read(self.field.rel.to, instance=instance)
|
||||||
if getattr(rel_mgr, 'use_for_related_fields', False):
|
if getattr(rel_mgr, 'use_for_related_fields', False):
|
||||||
rel_obj = rel_mgr.using(using).get(**params)
|
rel_obj = rel_mgr.using(db).get(**params)
|
||||||
else:
|
else:
|
||||||
rel_obj = QuerySet(self.field.rel.to).using(using).get(**params)
|
rel_obj = QuerySet(self.field.rel.to).using(db).get(**params)
|
||||||
setattr(instance, cache_name, rel_obj)
|
setattr(instance, cache_name, rel_obj)
|
||||||
return rel_obj
|
return rel_obj
|
||||||
|
|
||||||
|
@ -281,14 +292,15 @@ class ReverseSingleRelatedObjectDescriptor(object):
|
||||||
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
|
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
|
||||||
(value, instance._meta.object_name,
|
(value, instance._meta.object_name,
|
||||||
self.field.name, self.field.rel.to._meta.object_name))
|
self.field.name, self.field.rel.to._meta.object_name))
|
||||||
elif value is not None and value._state.db != instance._state.db:
|
elif value is not None:
|
||||||
if instance._state.db is None:
|
if instance._state.db is None:
|
||||||
instance._state.db = value._state.db
|
instance._state.db = router.db_for_write(instance.__class__, instance=value)
|
||||||
else:#elif value._state.db is None:
|
elif value._state.db is None:
|
||||||
value._state.db = instance._state.db
|
value._state.db = router.db_for_write(value.__class__, instance=instance)
|
||||||
# elif value._state.db is not None and instance._state.db is not None:
|
elif value._state.db is not None and instance._state.db is not None:
|
||||||
# raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
|
if not router.allow_relation(value, instance):
|
||||||
# (value, instance._state.db, value._state.db))
|
raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
|
||||||
|
(value, instance._state.db, value._state.db))
|
||||||
|
|
||||||
# If we're setting the value of a OneToOneField to None, we need to clear
|
# If we're setting the value of a OneToOneField to None, we need to clear
|
||||||
# out the cache on any old related object. Otherwise, deleting the
|
# out the cache on any old related object. Otherwise, deleting the
|
||||||
|
@ -370,15 +382,15 @@ class ForeignRelatedObjectsDescriptor(object):
|
||||||
|
|
||||||
class RelatedManager(superclass):
|
class RelatedManager(superclass):
|
||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
using = instance._state.db or DEFAULT_DB_ALIAS
|
db = router.db_for_read(rel_model, instance=instance)
|
||||||
return superclass.get_query_set(self).using(using).filter(**(self.core_filters))
|
return superclass.get_query_set(self).using(db).filter(**(self.core_filters))
|
||||||
|
|
||||||
def add(self, *objs):
|
def add(self, *objs):
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
if not isinstance(obj, self.model):
|
if not isinstance(obj, self.model):
|
||||||
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
||||||
setattr(obj, rel_field.name, instance)
|
setattr(obj, rel_field.name, instance)
|
||||||
obj.save(using=instance._state.db)
|
obj.save()
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
|
@ -390,8 +402,8 @@ class ForeignRelatedObjectsDescriptor(object):
|
||||||
# Update kwargs with the related object that this
|
# Update kwargs with the related object that this
|
||||||
# ForeignRelatedObjectsDescriptor knows about.
|
# ForeignRelatedObjectsDescriptor knows about.
|
||||||
kwargs.update({rel_field.name: instance})
|
kwargs.update({rel_field.name: instance})
|
||||||
using = instance._state.db or DEFAULT_DB_ALIAS
|
db = router.db_for_write(rel_model, instance=instance)
|
||||||
return super(RelatedManager, self).using(using).get_or_create(**kwargs)
|
return super(RelatedManager, self).using(db).get_or_create(**kwargs)
|
||||||
get_or_create.alters_data = True
|
get_or_create.alters_data = True
|
||||||
|
|
||||||
# remove() and clear() are only provided if the ForeignKey can have a value of null.
|
# remove() and clear() are only provided if the ForeignKey can have a value of null.
|
||||||
|
@ -402,7 +414,7 @@ class ForeignRelatedObjectsDescriptor(object):
|
||||||
# Is obj actually part of this descriptor set?
|
# Is obj actually part of this descriptor set?
|
||||||
if getattr(obj, rel_field.attname) == val:
|
if getattr(obj, rel_field.attname) == val:
|
||||||
setattr(obj, rel_field.name, None)
|
setattr(obj, rel_field.name, None)
|
||||||
obj.save(using=instance._state.db)
|
obj.save()
|
||||||
else:
|
else:
|
||||||
raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, instance))
|
raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, instance))
|
||||||
remove.alters_data = True
|
remove.alters_data = True
|
||||||
|
@ -410,7 +422,7 @@ class ForeignRelatedObjectsDescriptor(object):
|
||||||
def clear(self):
|
def clear(self):
|
||||||
for obj in self.all():
|
for obj in self.all():
|
||||||
setattr(obj, rel_field.name, None)
|
setattr(obj, rel_field.name, None)
|
||||||
obj.save(using=instance._state.db)
|
obj.save()
|
||||||
clear.alters_data = True
|
clear.alters_data = True
|
||||||
|
|
||||||
manager = RelatedManager()
|
manager = RelatedManager()
|
||||||
|
@ -443,7 +455,8 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
|
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
|
||||||
|
|
||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
return superclass.get_query_set(self).using(self.instance._state.db)._next_is_sticky().filter(**(self.core_filters))
|
db = router.db_for_read(self.instance.__class__, instance=self.instance)
|
||||||
|
return superclass.get_query_set(self).using(db)._next_is_sticky().filter(**(self.core_filters))
|
||||||
|
|
||||||
# If the ManyToMany relation has an intermediary model,
|
# If the ManyToMany relation has an intermediary model,
|
||||||
# the add and remove methods do not exist.
|
# the add and remove methods do not exist.
|
||||||
|
@ -478,14 +491,16 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
if not rel.through._meta.auto_created:
|
if not rel.through._meta.auto_created:
|
||||||
opts = through._meta
|
opts = through._meta
|
||||||
raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
||||||
new_obj = super(ManyRelatedManager, self).using(self.instance._state.db).create(**kwargs)
|
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
||||||
|
new_obj = super(ManyRelatedManager, self).using(db).create(**kwargs)
|
||||||
self.add(new_obj)
|
self.add(new_obj)
|
||||||
return new_obj
|
return new_obj
|
||||||
create.alters_data = True
|
create.alters_data = True
|
||||||
|
|
||||||
def get_or_create(self, **kwargs):
|
def get_or_create(self, **kwargs):
|
||||||
|
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
||||||
obj, created = \
|
obj, created = \
|
||||||
super(ManyRelatedManager, self).using(self.instance._state.db).get_or_create(**kwargs)
|
super(ManyRelatedManager, self).using(db).get_or_create(**kwargs)
|
||||||
# We only need to add() if created because if we got an object back
|
# We only need to add() if created because if we got an object back
|
||||||
# from get() then the relationship already exists.
|
# from get() then the relationship already exists.
|
||||||
if created:
|
if created:
|
||||||
|
@ -505,15 +520,16 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
new_ids = set()
|
new_ids = set()
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
if isinstance(obj, self.model):
|
if isinstance(obj, self.model):
|
||||||
# if obj._state.db != self.instance._state.db:
|
if not router.allow_relation(obj, self.instance):
|
||||||
# raise ValueError('Cannot add "%r": instance is on database "%s", value is is on database "%s"' %
|
raise ValueError('Cannot add "%r": instance is on database "%s", value is is on database "%s"' %
|
||||||
# (obj, self.instance._state.db, obj._state.db))
|
(obj, self.instance._state.db, obj._state.db))
|
||||||
new_ids.add(obj.pk)
|
new_ids.add(obj.pk)
|
||||||
elif isinstance(obj, Model):
|
elif isinstance(obj, Model):
|
||||||
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
||||||
else:
|
else:
|
||||||
new_ids.add(obj)
|
new_ids.add(obj)
|
||||||
vals = self.through._default_manager.using(self.instance._state.db).values_list(target_field_name, flat=True)
|
db = router.db_for_write(self.through.__class__, instance=self.instance)
|
||||||
|
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
|
||||||
vals = vals.filter(**{
|
vals = vals.filter(**{
|
||||||
source_field_name: self._pk_val,
|
source_field_name: self._pk_val,
|
||||||
'%s__in' % target_field_name: new_ids,
|
'%s__in' % target_field_name: new_ids,
|
||||||
|
@ -521,7 +537,7 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
new_ids = new_ids - set(vals)
|
new_ids = new_ids - set(vals)
|
||||||
# Add the ones that aren't there already
|
# Add the ones that aren't there already
|
||||||
for obj_id in new_ids:
|
for obj_id in new_ids:
|
||||||
self.through._default_manager.using(self.instance._state.db).create(**{
|
self.through._default_manager.using(db).create(**{
|
||||||
'%s_id' % source_field_name: self._pk_val,
|
'%s_id' % source_field_name: self._pk_val,
|
||||||
'%s_id' % target_field_name: obj_id,
|
'%s_id' % target_field_name: obj_id,
|
||||||
})
|
})
|
||||||
|
@ -547,7 +563,8 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
else:
|
else:
|
||||||
old_ids.add(obj)
|
old_ids.add(obj)
|
||||||
# Remove the specified objects from the join table
|
# Remove the specified objects from the join table
|
||||||
self.through._default_manager.using(self.instance._state.db).filter(**{
|
db = router.db_for_write(self.through.__class__, instance=self.instance)
|
||||||
|
self.through._default_manager.using(db).filter(**{
|
||||||
source_field_name: self._pk_val,
|
source_field_name: self._pk_val,
|
||||||
'%s__in' % target_field_name: old_ids
|
'%s__in' % target_field_name: old_ids
|
||||||
}).delete()
|
}).delete()
|
||||||
|
@ -566,7 +583,8 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
signals.m2m_changed.send(sender=rel.through, action="clear",
|
signals.m2m_changed.send(sender=rel.through, action="clear",
|
||||||
instance=self.instance, reverse=self.reverse,
|
instance=self.instance, reverse=self.reverse,
|
||||||
model=self.model, pk_set=None)
|
model=self.model, pk_set=None)
|
||||||
self.through._default_manager.using(self.instance._state.db).filter(**{
|
db = router.db_for_write(self.through.__class__, instance=self.instance)
|
||||||
|
self.through._default_manager.using(db).filter(**{
|
||||||
source_field_name: self._pk_val
|
source_field_name: self._pk_val
|
||||||
}).delete()
|
}).delete()
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.utils import copycompat as copy
|
from django.utils import copycompat as copy
|
||||||
|
from django.conf import settings
|
||||||
from django.db import DEFAULT_DB_ALIAS
|
from django.db import router
|
||||||
from django.db.models.query import QuerySet, EmptyQuerySet, insert_query, RawQuerySet
|
from django.db.models.query import QuerySet, EmptyQuerySet, insert_query, RawQuerySet
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
def ensure_default_manager(sender, **kwargs):
|
def ensure_default_manager(sender, **kwargs):
|
||||||
"""
|
"""
|
||||||
Ensures that a Model subclass contains a default manager and sets the
|
Ensures that a Model subclass contains a default manager and sets the
|
||||||
|
@ -87,30 +88,27 @@ class Manager(object):
|
||||||
mgr._inherited = True
|
mgr._inherited = True
|
||||||
return mgr
|
return mgr
|
||||||
|
|
||||||
def db_manager(self, alias):
|
def db_manager(self, using):
|
||||||
obj = copy.copy(self)
|
obj = copy.copy(self)
|
||||||
obj._db = alias
|
obj._db = using
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
return self._db or DEFAULT_DB_ALIAS
|
return self._db or router.db_for_read(self.model)
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
# PROXIES TO QUERYSET #
|
# PROXIES TO QUERYSET #
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
def get_empty_query_set(self):
|
def get_empty_query_set(self):
|
||||||
return EmptyQuerySet(self.model)
|
return EmptyQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
"""Returns a new QuerySet object. Subclasses can override this method
|
"""Returns a new QuerySet object. Subclasses can override this method
|
||||||
to easily customize the behavior of the Manager.
|
to easily customize the behavior of the Manager.
|
||||||
"""
|
"""
|
||||||
qs = QuerySet(self.model)
|
return QuerySet(self.model, using=self._db)
|
||||||
if self._db is not None:
|
|
||||||
qs = qs.using(self._db)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def none(self):
|
def none(self):
|
||||||
return self.get_empty_query_set()
|
return self.get_empty_query_set()
|
||||||
|
@ -200,7 +198,7 @@ class Manager(object):
|
||||||
return self.get_query_set()._update(values, **kwargs)
|
return self.get_query_set()._update(values, **kwargs)
|
||||||
|
|
||||||
def raw(self, raw_query, params=None, *args, **kwargs):
|
def raw(self, raw_query, params=None, *args, **kwargs):
|
||||||
return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self.db, *args, **kwargs)
|
return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs)
|
||||||
|
|
||||||
class ManagerDescriptor(object):
|
class ManagerDescriptor(object):
|
||||||
# This class ensures managers aren't accessible via model instances.
|
# This class ensures managers aren't accessible via model instances.
|
||||||
|
|
|
@ -4,7 +4,7 @@ The main QuerySet implementation. This provides the public API for the ORM.
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.db import connections, transaction, IntegrityError, DEFAULT_DB_ALIAS
|
from django.db import connections, router, transaction, IntegrityError
|
||||||
from django.db.models.aggregates import Aggregate
|
from django.db.models.aggregates import Aggregate
|
||||||
from django.db.models.fields import DateField
|
from django.db.models.fields import DateField
|
||||||
from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery
|
from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery
|
||||||
|
@ -34,6 +34,7 @@ class QuerySet(object):
|
||||||
self._result_cache = None
|
self._result_cache = None
|
||||||
self._iter = None
|
self._iter = None
|
||||||
self._sticky_filter = False
|
self._sticky_filter = False
|
||||||
|
self._for_write = False
|
||||||
|
|
||||||
########################
|
########################
|
||||||
# PYTHON MAGIC METHODS #
|
# PYTHON MAGIC METHODS #
|
||||||
|
@ -345,6 +346,7 @@ class QuerySet(object):
|
||||||
and returning the created object.
|
and returning the created object.
|
||||||
"""
|
"""
|
||||||
obj = self.model(**kwargs)
|
obj = self.model(**kwargs)
|
||||||
|
self._for_write = True
|
||||||
obj.save(force_insert=True, using=self.db)
|
obj.save(force_insert=True, using=self.db)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@ -358,6 +360,7 @@ class QuerySet(object):
|
||||||
'get_or_create() must be passed at least one keyword argument'
|
'get_or_create() must be passed at least one keyword argument'
|
||||||
defaults = kwargs.pop('defaults', {})
|
defaults = kwargs.pop('defaults', {})
|
||||||
try:
|
try:
|
||||||
|
self._for_write = True
|
||||||
return self.get(**kwargs), False
|
return self.get(**kwargs), False
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
|
@ -413,6 +416,11 @@ class QuerySet(object):
|
||||||
|
|
||||||
del_query = self._clone()
|
del_query = self._clone()
|
||||||
|
|
||||||
|
# The delete is actually 2 queries - one to find related objects,
|
||||||
|
# and one to delete. Make sure that the discovery of related
|
||||||
|
# objects is performed on the same database as the deletion.
|
||||||
|
del_query._for_write = True
|
||||||
|
|
||||||
# Disable non-supported fields.
|
# Disable non-supported fields.
|
||||||
del_query.query.select_related = False
|
del_query.query.select_related = False
|
||||||
del_query.query.clear_ordering()
|
del_query.query.clear_ordering()
|
||||||
|
@ -442,6 +450,7 @@ class QuerySet(object):
|
||||||
"""
|
"""
|
||||||
assert self.query.can_filter(), \
|
assert self.query.can_filter(), \
|
||||||
"Cannot update a query once a slice has been taken."
|
"Cannot update a query once a slice has been taken."
|
||||||
|
self._for_write = True
|
||||||
query = self.query.clone(sql.UpdateQuery)
|
query = self.query.clone(sql.UpdateQuery)
|
||||||
query.add_update_values(kwargs)
|
query.add_update_values(kwargs)
|
||||||
if not transaction.is_managed(using=self.db):
|
if not transaction.is_managed(using=self.db):
|
||||||
|
@ -714,7 +723,9 @@ class QuerySet(object):
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
"Return the database that will be used if this query is executed now"
|
"Return the database that will be used if this query is executed now"
|
||||||
return self._db or DEFAULT_DB_ALIAS
|
if self._for_write:
|
||||||
|
return self._db or router.db_for_write(self.model)
|
||||||
|
return self._db or router.db_for_read(self.model)
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# PRIVATE METHODS #
|
# PRIVATE METHODS #
|
||||||
|
@ -726,8 +737,8 @@ class QuerySet(object):
|
||||||
query = self.query.clone()
|
query = self.query.clone()
|
||||||
if self._sticky_filter:
|
if self._sticky_filter:
|
||||||
query.filter_is_sticky = True
|
query.filter_is_sticky = True
|
||||||
c = klass(model=self.model, query=query)
|
c = klass(model=self.model, query=query, using=self._db)
|
||||||
c._db = self._db
|
c._for_write = self._for_write
|
||||||
c.__dict__.update(kwargs)
|
c.__dict__.update(kwargs)
|
||||||
if setup and hasattr(c, '_setup_query'):
|
if setup and hasattr(c, '_setup_query'):
|
||||||
c._setup_query()
|
c._setup_query()
|
||||||
|
@ -988,8 +999,8 @@ class DateQuerySet(QuerySet):
|
||||||
|
|
||||||
|
|
||||||
class EmptyQuerySet(QuerySet):
|
class EmptyQuerySet(QuerySet):
|
||||||
def __init__(self, model=None, query=None):
|
def __init__(self, model=None, query=None, using=None):
|
||||||
super(EmptyQuerySet, self).__init__(model, query)
|
super(EmptyQuerySet, self).__init__(model, query, using)
|
||||||
self._result_cache = []
|
self._result_cache = []
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
|
@ -1254,7 +1265,7 @@ class RawQuerySet(object):
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
"Return the database that will be used if this query is executed now"
|
"Return the database that will be used if this query is executed now"
|
||||||
return self._db or DEFAULT_DB_ALIAS
|
return self._db or router.db_for_read(self.model)
|
||||||
|
|
||||||
def using(self, alias):
|
def using(self, alias):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,6 +5,8 @@ from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
|
|
||||||
|
DEFAULT_DB_ALIAS = 'default'
|
||||||
|
|
||||||
def load_backend(backend_name):
|
def load_backend(backend_name):
|
||||||
try:
|
try:
|
||||||
module = import_module('.base', 'django.db.backends.%s' % backend_name)
|
module = import_module('.base', 'django.db.backends.%s' % backend_name)
|
||||||
|
@ -55,6 +57,7 @@ class ConnectionHandler(object):
|
||||||
conn = self.databases[alias]
|
conn = self.databases[alias]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
|
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
|
||||||
|
|
||||||
conn.setdefault('ENGINE', 'django.db.backends.dummy')
|
conn.setdefault('ENGINE', 'django.db.backends.dummy')
|
||||||
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
|
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
|
||||||
conn['ENGINE'] = 'django.db.backends.dummy'
|
conn['ENGINE'] = 'django.db.backends.dummy'
|
||||||
|
@ -82,3 +85,38 @@ class ConnectionHandler(object):
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
return [self[alias] for alias in self]
|
return [self[alias] for alias in self]
|
||||||
|
|
||||||
|
class ConnectionRouter(object):
|
||||||
|
def __init__(self, routers):
|
||||||
|
self.routers = []
|
||||||
|
for r in routers:
|
||||||
|
if isinstance(r, basestring):
|
||||||
|
module_name, klass_name = r.rsplit('.', 1)
|
||||||
|
module = import_module(module_name)
|
||||||
|
router = getattr(module, klass_name)()
|
||||||
|
else:
|
||||||
|
router = r
|
||||||
|
self.routers.append(router)
|
||||||
|
|
||||||
|
def _router_func(action):
|
||||||
|
def _route_db(self, model, **hints):
|
||||||
|
chosen_db = None
|
||||||
|
for router in self.routers:
|
||||||
|
chosen_db = getattr(router, action)(model, **hints)
|
||||||
|
if chosen_db:
|
||||||
|
return chosen_db
|
||||||
|
try:
|
||||||
|
return hints['instance']._state.db or DEFAULT_DB_ALIAS
|
||||||
|
except KeyError:
|
||||||
|
return DEFAULT_DB_ALIAS
|
||||||
|
return _route_db
|
||||||
|
|
||||||
|
db_for_read = _router_func('db_for_read')
|
||||||
|
db_for_write = _router_func('db_for_write')
|
||||||
|
|
||||||
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
|
for router in self.routers:
|
||||||
|
allow = router.allow_relation(obj1, obj2, **hints)
|
||||||
|
if allow is not None:
|
||||||
|
return allow
|
||||||
|
return obj1._state.db == obj2._state.db
|
||||||
|
|
|
@ -372,6 +372,22 @@ test database will use the name ``'test_' + DATABASE_NAME``.
|
||||||
|
|
||||||
See :ref:`topics-testing`.
|
See :ref:`topics-testing`.
|
||||||
|
|
||||||
|
|
||||||
|
.. setting:: DATABASE_ROUTERS
|
||||||
|
|
||||||
|
DATABASE_ROUTERS
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. versionadded: 1.2
|
||||||
|
|
||||||
|
Default: ``[]`` (Empty list)
|
||||||
|
|
||||||
|
The list of routers that will be used to determine which database
|
||||||
|
to use when performing a database queries.
|
||||||
|
|
||||||
|
See the documentation on :ref:`automatic database routing in multi
|
||||||
|
database configurations <topics-db-multi-db-routing>`.
|
||||||
|
|
||||||
.. setting:: DATE_FORMAT
|
.. setting:: DATE_FORMAT
|
||||||
|
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
|
|
|
@ -6,10 +6,10 @@ Multiple databases
|
||||||
|
|
||||||
.. versionadded:: 1.2
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
This topic guide describes Django's support for interacting with multiple
|
This topic guide describes Django's support for interacting with
|
||||||
databases. Most of the rest of Django's documentation assumes you are
|
multiple databases. Most of the rest of Django's documentation assumes
|
||||||
interacting with a single database. If you want to interact with multiple
|
you are interacting with a single database. If you want to interact
|
||||||
databases, you'll need to take some additional steps.
|
with multiple databases, you'll need to take some additional steps.
|
||||||
|
|
||||||
Defining your databases
|
Defining your databases
|
||||||
=======================
|
=======================
|
||||||
|
@ -22,9 +22,11 @@ a dictionary of settings for that specific connection. The settings in
|
||||||
the inner dictionaries are described fully in the :setting:`DATABASES`
|
the inner dictionaries are described fully in the :setting:`DATABASES`
|
||||||
documentation.
|
documentation.
|
||||||
|
|
||||||
Regardless of how many databases you have, you *must* have a database
|
Databases can have any alias you choose. However, the alias
|
||||||
named ``'default'``. Any additional databases can have whatever alias
|
``default`` has special significance. Django uses the database with
|
||||||
you choose.
|
the alias of ``default`` when no other database has been selected. If
|
||||||
|
you don't have a ``default`` database, you need to be careful to
|
||||||
|
always specify the database that you want to use.
|
||||||
|
|
||||||
The following is an example ``settings.py`` snippet defining two
|
The following is an example ``settings.py`` snippet defining two
|
||||||
databases -- a default PostgreSQL database and a MySQL database called
|
databases -- a default PostgreSQL database and a MySQL database called
|
||||||
|
@ -65,10 +67,10 @@ all databases in our example, you would need to call::
|
||||||
|
|
||||||
If you don't want every application to be synchronized onto a
|
If you don't want every application to be synchronized onto a
|
||||||
particular database. you can specify the :djadminopt:`--exclude`
|
particular database. you can specify the :djadminopt:`--exclude`
|
||||||
argument to :djadmin:`syncdb`. The :djadminopt:`--exclude` option
|
argument to :djadmin:`syncdb`. The :djadminopt:`--exclude` option lets
|
||||||
lets you prevent a specific application or applications from
|
you prevent a specific application or applications from being
|
||||||
being synchronized. For example, if you don't want the ``sales``
|
synchronized. For example, if you don't want the ``sales`` application
|
||||||
application to be in the ``users`` database, you could run::
|
to be in the ``users`` database, you could run::
|
||||||
|
|
||||||
$ ./manage.py syncdb --database=users --exclude=sales
|
$ ./manage.py syncdb --database=users --exclude=sales
|
||||||
|
|
||||||
|
@ -86,46 +88,235 @@ operate in the same way as :djadmin:`syncdb` -- they only ever operate
|
||||||
on one database at a time, using :djadminopt:`--database` to control
|
on one database at a time, using :djadminopt:`--database` to control
|
||||||
the database used.
|
the database used.
|
||||||
|
|
||||||
Selecting a database for a ``QuerySet``
|
.. _topics-db-multi-db-routing:
|
||||||
=======================================
|
|
||||||
|
|
||||||
You can select the database for a ``QuerySet`` at any point in the ``QuerySet``
|
Automatic database routing
|
||||||
"chain." Just call ``using()`` on the ``QuerySet`` to get another ``QuerySet``
|
==========================
|
||||||
that uses the specified database.
|
|
||||||
|
|
||||||
``using()`` takes a single argument: the alias of the database on which you
|
The easiest way to use multiple databases is to set up a database
|
||||||
want to run the query. For example:
|
routing scheme. The default routing scheme ensures that objects remain
|
||||||
|
'sticky' to their original database (i.e., an object retrieved from
|
||||||
|
the ``foo`` database will be saved on the same database). However, you
|
||||||
|
can implement more interesting behaviors by defining a different
|
||||||
|
routing scheme.
|
||||||
|
|
||||||
.. code-block:: python
|
Database routers
|
||||||
|
----------------
|
||||||
|
|
||||||
# This will run on the 'default' database.
|
A database Router is a class that provides three methods:
|
||||||
|
|
||||||
|
.. method:: db_for_read(model, **hints)
|
||||||
|
|
||||||
|
Suggest the database that should be used for read operations for
|
||||||
|
objects of type ``model``.
|
||||||
|
|
||||||
|
If a database operation is able to provide any additional
|
||||||
|
information that might assist in selecting a database, it will be
|
||||||
|
provided in the ``hints`` dictionary. Details on valid hints are
|
||||||
|
provided :ref:`below <topics-db-multi-db-hints>`.
|
||||||
|
|
||||||
|
Returns None if there is no suggestion.
|
||||||
|
|
||||||
|
.. method:: db_for_write(model, **hints)
|
||||||
|
|
||||||
|
Suggest the database that should be used for writes of objects of
|
||||||
|
type Model.
|
||||||
|
|
||||||
|
If a database operation is able to provide any additional
|
||||||
|
information that might assist in selecting a database, it will be
|
||||||
|
provided in the ``hints`` dictionary. Details on valid hints are
|
||||||
|
provided :ref:`below <topics-db-multi-db-hints>`.
|
||||||
|
|
||||||
|
Returns None if there is no suggestion.
|
||||||
|
|
||||||
|
.. method:: allow_relation(obj1, obj2, **hints)
|
||||||
|
|
||||||
|
Return True if a relation between obj1 and obj2 should be
|
||||||
|
allowed, False if the relation should be prevented, or None if
|
||||||
|
the router has no opinion. This is purely a validation operation,
|
||||||
|
used by foreign key and many to many operations to determine if a
|
||||||
|
relation should be allowed between two objects.
|
||||||
|
|
||||||
|
.. _topics-db-multi-db-hints:
|
||||||
|
|
||||||
|
Hints
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
The hints received by the database router can be used to decide which
|
||||||
|
database should receive a given request.
|
||||||
|
|
||||||
|
At present, the only hint that will be provided is ``instance``, an
|
||||||
|
object instance that is related to the read or write operation that is
|
||||||
|
underway. This might be the instance that is being saved, or it might
|
||||||
|
be an instance that is being added in a many-to-many relation. In some
|
||||||
|
cases, no instance hint will be provided at all. The router check for
|
||||||
|
the existence of an instance hint, and determine if hat hint should be
|
||||||
|
used to alter routing behavior.
|
||||||
|
|
||||||
|
Using routers
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Database routers are installed using the :setting:`DATABASE_ROUTERS`
|
||||||
|
setting. This setting defines a list of class names, each specifying a
|
||||||
|
router that should be used by the master router
|
||||||
|
(``django.db.router``).
|
||||||
|
|
||||||
|
The master router is used by Django's database operations to allocate
|
||||||
|
database usage. Whenever a query needs to know which database to use,
|
||||||
|
it calls the master router, providing a model and a hint (if
|
||||||
|
available). Django then tries each router in turn until a database
|
||||||
|
suggestion can be found. If no suggestion can be found, it tries the
|
||||||
|
current ``_state.db`` of the hint instance. If a hint instance wasn't
|
||||||
|
provided, or the instance doesn't currently have database state, the
|
||||||
|
master router will allocate the ``default`` database.
|
||||||
|
|
||||||
|
An example
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. admonition:: Example purposes only!
|
||||||
|
|
||||||
|
This example is intended as a demonstration of how the router
|
||||||
|
infrastructure can be used to alter database usage. It
|
||||||
|
intentionally ignores some complex issues in order to
|
||||||
|
demonstrate how routers are used.
|
||||||
|
|
||||||
|
The approach of splitting ``contrib.auth`` onto a different
|
||||||
|
database won't actually work on Postgres, Oracle, or MySQL with
|
||||||
|
InnoDB tables. ForeignKeys to a remote database won't work due as
|
||||||
|
they introduce referential integrity problems. If you're using
|
||||||
|
SQLite or MySQL with MyISAM tables, there is no referential
|
||||||
|
integrity checking, so you will be able to define cross-database
|
||||||
|
foreign keys.
|
||||||
|
|
||||||
|
The master/slave configuration described is also flawed -- it
|
||||||
|
doesn't provide any solution for handling replication lag (i.e.,
|
||||||
|
query inconsistencies introduced because of the time taken for a
|
||||||
|
write to propagate to the slaves). It also doesn't consider the
|
||||||
|
interaction of transactions with the database utiliztion strategy.
|
||||||
|
|
||||||
|
So - what does this mean in practice? Say you want ``contrib.auth`` to
|
||||||
|
exist on the 'credentials' database, and you want all other models in a
|
||||||
|
master/slave relationship between the databses 'master', 'slave1' and
|
||||||
|
'slave2'. To implement this, you would need 2 routers::
|
||||||
|
|
||||||
|
class AuthRouter(object):
|
||||||
|
"""A router to control all database operations on models in
|
||||||
|
the contrib.auth application"""
|
||||||
|
|
||||||
|
def db_for_read(self, model, **hints):
|
||||||
|
"Point all operations on auth models to 'credentials'"
|
||||||
|
if model._meta.app_label == 'auth':
|
||||||
|
return 'credentials'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def db_for_write(self, model, **hints):
|
||||||
|
"Point all operations on auth models to 'credentials'"
|
||||||
|
if model._meta.app_label == 'auth':
|
||||||
|
return 'credentials'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
|
"Allow any relation if a model in Auth is involved"
|
||||||
|
if obj1._meta.app_label == 'auth' or obj2._meta.app_label == 'auth':
|
||||||
|
return True
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class MasterSlaveRouter(object):
|
||||||
|
"""A router that sets up a simple master/slave configuration"""
|
||||||
|
|
||||||
|
def db_for_read(self, model, **hints):
|
||||||
|
"Point all read operations to a random slave"
|
||||||
|
return random.choice(['slave1','slave2'])
|
||||||
|
|
||||||
|
def db_for_write(self, model, **hints):
|
||||||
|
"Point all write operations to the master"
|
||||||
|
return 'master'
|
||||||
|
|
||||||
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
|
"Allow any relation between two objects in the db pool"
|
||||||
|
db_list = ('master','slave1','slave2')
|
||||||
|
if obj1 in db_list and obj2 in db_list:
|
||||||
|
return True
|
||||||
|
return None
|
||||||
|
|
||||||
|
Then, in your settings file, add the following (substituting ``path.to.`` with
|
||||||
|
the actual python path to the module where you define the routers)::
|
||||||
|
|
||||||
|
DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.MasterSlaveRouter']
|
||||||
|
|
||||||
|
With this setup installed, lets run some Django code::
|
||||||
|
|
||||||
|
>>> # This retrieval will be performed on the 'credentials' database
|
||||||
|
>>> fred = User.objects.get(username='fred')
|
||||||
|
>>> fred.first_name = 'Frederick'
|
||||||
|
|
||||||
|
>>> # This save will also be directed to 'credentials'
|
||||||
|
>>> fred.save()
|
||||||
|
|
||||||
|
>>> # These retrieval will be randomly allocated to a slave database
|
||||||
|
>>> dna = Person.objects.get(name='Douglas Adams')
|
||||||
|
|
||||||
|
>>> # A new object has no database allocation when created
|
||||||
|
>>> mh = Book(title='Mostly Harmless')
|
||||||
|
|
||||||
|
>>> # This assignment will consult the router, and set mh onto
|
||||||
|
>>> # the same database as the author object
|
||||||
|
>>> mh.author = dna
|
||||||
|
|
||||||
|
>>> # This save will force the 'mh' instance onto the master database...
|
||||||
|
>>> mh.save()
|
||||||
|
|
||||||
|
>>> # ... but if we re-retrieve the object, it will come back on a slave
|
||||||
|
>>> mh = Book.objects.get(title='Mostly Harmless')
|
||||||
|
|
||||||
|
Manually selecting a database
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Django also provides an API that allows you to maintain complete control
|
||||||
|
over database usage in your code. A manually specified database allocation
|
||||||
|
will take priority over a database allocated by a router.
|
||||||
|
|
||||||
|
Manually selecting a database for a ``QuerySet``
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
You can select the database for a ``QuerySet`` at any point in the
|
||||||
|
``QuerySet`` "chain." Just call ``using()`` on the ``QuerySet`` to get
|
||||||
|
another ``QuerySet`` that uses the specified database.
|
||||||
|
|
||||||
|
``using()`` takes a single argument: the alias of the database on
|
||||||
|
which you want to run the query. For example::
|
||||||
|
|
||||||
|
>>> # This will run on the 'default' database.
|
||||||
>>> Author.objects.all()
|
>>> Author.objects.all()
|
||||||
|
|
||||||
# So will this.
|
>>> # So will this.
|
||||||
>>> Author.objects.using('default').all()
|
>>> Author.objects.using('default').all()
|
||||||
|
|
||||||
# This will run on the 'other' database.
|
>>> # This will run on the 'other' database.
|
||||||
>>> Author.objects.using('other').all()
|
>>> Author.objects.using('other').all()
|
||||||
|
|
||||||
Selecting a database for ``save()``
|
Selecting a database for ``save()``
|
||||||
===================================
|
-----------------------------------
|
||||||
|
|
||||||
Use the ``using`` keyword to ``Model.save()`` to specify to which database the
|
Use the ``using`` keyword to ``Model.save()`` to specify to which
|
||||||
data should be saved.
|
database the data should be saved.
|
||||||
|
|
||||||
For example, to save an object to the ``legacy_users`` database, you'd use this::
|
For example, to save an object to the ``legacy_users`` database, you'd
|
||||||
|
use this::
|
||||||
|
|
||||||
>>> my_object.save(using='legacy_users')
|
>>> my_object.save(using='legacy_users')
|
||||||
|
|
||||||
If you don't specify ``using``, the ``save()`` method will always save into the
|
If you don't specify ``using``, the ``save()`` method will save into
|
||||||
default database.
|
the default database allocated by the routers.
|
||||||
|
|
||||||
Moving an object from one database to another
|
Moving an object from one database to another
|
||||||
---------------------------------------------
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If you've saved an instance to one database, it might be tempting to use
|
If you've saved an instance to one database, it might be tempting to
|
||||||
``save(using=...)`` as a way to migrate the instance to a new database. However,
|
use ``save(using=...)`` as a way to migrate the instance to a new
|
||||||
if you don't take appropriate steps, this could have some unexpected consequences.
|
database. However, if you don't take appropriate steps, this could
|
||||||
|
have some unexpected consequences.
|
||||||
|
|
||||||
Consider the following example::
|
Consider the following example::
|
||||||
|
|
||||||
|
@ -149,16 +340,17 @@ However, if the primary key of ``p`` is already in use on the
|
||||||
will be overridden when ``p`` is saved.
|
will be overridden when ``p`` is saved.
|
||||||
|
|
||||||
You can avoid this in two ways. First, you can clear the primary key
|
You can avoid this in two ways. First, you can clear the primary key
|
||||||
of the instance. If an object has no primary key, Django will treat it as
|
of the instance. If an object has no primary key, Django will treat it
|
||||||
a new object, avoiding any loss of data on the ``second`` database::
|
as a new object, avoiding any loss of data on the ``second``
|
||||||
|
database::
|
||||||
|
|
||||||
>>> p = Person(name='Fred')
|
>>> p = Person(name='Fred')
|
||||||
>>> p.save(using='first')
|
>>> p.save(using='first')
|
||||||
>>> p.pk = None # Clear the primary key.
|
>>> p.pk = None # Clear the primary key.
|
||||||
>>> p.save(using='second') # Write a completely new object.
|
>>> p.save(using='second') # Write a completely new object.
|
||||||
|
|
||||||
The second option is to use the ``force_insert`` option to ``save()`` to ensure
|
The second option is to use the ``force_insert`` option to ``save()``
|
||||||
that Django does a SQL ``INSERT``::
|
to ensure that Django does a SQL ``INSERT``::
|
||||||
|
|
||||||
>>> p = Person(name='Fred')
|
>>> p = Person(name='Fred')
|
||||||
>>> p.save(using='first')
|
>>> p.save(using='first')
|
||||||
|
@ -170,51 +362,53 @@ when you try to save onto the ``second`` database, an error will be
|
||||||
raised.
|
raised.
|
||||||
|
|
||||||
Selecting a database to delete from
|
Selecting a database to delete from
|
||||||
===================================
|
-----------------------------------
|
||||||
|
|
||||||
By default, a call to delete an existing object will be executed on the
|
By default, a call to delete an existing object will be executed on
|
||||||
same database that was used to retrieve the object in the first place::
|
the same database that was used to retrieve the object in the first
|
||||||
|
place::
|
||||||
|
|
||||||
>>> u = User.objects.using('legacy_users').get(username='fred')
|
>>> u = User.objects.using('legacy_users').get(username='fred')
|
||||||
>>> u.delete() # will delete from the `legacy_users` database
|
>>> u.delete() # will delete from the `legacy_users` database
|
||||||
|
|
||||||
To specify the database from which a model will be deleted, pass a
|
To specify the database from which a model will be deleted, pass a
|
||||||
``using`` keyword argument to the ``Model.delete()`` method. This argument
|
``using`` keyword argument to the ``Model.delete()`` method. This
|
||||||
works just like the ``using`` keyword argument to ``save()``.
|
argument works just like the ``using`` keyword argument to ``save()``.
|
||||||
|
|
||||||
For example, if you're migrating a user from the ``legacy_users`` database
|
For example, if you're migrating a user from the ``legacy_users``
|
||||||
to the ``new_users`` database, you might use these commands::
|
database to the ``new_users`` database, you might use these commands::
|
||||||
|
|
||||||
>>> user_obj.save(using='new_users')
|
>>> user_obj.save(using='new_users')
|
||||||
>>> user_obj.delete(using='legacy_users')
|
>>> user_obj.delete(using='legacy_users')
|
||||||
|
|
||||||
Using managers with multiple databases
|
Using managers with multiple databases
|
||||||
======================================
|
--------------------------------------
|
||||||
|
|
||||||
Use the ``db_manager()`` method on managers to give managers access to a
|
Use the ``db_manager()`` method on managers to give managers access to
|
||||||
non-default database.
|
a non-default database.
|
||||||
|
|
||||||
For example, say you have a custom manager method that touches the database --
|
For example, say you have a custom manager method that touches the
|
||||||
``User.objects.create_user()``. Because ``create_user()`` is a
|
database -- ``User.objects.create_user()``. Because ``create_user()``
|
||||||
manager method, not a ``QuerySet`` method, you can't do
|
is a manager method, not a ``QuerySet`` method, you can't do
|
||||||
``User.objects.using('new_users').create_user()``. (The ``create_user()`` method
|
``User.objects.using('new_users').create_user()``. (The
|
||||||
is only available on ``User.objects``, the manager, not on ``QuerySet`` objects
|
``create_user()`` method is only available on ``User.objects``, the
|
||||||
derived from the manager.) The solution is to use ``db_manager()``, like this::
|
manager, not on ``QuerySet`` objects derived from the manager.) The
|
||||||
|
solution is to use ``db_manager()``, like this::
|
||||||
|
|
||||||
User.objects.db_manager('new_users').create_user(...)
|
User.objects.db_manager('new_users').create_user(...)
|
||||||
|
|
||||||
``db_manager()`` returns a copy of the manager bound to the database you specify.
|
``db_manager()`` returns a copy of the manager bound to the database you specify.
|
||||||
|
|
||||||
Using ``get_query_set()`` with multiple databases
|
Using ``get_query_set()`` with multiple databases
|
||||||
-------------------------------------------------
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If you're overriding ``get_query_set()`` on your manager, be sure to either
|
If you're overriding ``get_query_set()`` on your manager, be sure to
|
||||||
call the method on the parent (using ``super()``) or do the appropriate
|
either call the method on the parent (using ``super()``) or do the
|
||||||
handling of the ``_db`` attribute on the manager (a string containing the name
|
appropriate handling of the ``_db`` attribute on the manager (a string
|
||||||
of the database to use).
|
containing the name of the database to use).
|
||||||
|
|
||||||
For example, if you want to return a custom ``QuerySet`` class from the
|
For example, if you want to return a custom ``QuerySet`` class from
|
||||||
``get_query_set`` method, you could do this::
|
the ``get_query_set`` method, you could do this::
|
||||||
|
|
||||||
class MyManager(models.Manager):
|
class MyManager(models.Manager):
|
||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
|
@ -228,9 +422,9 @@ Exposing multiple databases in Django's admin interface
|
||||||
|
|
||||||
Django's admin doesn't have any explicit support for multiple
|
Django's admin doesn't have any explicit support for multiple
|
||||||
databases. If you want to provide an admin interface for a model on a
|
databases. If you want to provide an admin interface for a model on a
|
||||||
database other than ``default``, you'll need to write custom
|
database other than that that specified by your router chain, you'll
|
||||||
:class:`~django.contrib.admin.ModelAdmin` classes that will direct the
|
need to write custom :class:`~django.contrib.admin.ModelAdmin` classes
|
||||||
admin to use a specific database for content.
|
that will direct the admin to use a specific database for content.
|
||||||
|
|
||||||
``ModelAdmin`` objects have four methods that require customization for
|
``ModelAdmin`` objects have four methods that require customization for
|
||||||
multiple-database support::
|
multiple-database support::
|
||||||
|
@ -257,11 +451,11 @@ multiple-database support::
|
||||||
# on the 'other' database.
|
# on the 'other' database.
|
||||||
return super(MultiDBModelAdmin, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
|
return super(MultiDBModelAdmin, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
|
||||||
|
|
||||||
The implementation provided here implements a multi-database strategy where
|
The implementation provided here implements a multi-database strategy
|
||||||
all objects of a given type are stored on a specific database (e.g.,
|
where all objects of a given type are stored on a specific database
|
||||||
all ``User`` objects are in the ``other`` database). If your usage of
|
(e.g., all ``User`` objects are in the ``other`` database). If your
|
||||||
multiple databases is more complex, your ``ModelAdmin`` will need to reflect
|
usage of multiple databases is more complex, your ``ModelAdmin`` will
|
||||||
that strategy.
|
need to reflect that strategy.
|
||||||
|
|
||||||
Inlines can be handled in a similar fashion. They require three customized methods::
|
Inlines can be handled in a similar fashion. They require three customized methods::
|
||||||
|
|
||||||
|
@ -282,8 +476,8 @@ Inlines can be handled in a similar fashion. They require three customized metho
|
||||||
# on the 'other' database.
|
# on the 'other' database.
|
||||||
return super(MultiDBTabularInline, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
|
return super(MultiDBTabularInline, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
|
||||||
|
|
||||||
Once you've written your model admin definitions, they can be registered with
|
Once you've written your model admin definitions, they can be
|
||||||
any ``Admin`` instance::
|
registered with any ``Admin`` instance::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes import generic
|
||||||
from django.db import models, DEFAULT_DB_ALIAS
|
from django.db import models
|
||||||
|
|
||||||
class Review(models.Model):
|
class Review(models.Model):
|
||||||
source = models.CharField(max_length=100)
|
source = models.CharField(max_length=100)
|
||||||
|
@ -36,6 +36,7 @@ class Book(models.Model):
|
||||||
authors = models.ManyToManyField(Person)
|
authors = models.ManyToManyField(Person)
|
||||||
editor = models.ForeignKey(Person, null=True, related_name='edited')
|
editor = models.ForeignKey(Person, null=True, related_name='edited')
|
||||||
reviews = generic.GenericRelation(Review)
|
reviews = generic.GenericRelation(Review)
|
||||||
|
pages = models.IntegerField(default=100)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
|
@ -3,7 +3,8 @@ import pickle
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import connections
|
from django.db import connections, router, DEFAULT_DB_ALIAS
|
||||||
|
from django.db.utils import ConnectionRouter
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from models import Book, Person, Review, UserProfile
|
from models import Book, Person, Review, UserProfile
|
||||||
|
@ -18,6 +19,16 @@ except ImportError:
|
||||||
class QueryTestCase(TestCase):
|
class QueryTestCase(TestCase):
|
||||||
multi_db = True
|
multi_db = True
|
||||||
|
|
||||||
|
def test_db_selection(self):
|
||||||
|
"Check that querysets will use the default databse by default"
|
||||||
|
self.assertEquals(Book.objects.db, DEFAULT_DB_ALIAS)
|
||||||
|
self.assertEquals(Book.objects.all().db, DEFAULT_DB_ALIAS)
|
||||||
|
|
||||||
|
self.assertEquals(Book.objects.using('other').db, 'other')
|
||||||
|
|
||||||
|
self.assertEquals(Book.objects.db_manager('other').db, 'other')
|
||||||
|
self.assertEquals(Book.objects.db_manager('other').all().db, 'other')
|
||||||
|
|
||||||
def test_default_creation(self):
|
def test_default_creation(self):
|
||||||
"Objects created on the default database don't leak onto other databases"
|
"Objects created on the default database don't leak onto other databases"
|
||||||
# Create a book on the default database using create()
|
# Create a book on the default database using create()
|
||||||
|
@ -259,53 +270,53 @@ class QueryTestCase(TestCase):
|
||||||
self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)),
|
self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)),
|
||||||
[u'Mark Pilgrim'])
|
[u'Mark Pilgrim'])
|
||||||
|
|
||||||
# def test_m2m_cross_database_protection(self):
|
def test_m2m_cross_database_protection(self):
|
||||||
# "Operations that involve sharing M2M objects across databases raise an error"
|
"Operations that involve sharing M2M objects across databases raise an error"
|
||||||
# # Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
# pro = Book.objects.create(title="Pro Django",
|
pro = Book.objects.create(title="Pro Django",
|
||||||
# published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
# marty = Person.objects.create(name="Marty Alchin")
|
marty = Person.objects.create(name="Marty Alchin")
|
||||||
|
|
||||||
# # Create a book and author on the other database
|
# Create a book and author on the other database
|
||||||
# dive = Book.objects.using('other').create(title="Dive into Python",
|
dive = Book.objects.using('other').create(title="Dive into Python",
|
||||||
# published=datetime.date(2009, 5, 4))
|
published=datetime.date(2009, 5, 4))
|
||||||
|
|
||||||
# mark = Person.objects.using('other').create(name="Mark Pilgrim")
|
mark = Person.objects.using('other').create(name="Mark Pilgrim")
|
||||||
# # Set a foreign key set with an object from a different database
|
# Set a foreign key set with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# marty.book_set = [pro, dive]
|
marty.book_set = [pro, dive]
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Add to an m2m with an object from a different database
|
# Add to an m2m with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# marty.book_set.add(dive)
|
marty.book_set.add(dive)
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Set a m2m with an object from a different database
|
# Set a m2m with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# marty.book_set = [pro, dive]
|
marty.book_set = [pro, dive]
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Add to a reverse m2m with an object from a different database
|
# Add to a reverse m2m with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# dive.authors.add(marty)
|
dive.authors.add(marty)
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Set a reverse m2m with an object from a different database
|
# Set a reverse m2m with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# dive.authors = [mark, marty]
|
dive.authors = [mark, marty]
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
def test_foreign_key_separation(self):
|
def test_foreign_key_separation(self):
|
||||||
"FK fields are constrained to a single database"
|
"FK fields are constrained to a single database"
|
||||||
|
@ -401,88 +412,88 @@ class QueryTestCase(TestCase):
|
||||||
self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)),
|
self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)),
|
||||||
[])
|
[])
|
||||||
|
|
||||||
# def test_foreign_key_cross_database_protection(self):
|
def test_foreign_key_cross_database_protection(self):
|
||||||
# "Operations that involve sharing FK objects across databases raise an error"
|
"Operations that involve sharing FK objects across databases raise an error"
|
||||||
# # Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
# pro = Book.objects.create(title="Pro Django",
|
pro = Book.objects.create(title="Pro Django",
|
||||||
# published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
# marty = Person.objects.create(name="Marty Alchin")
|
marty = Person.objects.create(name="Marty Alchin")
|
||||||
|
|
||||||
# # Create a book and author on the other database
|
# Create a book and author on the other database
|
||||||
# dive = Book.objects.using('other').create(title="Dive into Python",
|
dive = Book.objects.using('other').create(title="Dive into Python",
|
||||||
# published=datetime.date(2009, 5, 4))
|
published=datetime.date(2009, 5, 4))
|
||||||
|
|
||||||
# mark = Person.objects.using('other').create(name="Mark Pilgrim")
|
mark = Person.objects.using('other').create(name="Mark Pilgrim")
|
||||||
|
|
||||||
# # Set a foreign key with an object from a different database
|
# Set a foreign key with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# dive.editor = marty
|
dive.editor = marty
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Set a foreign key set with an object from a different database
|
# Set a foreign key set with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# marty.edited = [pro, dive]
|
marty.edited = [pro, dive]
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # Add to a foreign key set with an object from a different database
|
# Add to a foreign key set with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# marty.edited.add(dive)
|
marty.edited.add(dive)
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # BUT! if you assign a FK object when the base object hasn't
|
# BUT! if you assign a FK object when the base object hasn't
|
||||||
# # been saved yet, you implicitly assign the database for the
|
# been saved yet, you implicitly assign the database for the
|
||||||
# # base object.
|
# base object.
|
||||||
# chris = Person(name="Chris Mills")
|
chris = Person(name="Chris Mills")
|
||||||
# html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
|
html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
|
||||||
# # initially, no db assigned
|
# initially, no db assigned
|
||||||
# self.assertEquals(chris._state.db, None)
|
self.assertEquals(chris._state.db, None)
|
||||||
# self.assertEquals(html5._state.db, None)
|
self.assertEquals(html5._state.db, None)
|
||||||
|
|
||||||
# # old object comes from 'other', so the new object is set to use 'other'...
|
# old object comes from 'other', so the new object is set to use 'other'...
|
||||||
# dive.editor = chris
|
dive.editor = chris
|
||||||
# html5.editor = mark
|
html5.editor = mark
|
||||||
# # self.assertEquals(chris._state.db, 'other')
|
self.assertEquals(chris._state.db, 'other')
|
||||||
# self.assertEquals(html5._state.db, 'other')
|
self.assertEquals(html5._state.db, 'other')
|
||||||
# # ... but it isn't saved yet
|
# ... but it isn't saved yet
|
||||||
# self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
|
self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
|
||||||
# [u'Mark Pilgrim'])
|
[u'Mark Pilgrim'])
|
||||||
# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
||||||
# [u'Dive into Python'])
|
[u'Dive into Python'])
|
||||||
|
|
||||||
# # When saved (no using required), new objects goes to 'other'
|
# When saved (no using required), new objects goes to 'other'
|
||||||
# chris.save()
|
chris.save()
|
||||||
# html5.save()
|
html5.save()
|
||||||
# self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
|
self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
|
||||||
# [u'Marty Alchin'])
|
[u'Marty Alchin'])
|
||||||
# self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
|
self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
|
||||||
# [u'Chris Mills', u'Mark Pilgrim'])
|
[u'Chris Mills', u'Mark Pilgrim'])
|
||||||
# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
||||||
# [u'Pro Django'])
|
[u'Pro Django'])
|
||||||
# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
||||||
# [u'Dive into HTML5', u'Dive into Python'])
|
[u'Dive into HTML5', u'Dive into Python'])
|
||||||
|
|
||||||
# # This also works if you assign the FK in the constructor
|
# This also works if you assign the FK in the constructor
|
||||||
# water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
|
water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
|
||||||
# self.assertEquals(water._state.db, 'other')
|
self.assertEquals(water._state.db, 'other')
|
||||||
# # ... but it isn't saved yet
|
# ... but it isn't saved yet
|
||||||
# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
||||||
# [u'Pro Django'])
|
[u'Pro Django'])
|
||||||
# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
||||||
# [u'Dive into HTML5', u'Dive into Python'])
|
[u'Dive into HTML5', u'Dive into Python'])
|
||||||
|
|
||||||
# # When saved, the new book goes to 'other'
|
# When saved, the new book goes to 'other'
|
||||||
# water.save()
|
water.save()
|
||||||
# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
|
||||||
# [u'Pro Django'])
|
[u'Pro Django'])
|
||||||
# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
|
||||||
# [u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
|
[u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
|
||||||
|
|
||||||
def test_generic_key_separation(self):
|
def test_generic_key_separation(self):
|
||||||
"Generic fields are constrained to a single database"
|
"Generic fields are constrained to a single database"
|
||||||
|
@ -555,56 +566,56 @@ class QueryTestCase(TestCase):
|
||||||
self.assertEquals(list(Review.objects.using('other').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'])
|
[u'Python Daily'])
|
||||||
|
|
||||||
# def test_generic_key_cross_database_protection(self):
|
def test_generic_key_cross_database_protection(self):
|
||||||
## "Operations that involve sharing FK objects across databases raise an error"
|
"Operations that involve sharing generic key objects across databases raise an error"
|
||||||
## # Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
## pro = Book.objects.create(title="Pro Django",
|
pro = Book.objects.create(title="Pro Django",
|
||||||
## published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
## review1 = Review.objects.create(source="Python Monthly", content_object=pro)
|
review1 = Review.objects.create(source="Python Monthly", content_object=pro)
|
||||||
|
|
||||||
## # Create a book and author on the other database
|
# Create a book and author on the other database
|
||||||
## dive = Book.objects.using('other').create(title="Dive into Python",
|
dive = Book.objects.using('other').create(title="Dive into Python",
|
||||||
## published=datetime.date(2009, 5, 4))
|
published=datetime.date(2009, 5, 4))
|
||||||
|
|
||||||
## review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
|
review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
|
||||||
|
|
||||||
## # Set a foreign key with an object from a different database
|
# Set a foreign key with an object from a different database
|
||||||
## try:
|
try:
|
||||||
## review1.content_object = dive
|
review1.content_object = dive
|
||||||
## self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
## except ValueError:
|
except ValueError:
|
||||||
## pass
|
pass
|
||||||
|
|
||||||
# # Add to a foreign key set with an object from a different database
|
# Add to a foreign key set with an object from a different database
|
||||||
# try:
|
try:
|
||||||
# dive.reviews.add(review1)
|
dive.reviews.add(review1)
|
||||||
# self.fail("Shouldn't be able to assign across databases")
|
self.fail("Shouldn't be able to assign across databases")
|
||||||
# except ValueError:
|
except ValueError:
|
||||||
# pass
|
pass
|
||||||
|
|
||||||
# # BUT! if you assign a FK object when the base object hasn't
|
# BUT! if you assign a FK object when the base object hasn't
|
||||||
# # been saved yet, you implicitly assign the database for the
|
# been saved yet, you implicitly assign the database for the
|
||||||
# # base object.
|
# base object.
|
||||||
# review3 = Review(source="Python Daily")
|
review3 = Review(source="Python Daily")
|
||||||
# # initially, no db assigned
|
# initially, no db assigned
|
||||||
# self.assertEquals(review3._state.db, None)
|
self.assertEquals(review3._state.db, None)
|
||||||
|
|
||||||
# # Dive comes from 'other', so review3 is set to use 'other'...
|
# Dive comes from 'other', so review3 is set to use 'other'...
|
||||||
# review3.content_object = dive
|
review3.content_object = dive
|
||||||
# self.assertEquals(review3._state.db, 'other')
|
self.assertEquals(review3._state.db, 'other')
|
||||||
# # ... but it isn't saved yet
|
# ... but it isn't saved yet
|
||||||
# self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
|
self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
|
||||||
# [u'Python Monthly'])
|
[u'Python Monthly'])
|
||||||
# self.assertEquals(list(Review.objects.using('other').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'])
|
[u'Python Weekly'])
|
||||||
|
|
||||||
# # When saved, John goes to 'other'
|
# When saved, John goes to 'other'
|
||||||
# review3.save()
|
review3.save()
|
||||||
# self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
|
self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
|
||||||
# [u'Python Monthly'])
|
[u'Python Monthly'])
|
||||||
# self.assertEquals(list(Review.objects.using('other').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', u'Python Weekly'])
|
[u'Python Daily', u'Python Weekly'])
|
||||||
|
|
||||||
def test_ordering(self):
|
def test_ordering(self):
|
||||||
"get_next_by_XXX commands stick to a single database"
|
"get_next_by_XXX commands stick to a single database"
|
||||||
|
@ -630,6 +641,388 @@ class QueryTestCase(TestCase):
|
||||||
val = Book.objects.raw('SELECT id FROM "multiple_database_book"').using('other')
|
val = Book.objects.raw('SELECT id FROM "multiple_database_book"').using('other')
|
||||||
self.assertEqual(map(lambda o: o.pk, val), [dive.pk])
|
self.assertEqual(map(lambda o: o.pk, val), [dive.pk])
|
||||||
|
|
||||||
|
class TestRouter(object):
|
||||||
|
# A test router. The behaviour is vaguely master/slave, but the
|
||||||
|
# databases aren't assumed to propagate changes.
|
||||||
|
def db_for_read(self, model, instance=None, **hints):
|
||||||
|
if instance:
|
||||||
|
return instance._state.db or 'other'
|
||||||
|
return 'other'
|
||||||
|
|
||||||
|
def db_for_write(self, model, **hints):
|
||||||
|
return DEFAULT_DB_ALIAS
|
||||||
|
|
||||||
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
|
return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other')
|
||||||
|
|
||||||
|
class RouterTestCase(TestCase):
|
||||||
|
multi_db = True
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Make the 'other' database appear to be a slave of the 'default'
|
||||||
|
self.old_routers = router.routers
|
||||||
|
router.routers = [TestRouter()]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# Restore the 'other' database as an independent database
|
||||||
|
router.routers = self.old_routers
|
||||||
|
|
||||||
|
def test_db_selection(self):
|
||||||
|
"Check that querysets obey the router for db suggestions"
|
||||||
|
self.assertEquals(Book.objects.db, 'other')
|
||||||
|
self.assertEquals(Book.objects.all().db, 'other')
|
||||||
|
|
||||||
|
self.assertEquals(Book.objects.using('default').db, 'default')
|
||||||
|
|
||||||
|
self.assertEquals(Book.objects.db_manager('default').db, 'default')
|
||||||
|
self.assertEquals(Book.objects.db_manager('default').all().db, 'default')
|
||||||
|
|
||||||
|
def test_database_routing(self):
|
||||||
|
marty = Person.objects.using('default').create(name="Marty Alchin")
|
||||||
|
pro = Book.objects.using('default').create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16),
|
||||||
|
editor=marty)
|
||||||
|
pro.authors = [marty]
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
# An update query will be routed to the default database
|
||||||
|
Book.objects.filter(title='Pro Django').update(pages=200)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# By default, the get query will be directed to 'other'
|
||||||
|
Book.objects.get(title='Pro Django')
|
||||||
|
self.fail("Shouldn't be able to find the book")
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# But the same query issued explicitly at a database will work.
|
||||||
|
pro = Book.objects.using('default').get(title='Pro Django')
|
||||||
|
|
||||||
|
# Check that the update worked.
|
||||||
|
self.assertEquals(pro.pages, 200)
|
||||||
|
|
||||||
|
# An update query with an explicit using clause will be routed
|
||||||
|
# to the requested database.
|
||||||
|
Book.objects.using('other').filter(title='Dive into Python').update(pages=300)
|
||||||
|
self.assertEquals(Book.objects.get(title='Dive into Python').pages, 300)
|
||||||
|
|
||||||
|
# Related object queries stick to the same database
|
||||||
|
# as the original object, regardless of the router
|
||||||
|
self.assertEquals(list(pro.authors.values_list('name', flat=True)), [u'Marty Alchin'])
|
||||||
|
self.assertEquals(pro.editor.name, u'Marty Alchin')
|
||||||
|
|
||||||
|
# get_or_create is a special case. The get needs to be targetted at
|
||||||
|
# the write database in order to avoid potential transaction
|
||||||
|
# consistency problems
|
||||||
|
book, created = Book.objects.get_or_create(title="Pro Django")
|
||||||
|
self.assertFalse(created)
|
||||||
|
|
||||||
|
book, created = Book.objects.get_or_create(title="Dive Into Python",
|
||||||
|
defaults={'published':datetime.date(2009, 5, 4)})
|
||||||
|
self.assertTrue(created)
|
||||||
|
|
||||||
|
# Check the head count of objects
|
||||||
|
self.assertEquals(Book.objects.using('default').count(), 2)
|
||||||
|
self.assertEquals(Book.objects.using('other').count(), 1)
|
||||||
|
# If a database isn't specified, the read database is used
|
||||||
|
self.assertEquals(Book.objects.count(), 1)
|
||||||
|
|
||||||
|
# A delete query will also be routed to the default database
|
||||||
|
Book.objects.filter(pages__gt=150).delete()
|
||||||
|
|
||||||
|
# The default database has lost the book.
|
||||||
|
self.assertEquals(Book.objects.using('default').count(), 1)
|
||||||
|
self.assertEquals(Book.objects.using('other').count(), 1)
|
||||||
|
|
||||||
|
def test_foreign_key_cross_database_protection(self):
|
||||||
|
"Foreign keys can cross databases if they two databases have a common source"
|
||||||
|
# Create a book and author on the default database
|
||||||
|
pro = Book.objects.using('default').create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
|
marty = Person.objects.using('default').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 = Person.objects.using('other').create(name="Mark Pilgrim")
|
||||||
|
|
||||||
|
# Set a foreign key with an object from a different database
|
||||||
|
try:
|
||||||
|
dive.editor = marty
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments of original objects haven't changed...
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# ... but they will when the affected object is saved.
|
||||||
|
dive.save()
|
||||||
|
self.assertEquals(dive._state.db, 'default')
|
||||||
|
|
||||||
|
# ...and the source database now has a copy of any object saved
|
||||||
|
try:
|
||||||
|
Book.objects.using('default').get(title='Dive into Python').delete()
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
self.fail('Source database should have a copy of saved object')
|
||||||
|
|
||||||
|
# This isn't a real master-slave database, so restore the original from other
|
||||||
|
dive = Book.objects.using('other').get(title='Dive into Python')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
|
||||||
|
# Set a foreign key set with an object from a different database
|
||||||
|
try:
|
||||||
|
marty.edited = [pro, dive]
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Assignment implies a save, so database assignments of original objects have changed...
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'default')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# ...and the source database now has a copy of any object saved
|
||||||
|
try:
|
||||||
|
Book.objects.using('default').get(title='Dive into Python').delete()
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
self.fail('Source database should have a copy of saved object')
|
||||||
|
|
||||||
|
# This isn't a real master-slave database, so restore the original from other
|
||||||
|
dive = Book.objects.using('other').get(title='Dive into Python')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
|
||||||
|
# Add to a foreign key set with an object from a different database
|
||||||
|
try:
|
||||||
|
marty.edited.add(dive)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Add implies a save, so database assignments of original objects have changed...
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'default')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# ...and the source database now has a copy of any object saved
|
||||||
|
try:
|
||||||
|
Book.objects.using('default').get(title='Dive into Python').delete()
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
self.fail('Source database should have a copy of saved object')
|
||||||
|
|
||||||
|
# This isn't a real master-slave database, so restore the original from other
|
||||||
|
dive = Book.objects.using('other').get(title='Dive into Python')
|
||||||
|
|
||||||
|
# If you assign a FK object when the base object hasn't
|
||||||
|
# been saved yet, you implicitly assign the database for the
|
||||||
|
# base object.
|
||||||
|
chris = Person(name="Chris Mills")
|
||||||
|
html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
|
||||||
|
# initially, no db assigned
|
||||||
|
self.assertEquals(chris._state.db, None)
|
||||||
|
self.assertEquals(html5._state.db, None)
|
||||||
|
|
||||||
|
# old object comes from 'other', so the new object is set to use the
|
||||||
|
# source of 'other'...
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
dive.editor = chris
|
||||||
|
html5.editor = mark
|
||||||
|
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
self.assertEquals(chris._state.db, 'default')
|
||||||
|
self.assertEquals(html5._state.db, 'default')
|
||||||
|
|
||||||
|
# This also works if you assign the FK in the constructor
|
||||||
|
water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
|
||||||
|
self.assertEquals(water._state.db, 'default')
|
||||||
|
|
||||||
|
def test_m2m_cross_database_protection(self):
|
||||||
|
"M2M relations can cross databases if the database share a source"
|
||||||
|
# Create books and authors on the inverse to the usual database
|
||||||
|
pro = Book.objects.using('other').create(pk=1, title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
|
marty = Person.objects.using('other').create(pk=1, name="Marty Alchin")
|
||||||
|
|
||||||
|
dive = Book.objects.using('default').create(pk=2, title="Dive into Python",
|
||||||
|
published=datetime.date(2009, 5, 4))
|
||||||
|
|
||||||
|
mark = Person.objects.using('default').create(pk=2, name="Mark Pilgrim")
|
||||||
|
|
||||||
|
# Now save back onto the usual databse.
|
||||||
|
# This simulates master/slave - the objects exist on both database,
|
||||||
|
# but the _state.db is as it is for all other tests.
|
||||||
|
pro.save(using='default')
|
||||||
|
marty.save(using='default')
|
||||||
|
dive.save(using='other')
|
||||||
|
mark.save(using='other')
|
||||||
|
|
||||||
|
# Check that we have 2 of both types of object on both databases
|
||||||
|
self.assertEquals(Book.objects.using('default').count(), 2)
|
||||||
|
self.assertEquals(Book.objects.using('other').count(), 2)
|
||||||
|
self.assertEquals(Person.objects.using('default').count(), 2)
|
||||||
|
self.assertEquals(Person.objects.using('other').count(), 2)
|
||||||
|
|
||||||
|
# Set a m2m set with an object from a different database
|
||||||
|
try:
|
||||||
|
marty.book_set = [pro, dive]
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments don't change
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# All m2m relations should be saved on the default database
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('default').count(), 2)
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
|
||||||
|
|
||||||
|
# Reset relations
|
||||||
|
Book.authors.through.objects.using('default').delete()
|
||||||
|
|
||||||
|
# Add to an m2m with an object from a different database
|
||||||
|
try:
|
||||||
|
marty.book_set.add(dive)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments don't change
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# All m2m relations should be saved on the default database
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('default').count(), 1)
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
|
||||||
|
|
||||||
|
# Reset relations
|
||||||
|
Book.authors.through.objects.using('default').delete()
|
||||||
|
|
||||||
|
# Set a reverse m2m with an object from a different database
|
||||||
|
try:
|
||||||
|
dive.authors = [mark, marty]
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments don't change
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# All m2m relations should be saved on the default database
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('default').count(), 2)
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
|
||||||
|
|
||||||
|
# Reset relations
|
||||||
|
Book.authors.through.objects.using('default').delete()
|
||||||
|
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('default').count(), 0)
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
|
||||||
|
|
||||||
|
# Add to a reverse m2m with an object from a different database
|
||||||
|
try:
|
||||||
|
dive.authors.add(marty)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments don't change
|
||||||
|
self.assertEquals(marty._state.db, 'default')
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(mark._state.db, 'other')
|
||||||
|
|
||||||
|
# All m2m relations should be saved on the default database
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('default').count(), 1)
|
||||||
|
self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
|
||||||
|
|
||||||
|
def test_generic_key_cross_database_protection(self):
|
||||||
|
"Generic Key operations can span databases if they share a source"
|
||||||
|
# Create a book and author on the default database
|
||||||
|
pro = Book.objects.using('default'
|
||||||
|
).create(title="Pro Django", published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
|
review1 = Review.objects.using('default'
|
||||||
|
).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 generic foreign key with an object from a different database
|
||||||
|
try:
|
||||||
|
review1.content_object = dive
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments of original objects haven't changed...
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(review1._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(review2._state.db, 'other')
|
||||||
|
|
||||||
|
# ... but they will when the affected object is saved.
|
||||||
|
dive.save()
|
||||||
|
self.assertEquals(review1._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'default')
|
||||||
|
|
||||||
|
# ...and the source database now has a copy of any object saved
|
||||||
|
try:
|
||||||
|
Book.objects.using('default').get(title='Dive into Python').delete()
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
self.fail('Source database should have a copy of saved object')
|
||||||
|
|
||||||
|
# This isn't a real master-slave database, so restore the original from other
|
||||||
|
dive = Book.objects.using('other').get(title='Dive into Python')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
|
||||||
|
# Add to a generic foreign key set with an object from a different database
|
||||||
|
try:
|
||||||
|
dive.reviews.add(review1)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Assignment across master/slave databases with a common source should be ok")
|
||||||
|
|
||||||
|
# Database assignments of original objects haven't changed...
|
||||||
|
self.assertEquals(pro._state.db, 'default')
|
||||||
|
self.assertEquals(review1._state.db, 'default')
|
||||||
|
self.assertEquals(dive._state.db, 'other')
|
||||||
|
self.assertEquals(review2._state.db, 'other')
|
||||||
|
|
||||||
|
# ... but they will when the affected object is saved.
|
||||||
|
dive.save()
|
||||||
|
self.assertEquals(dive._state.db, 'default')
|
||||||
|
|
||||||
|
# ...and the source database now has a copy of any object saved
|
||||||
|
try:
|
||||||
|
Book.objects.using('default').get(title='Dive into Python').delete()
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
self.fail('Source database should have a copy of saved object')
|
||||||
|
|
||||||
|
# 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 the source of 'other'...
|
||||||
|
review3.content_object = dive
|
||||||
|
self.assertEquals(review3._state.db, 'default')
|
||||||
|
|
||||||
|
|
||||||
class UserProfileTestCase(TestCase):
|
class UserProfileTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue