Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #12540, #12541 -- Added database routers, allowing for configur…

…able 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
  • Loading branch information...
commit 1b3dc8ad9a28486542f766ff93318aa6b4f5999b 1 parent acc095c
@freakboy3742 freakboy3742 authored
View
5 django/conf/global_settings.py
@@ -128,6 +128,7 @@
SEND_BROKEN_LINK_EMAILS = False
# Database connection info.
+# Legacy format
DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = '' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
@@ -136,9 +137,13 @@
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
DATABASE_OPTIONS = {} # Set to empty dictionary for default.
+# New format
DATABASES = {
}
+# Classes used to implement db routing behaviour
+DATABASE_ROUTERS = []
+
# The email backend to use. For possible shortcuts see django.core.mail.
# The default is to use the SMTP backend.
# Third-party backends can be specified by providing a Python path
View
2  django/contrib/auth/models.py
@@ -3,7 +3,7 @@
from django.contrib import auth
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.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_str
View
4 django/contrib/contenttypes/generic.py
@@ -5,7 +5,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
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.loading import get_model
from django.forms import ModelForm
@@ -255,7 +255,7 @@ def add(self, *objs):
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
setattr(obj, self.content_type_field_name, self.content_type)
setattr(obj, self.object_id_field_name, self.pk_val)
- obj.save(using=self.instance._state.db)
+ obj.save()
add.alters_data = True
def remove(self, *objs):
View
2  django/contrib/contenttypes/models.py
@@ -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.encoding import smart_unicode
View
2  django/contrib/gis/db/models/sql/query.py
@@ -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.contrib.gis.db.models.fields import GeometryField
View
6 django/db/__init__.py
@@ -1,13 +1,12 @@
from django.conf import settings
from django.core import signals
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
-__all__ = ('backend', 'connection', 'connections', 'DatabaseError',
+__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
'IntegrityError', 'DEFAULT_DB_ALIAS')
-DEFAULT_DB_ALIAS = 'default'
# For backwards compatibility - Port any old database settings over to
# the new values.
@@ -61,6 +60,7 @@
connections = ConnectionHandler(settings.DATABASES)
+router = ConnectionRouter(settings.DATABASE_ROUTERS)
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
# for backend bits.
View
8 django/db/models/base.py
@@ -10,7 +10,7 @@
from django.db.models.query import delete_objects, Q
from django.db.models.query_utils import CollectedObjects, DeferredAttribute
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.loading import register_models, get_model
from django.utils.translation import ugettext_lazy as _
@@ -439,7 +439,7 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
need for overrides of save() to pass around internal-only parameters
('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]
assert not (force_insert and force_update)
if cls is None:
@@ -592,7 +592,7 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
parent_obj._collect_sub_objects(seen_objs)
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]
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 @@ def _perform_unique_checks(self, unique_checks):
# no value, skip the lookup
continue
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
lookup_kwargs[str(field_name)] = lookup_value
View
76 django/db/models/fields/related.py
@@ -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.models import signals, get_model
from django.db.models.fields import (AutoField, Field, IntegerField,
@@ -197,7 +198,8 @@ def __get__(self, instance, instance_type=None):
return getattr(instance, self.cache_name)
except AttributeError:
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)
return rel_obj
@@ -218,6 +220,15 @@ def __set__(self, instance, value):
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
(value, instance._meta.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
setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname))
@@ -260,11 +271,11 @@ def __get__(self, instance, instance_type=None):
# If the related manager indicates that it should be used for
# related fields, respect that.
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):
- rel_obj = rel_mgr.using(using).get(**params)
+ rel_obj = rel_mgr.using(db).get(**params)
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)
return rel_obj
@@ -281,14 +292,15 @@ def __set__(self, instance, value):
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
(value, instance._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:
- instance._state.db = value._state.db
- else:#elif value._state.db is None:
- value._state.db = instance._state.db
-# 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"' %
-# (value, instance._state.db, value._state.db))
+ 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))
# 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
@@ -370,15 +382,15 @@ def create_manager(self, instance, superclass):
class RelatedManager(superclass):
def get_query_set(self):
- using = instance._state.db or DEFAULT_DB_ALIAS
- return superclass.get_query_set(self).using(using).filter(**(self.core_filters))
+ db = router.db_for_read(rel_model, instance=instance)
+ return superclass.get_query_set(self).using(db).filter(**(self.core_filters))
def add(self, *objs):
for obj in objs:
if not isinstance(obj, self.model):
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
setattr(obj, rel_field.name, instance)
- obj.save(using=instance._state.db)
+ obj.save()
add.alters_data = True
def create(self, **kwargs):
@@ -390,8 +402,8 @@ def get_or_create(self, **kwargs):
# Update kwargs with the related object that this
# ForeignRelatedObjectsDescriptor knows about.
kwargs.update({rel_field.name: instance})
- using = instance._state.db or DEFAULT_DB_ALIAS
- return super(RelatedManager, self).using(using).get_or_create(**kwargs)
+ db = router.db_for_write(rel_model, instance=instance)
+ return super(RelatedManager, self).using(db).get_or_create(**kwargs)
get_or_create.alters_data = True
# remove() and clear() are only provided if the ForeignKey can have a value of null.
@@ -402,7 +414,7 @@ def remove(self, *objs):
# Is obj actually part of this descriptor set?
if getattr(obj, rel_field.attname) == val:
setattr(obj, rel_field.name, None)
- obj.save(using=instance._state.db)
+ obj.save()
else:
raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, instance))
remove.alters_data = True
@@ -410,7 +422,7 @@ def remove(self, *objs):
def clear(self):
for obj in self.all():
setattr(obj, rel_field.name, None)
- obj.save(using=instance._state.db)
+ obj.save()
clear.alters_data = True
manager = RelatedManager()
@@ -443,7 +455,8 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
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):
- 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,
# the add and remove methods do not exist.
@@ -478,14 +491,16 @@ def create(self, **kwargs):
if not rel.through._meta.auto_created:
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))
- 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)
return new_obj
create.alters_data = True
def get_or_create(self, **kwargs):
+ db = router.db_for_write(self.instance.__class__, instance=self.instance)
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
# from get() then the relationship already exists.
if created:
@@ -505,15 +520,16 @@ def _add_items(self, source_field_name, target_field_name, *objs):
new_ids = set()
for obj in objs:
if isinstance(obj, self.model):
-# if obj._state.db != self.instance._state.db:
-# raise ValueError('Cannot add "%r": instance is on database "%s", value is is on database "%s"' %
-# (obj, self.instance._state.db, obj._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"' %
+ (obj, self.instance._state.db, obj._state.db))
new_ids.add(obj.pk)
elif isinstance(obj, Model):
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
else:
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(**{
source_field_name: self._pk_val,
'%s__in' % target_field_name: new_ids,
@@ -521,7 +537,7 @@ def _add_items(self, source_field_name, target_field_name, *objs):
new_ids = new_ids - set(vals)
# Add the ones that aren't there already
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' % target_field_name: obj_id,
})
@@ -547,7 +563,8 @@ def _remove_items(self, source_field_name, target_field_name, *objs):
else:
old_ids.add(obj)
# 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,
'%s__in' % target_field_name: old_ids
}).delete()
@@ -566,7 +583,8 @@ def _clear_items(self, source_field_name):
signals.m2m_changed.send(sender=rel.through, action="clear",
instance=self.instance, reverse=self.reverse,
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
}).delete()
View
20 django/db/models/manager.py
@@ -1,10 +1,11 @@
from django.utils import copycompat as copy
-
-from django.db import DEFAULT_DB_ALIAS
+from django.conf import settings
+from django.db import router
from django.db.models.query import QuerySet, EmptyQuerySet, insert_query, RawQuerySet
from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
+
def ensure_default_manager(sender, **kwargs):
"""
Ensures that a Model subclass contains a default manager and sets the
@@ -87,30 +88,27 @@ def _copy_to_model(self, model):
mgr._inherited = True
return mgr
- def db_manager(self, alias):
+ def db_manager(self, using):
obj = copy.copy(self)
- obj._db = alias
+ obj._db = using
return obj
@property
def db(self):
- return self._db or DEFAULT_DB_ALIAS
+ return self._db or router.db_for_read(self.model)
#######################
# PROXIES TO QUERYSET #
#######################
def get_empty_query_set(self):
- return EmptyQuerySet(self.model)
+ return EmptyQuerySet(self.model, using=self._db)
def get_query_set(self):
"""Returns a new QuerySet object. Subclasses can override this method
to easily customize the behavior of the Manager.
"""
- qs = QuerySet(self.model)
- if self._db is not None:
- qs = qs.using(self._db)
- return qs
+ return QuerySet(self.model, using=self._db)
def none(self):
return self.get_empty_query_set()
@@ -200,7 +198,7 @@ def _update(self, values, **kwargs):
return self.get_query_set()._update(values, **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):
# This class ensures managers aren't accessible via model instances.
View
25 django/db/models/query.py
@@ -4,7 +4,7 @@
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.fields import DateField
from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery
@@ -34,6 +34,7 @@ def __init__(self, model=None, query=None, using=None):
self._result_cache = None
self._iter = None
self._sticky_filter = False
+ self._for_write = False
########################
# PYTHON MAGIC METHODS #
@@ -345,6 +346,7 @@ def create(self, **kwargs):
and returning the created object.
"""
obj = self.model(**kwargs)
+ self._for_write = True
obj.save(force_insert=True, using=self.db)
return obj
@@ -358,6 +360,7 @@ def get_or_create(self, **kwargs):
'get_or_create() must be passed at least one keyword argument'
defaults = kwargs.pop('defaults', {})
try:
+ self._for_write = True
return self.get(**kwargs), False
except self.model.DoesNotExist:
try:
@@ -413,6 +416,11 @@ def delete(self):
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.
del_query.query.select_related = False
del_query.query.clear_ordering()
@@ -442,6 +450,7 @@ def update(self, **kwargs):
"""
assert self.query.can_filter(), \
"Cannot update a query once a slice has been taken."
+ self._for_write = True
query = self.query.clone(sql.UpdateQuery)
query.add_update_values(kwargs)
if not transaction.is_managed(using=self.db):
@@ -714,7 +723,9 @@ def ordered(self):
@property
def db(self):
"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 #
@@ -726,8 +737,8 @@ def _clone(self, klass=None, setup=False, **kwargs):
query = self.query.clone()
if self._sticky_filter:
query.filter_is_sticky = True
- c = klass(model=self.model, query=query)
- c._db = self._db
+ c = klass(model=self.model, query=query, using=self._db)
+ c._for_write = self._for_write
c.__dict__.update(kwargs)
if setup and hasattr(c, '_setup_query'):
c._setup_query()
@@ -988,8 +999,8 @@ def _clone(self, klass=None, setup=False, **kwargs):
class EmptyQuerySet(QuerySet):
- def __init__(self, model=None, query=None):
- super(EmptyQuerySet, self).__init__(model, query)
+ def __init__(self, model=None, query=None, using=None):
+ super(EmptyQuerySet, self).__init__(model, query, using)
self._result_cache = []
def __and__(self, other):
@@ -1254,7 +1265,7 @@ def __repr__(self):
@property
def db(self):
"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):
"""
View
38 django/db/utils.py
@@ -5,6 +5,8 @@
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
+DEFAULT_DB_ALIAS = 'default'
+
def load_backend(backend_name):
try:
module = import_module('.base', 'django.db.backends.%s' % backend_name)
@@ -55,6 +57,7 @@ def ensure_defaults(self, alias):
conn = self.databases[alias]
except KeyError:
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
+
conn.setdefault('ENGINE', 'django.db.backends.dummy')
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
conn['ENGINE'] = 'django.db.backends.dummy'
@@ -82,3 +85,38 @@ def __iter__(self):
def all(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
View
16 docs/ref/settings.txt
@@ -372,6 +372,22 @@ test database will use the name ``'test_' + DATABASE_NAME``.
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
DATE_FORMAT
View
336 docs/topics/db/multi-db.txt
@@ -6,10 +6,10 @@ Multiple databases
.. versionadded:: 1.2
-This topic guide describes Django's support for interacting with multiple
-databases. Most of the rest of Django's documentation assumes you are
-interacting with a single database. If you want to interact with multiple
-databases, you'll need to take some additional steps.
+This topic guide describes Django's support for interacting with
+multiple databases. Most of the rest of Django's documentation assumes
+you are interacting with a single database. If you want to interact
+with multiple databases, you'll need to take some additional steps.
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`
documentation.
-Regardless of how many databases you have, you *must* have a database
-named ``'default'``. Any additional databases can have whatever alias
-you choose.
+Databases can have any alias you choose. However, the alias
+``default`` has special significance. Django uses the database with
+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
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
particular database. you can specify the :djadminopt:`--exclude`
-argument to :djadmin:`syncdb`. The :djadminopt:`--exclude` option
-lets you prevent a specific application or applications from
-being synchronized. For example, if you don't want the ``sales``
-application to be in the ``users`` database, you could run::
+argument to :djadmin:`syncdb`. The :djadminopt:`--exclude` option lets
+you prevent a specific application or applications from being
+synchronized. For example, if you don't want the ``sales`` application
+to be in the ``users`` database, you could run::
$ ./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
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``
-"chain." Just call ``using()`` on the ``QuerySet`` to get another ``QuerySet``
-that uses the specified database.
+Automatic database routing
+==========================
-``using()`` takes a single argument: the alias of the database on which you
-want to run the query. For example:
+The easiest way to use multiple databases is to set up a database
+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
+----------------
+
+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.
- # This will run on the 'default' database.
+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()
-
- # So will this.
+
+ >>> # So will this.
>>> Author.objects.using('default').all()
-
- # This will run on the 'other' database.
+
+ >>> # This will run on the 'other' database.
>>> Author.objects.using('other').all()
Selecting a database for ``save()``
-===================================
+-----------------------------------
-Use the ``using`` keyword to ``Model.save()`` to specify to which database the
-data should be saved.
+Use the ``using`` keyword to ``Model.save()`` to specify to which
+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')
-If you don't specify ``using``, the ``save()`` method will always save into the
-default database.
+If you don't specify ``using``, the ``save()`` method will save into
+the default database allocated by the routers.
Moving an object from one database to another
----------------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-If you've saved an instance to one database, it might be tempting to use
-``save(using=...)`` as a way to migrate the instance to a new database. However,
-if you don't take appropriate steps, this could have some unexpected consequences.
+If you've saved an instance to one database, it might be tempting to
+use ``save(using=...)`` as a way to migrate the instance to a new
+database. However, if you don't take appropriate steps, this could
+have some unexpected consequences.
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.
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
-a new object, avoiding any loss of data on the ``second`` database::
+of the instance. If an object has no primary key, Django will treat it
+as a new object, avoiding any loss of data on the ``second``
+database::
>>> p = Person(name='Fred')
>>> p.save(using='first')
>>> p.pk = None # Clear the primary key.
>>> p.save(using='second') # Write a completely new object.
-The second option is to use the ``force_insert`` option to ``save()`` to ensure
-that Django does a SQL ``INSERT``::
+The second option is to use the ``force_insert`` option to ``save()``
+to ensure that Django does a SQL ``INSERT``::
>>> p = Person(name='Fred')
>>> p.save(using='first')
@@ -170,51 +362,53 @@ when you try to save onto the ``second`` database, an error will be
raised.
Selecting a database to delete from
-===================================
+-----------------------------------
-By default, a call to delete an existing object will be executed on the
-same database that was used to retrieve the object in the first place::
+By default, a call to delete an existing object will be executed on
+the same database that was used to retrieve the object in the first
+place::
>>> u = User.objects.using('legacy_users').get(username='fred')
>>> u.delete() # will delete from the `legacy_users` database
To specify the database from which a model will be deleted, pass a
-``using`` keyword argument to the ``Model.delete()`` method. This argument
-works just like the ``using`` keyword argument to ``save()``.
+``using`` keyword argument to the ``Model.delete()`` method. This
+argument works just like the ``using`` keyword argument to ``save()``.
-For example, if you're migrating a user from the ``legacy_users`` database
-to the ``new_users`` database, you might use these commands::
+For example, if you're migrating a user from the ``legacy_users``
+database to the ``new_users`` database, you might use these commands::
>>> user_obj.save(using='new_users')
>>> user_obj.delete(using='legacy_users')
Using managers with multiple databases
-======================================
+--------------------------------------
-Use the ``db_manager()`` method on managers to give managers access to a
-non-default database.
+Use the ``db_manager()`` method on managers to give managers access to
+a non-default database.
-For example, say you have a custom manager method that touches the database --
-``User.objects.create_user()``. Because ``create_user()`` is a
-manager method, not a ``QuerySet`` method, you can't do
-``User.objects.using('new_users').create_user()``. (The ``create_user()`` method
-is only available on ``User.objects``, the manager, not on ``QuerySet`` objects
-derived from the manager.) The solution is to use ``db_manager()``, like this::
+For example, say you have a custom manager method that touches the
+database -- ``User.objects.create_user()``. Because ``create_user()``
+is a manager method, not a ``QuerySet`` method, you can't do
+``User.objects.using('new_users').create_user()``. (The
+``create_user()`` method is only available on ``User.objects``, the
+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(...)
``db_manager()`` returns a copy of the manager bound to the database you specify.
Using ``get_query_set()`` with multiple databases
--------------------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-If you're overriding ``get_query_set()`` on your manager, be sure to either
-call the method on the parent (using ``super()``) or do the appropriate
-handling of the ``_db`` attribute on the manager (a string containing the name
-of the database to use).
+If you're overriding ``get_query_set()`` on your manager, be sure to
+either call the method on the parent (using ``super()``) or do the
+appropriate handling of the ``_db`` attribute on the manager (a string
+containing the name of the database to use).
-For example, if you want to return a custom ``QuerySet`` class from the
-``get_query_set`` method, you could do this::
+For example, if you want to return a custom ``QuerySet`` class from
+the ``get_query_set`` method, you could do this::
class MyManager(models.Manager):
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
databases. If you want to provide an admin interface for a model on a
-database other than ``default``, you'll need to write custom
-:class:`~django.contrib.admin.ModelAdmin` classes that will direct the
-admin to use a specific database for content.
+database other than that that specified by your router chain, you'll
+need to write custom :class:`~django.contrib.admin.ModelAdmin` classes
+that will direct the admin to use a specific database for content.
``ModelAdmin`` objects have four methods that require customization for
multiple-database support::
@@ -257,11 +451,11 @@ multiple-database support::
# on the 'other' database.
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
-all objects of a given type are stored on a specific database (e.g.,
-all ``User`` objects are in the ``other`` database). If your usage of
-multiple databases is more complex, your ``ModelAdmin`` will need to reflect
-that strategy.
+The implementation provided here implements a multi-database strategy
+where all objects of a given type are stored on a specific database
+(e.g., all ``User`` objects are in the ``other`` database). If your
+usage of multiple databases is more complex, your ``ModelAdmin`` will
+need to reflect that strategy.
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.
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
-any ``Admin`` instance::
+Once you've written your model admin definitions, they can be
+registered with any ``Admin`` instance::
from django.contrib import admin
View
3  tests/regressiontests/multiple_database/models.py
@@ -2,7 +2,7 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
-from django.db import models, DEFAULT_DB_ALIAS
+from django.db import models
class Review(models.Model):
source = models.CharField(max_length=100)
@@ -36,6 +36,7 @@ class Book(models.Model):
authors = models.ManyToManyField(Person)
editor = models.ForeignKey(Person, null=True, related_name='edited')
reviews = generic.GenericRelation(Review)
+ pages = models.IntegerField(default=100)
def __unicode__(self):
return self.title
View
753 tests/regressiontests/multiple_database/tests.py
@@ -3,7 +3,8 @@
from django.conf import settings
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 models import Book, Person, Review, UserProfile
@@ -18,6 +19,16 @@
class QueryTestCase(TestCase):
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):
"Objects created on the default database don't leak onto other databases"
# Create a book on the default database using create()
@@ -259,53 +270,53 @@ def test_m2m_reverse_operations(self):
self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)),
[u'Mark Pilgrim'])
-# def test_m2m_cross_database_protection(self):
-# "Operations that involve sharing M2M objects across databases raise an error"
-# # Create a book and author on the default database
-# pro = Book.objects.create(title="Pro Django",
-# published=datetime.date(2008, 12, 16))
-
-# marty = Person.objects.create(name="Marty Alchin")
-
-# # Create a book and author on the other database
-# dive = Book.objects.using('other').create(title="Dive into Python",
-# published=datetime.date(2009, 5, 4))
-
-# mark = Person.objects.using('other').create(name="Mark Pilgrim")
-# # Set a foreign key set with an object from a different database
-# try:
-# marty.book_set = [pro, dive]
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Add to an m2m with an object from a different database
-# try:
-# marty.book_set.add(dive)
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Set a m2m with an object from a different database
-# try:
-# marty.book_set = [pro, dive]
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Add to a reverse m2m with an object from a different database
-# try:
-# dive.authors.add(marty)
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Set a reverse m2m with an object from a different database
-# try:
-# dive.authors = [mark, marty]
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
+ def test_m2m_cross_database_protection(self):
+ "Operations that involve sharing M2M objects across databases raise an error"
+ # Create a book and author on the default database
+ pro = Book.objects.create(title="Pro Django",
+ published=datetime.date(2008, 12, 16))
+
+ marty = Person.objects.create(name="Marty Alchin")
+
+ # Create a book and author on the other database
+ dive = Book.objects.using('other').create(title="Dive into Python",
+ published=datetime.date(2009, 5, 4))
+
+ mark = Person.objects.using('other').create(name="Mark Pilgrim")
+ # Set a foreign key set with an object from a different database
+ try:
+ marty.book_set = [pro, dive]
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Add to an m2m with an object from a different database
+ try:
+ marty.book_set.add(dive)
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Set a m2m with an object from a different database
+ try:
+ marty.book_set = [pro, dive]
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Add to a reverse m2m with an object from a different database
+ try:
+ dive.authors.add(marty)
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Set a reverse m2m with an object from a different database
+ try:
+ dive.authors = [mark, marty]
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
def test_foreign_key_separation(self):
"FK fields are constrained to a single database"
@@ -401,88 +412,88 @@ def test_foreign_key_reverse_operations(self):
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):
-# "Operations that involve sharing FK objects across databases raise an error"
-# # Create a book and author on the default database
-# pro = Book.objects.create(title="Pro Django",
-# published=datetime.date(2008, 12, 16))
-
-# marty = Person.objects.create(name="Marty Alchin")
-
-# # Create a book and author on the other database
-# dive = Book.objects.using('other').create(title="Dive into Python",
-# published=datetime.date(2009, 5, 4))
-
-# mark = Person.objects.using('other').create(name="Mark Pilgrim")
-
-# # Set a foreign key with an object from a different database
-# try:
-# dive.editor = marty
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Set a foreign key set with an object from a different database
-# try:
-# marty.edited = [pro, dive]
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # Add to a foreign key set with an object from a different database
-# try:
-# marty.edited.add(dive)
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # BUT! if you assign a FK object when the base object hasn't
-# # been saved yet, you implicitly assign the database for the
-# # base object.
-# 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 'other'...
-# dive.editor = chris
-# html5.editor = mark
-# # self.assertEquals(chris._state.db, 'other')
-# self.assertEquals(html5._state.db, 'other')
-# # ... but it isn't saved yet
-# self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
-# [u'Mark Pilgrim'])
-# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
-# [u'Dive into Python'])
-
-# # When saved (no using required), new objects goes to 'other'
-# chris.save()
-# html5.save()
-# self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
-# [u'Marty Alchin'])
-# self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
-# [u'Chris Mills', u'Mark Pilgrim'])
-# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
-# [u'Pro Django'])
-# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
-# [u'Dive into HTML5', u'Dive into Python'])
-
-# # 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, 'other')
-# # ... but it isn't saved yet
-# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
-# [u'Pro Django'])
-# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
-# [u'Dive into HTML5', u'Dive into Python'])
-
-# # When saved, the new book goes to 'other'
-# water.save()
-# self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
-# [u'Pro Django'])
-# self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
-# [u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
+ def test_foreign_key_cross_database_protection(self):
+ "Operations that involve sharing FK objects across databases raise an error"
+ # Create a book and author on the default database
+ pro = Book.objects.create(title="Pro Django",
+ published=datetime.date(2008, 12, 16))
+
+ marty = Person.objects.create(name="Marty Alchin")
+
+ # Create a book and author on the other database
+ dive = Book.objects.using('other').create(title="Dive into Python",
+ published=datetime.date(2009, 5, 4))
+
+ mark = Person.objects.using('other').create(name="Mark Pilgrim")
+
+ # Set a foreign key with an object from a different database
+ try:
+ dive.editor = marty
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Set a foreign key set with an object from a different database
+ try:
+ marty.edited = [pro, dive]
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Add to a foreign key set with an object from a different database
+ try:
+ marty.edited.add(dive)
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # BUT! if you assign a FK object when the base object hasn't
+ # been saved yet, you implicitly assign the database for the
+ # base object.
+ 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 'other'...
+ dive.editor = chris
+ html5.editor = mark
+ self.assertEquals(chris._state.db, 'other')
+ self.assertEquals(html5._state.db, 'other')
+ # ... but it isn't saved yet
+ self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
+ [u'Mark Pilgrim'])
+ self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
+ [u'Dive into Python'])
+
+ # When saved (no using required), new objects goes to 'other'
+ chris.save()
+ html5.save()
+ self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
+ [u'Marty Alchin'])
+ self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
+ [u'Chris Mills', u'Mark Pilgrim'])
+ self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
+ [u'Pro Django'])
+ self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
+ [u'Dive into HTML5', u'Dive into Python'])
+
+ # 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, 'other')
+ # ... but it isn't saved yet
+ self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
+ [u'Pro Django'])
+ self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
+ [u'Dive into HTML5', u'Dive into Python'])
+
+ # When saved, the new book goes to 'other'
+ water.save()
+ self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
+ [u'Pro Django'])
+ self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
+ [u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
def test_generic_key_separation(self):
"Generic fields are constrained to a single database"
@@ -555,56 +566,56 @@ def test_generic_key_reverse_operations(self):
self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
[u'Python Daily'])
-# def test_generic_key_cross_database_protection(self):
-## "Operations that involve sharing FK objects across databases raise an error"
-## # Create a book and author on the default database
-## pro = Book.objects.create(title="Pro Django",
-## published=datetime.date(2008, 12, 16))
-
-## review1 = Review.objects.create(source="Python Monthly", content_object=pro)
-
-## # Create a book and author on the other database
-## dive = Book.objects.using('other').create(title="Dive into Python",
-## published=datetime.date(2009, 5, 4))
-
-## review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
-
-## # Set a foreign key with an object from a different database
-## try:
-## review1.content_object = dive
-## self.fail("Shouldn't be able to assign across databases")
-## except ValueError:
-## pass
-
-# # Add to a foreign key set with an object from a different database
-# try:
-# dive.reviews.add(review1)
-# self.fail("Shouldn't be able to assign across databases")
-# except ValueError:
-# pass
-
-# # BUT! if you assign a FK object when the base object hasn't
-# # been saved yet, you implicitly assign the database for the
-# # base object.
-# review3 = Review(source="Python Daily")
-# # initially, no db assigned
-# self.assertEquals(review3._state.db, None)
-
-# # Dive comes from 'other', so review3 is set to use 'other'...
-# review3.content_object = dive
-# self.assertEquals(review3._state.db, 'other')
-# # ... but it isn't saved yet
-# self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
-# [u'Python Monthly'])
-# self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
-# [u'Python Weekly'])
-
-# # When saved, John goes to 'other'
-# review3.save()
-# self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
-# [u'Python Monthly'])
-# self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
-# [u'Python Daily', u'Python Weekly'])
+ def test_generic_key_cross_database_protection(self):
+ "Operations that involve sharing generic key objects across databases raise an error"
+ # Create a book and author on the default database
+ pro = Book.objects.create(title="Pro Django",
+ published=datetime.date(2008, 12, 16))
+
+ review1 = Review.objects.create(source="Python Monthly", content_object=pro)
+
+ # Create a book and author on the other database
+ dive = Book.objects.using('other').create(title="Dive into Python",
+ published=datetime.date(2009, 5, 4))
+
+ review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
+
+ # Set a foreign key with an object from a different database
+ try:
+ review1.content_object = dive
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # Add to a foreign key set with an object from a different database
+ try:
+ dive.reviews.add(review1)
+ self.fail("Shouldn't be able to assign across databases")
+ except ValueError:
+ pass
+
+ # BUT! if you assign a FK object when the base object hasn't
+ # been saved yet, you implicitly assign the database for the
+ # base object.
+ review3 = Review(source="Python Daily")
+ # initially, no db assigned
+ self.assertEquals(review3._state.db, None)
+
+ # Dive comes from 'other', so review3 is set to use 'other'...
+ review3.content_object = dive
+ self.assertEquals(review3._state.db, 'other')
+ # ... but it isn't saved yet
+ self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
+ [u'Python Monthly'])
+ self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
+ [u'Python Weekly'])
+
+ # When saved, John goes to 'other'
+ review3.save()
+ self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
+ [u'Python Monthly'])
+ self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
+ [u'Python Daily', u'Python Weekly'])
def test_ordering(self):
"get_next_by_XXX commands stick to a single database"
@@ -630,6 +641,388 @@ def test_raw(self):
val = Book.objects.raw('SELECT id FROM "multiple_database_book"').using('other')
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):
def setUp(self):
Please sign in to comment.
Something went wrong with that request. Please try again.