Kaydet (Commit) 1b3dc8ad authored tarafından Russell Keith-Magee's avatar Russell Keith-Magee

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

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
üst acc095c3
......@@ -128,6 +128,7 @@ SERVER_EMAIL = 'root@localhost'
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_HOST = '' # Set to empty string for localhost. Not used wit
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
......
......@@ -3,7 +3,7 @@ import urllib
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
......
......@@ -5,7 +5,7 @@ Classes allowing "generic" relations through ContentType and object-id fields.
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db.models import signals
from django.db import models, 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 create_generic_related_manager(superclass):
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
setattr(obj, self.content_type_field_name, self.content_type)
setattr(obj, self.object_id_field_name, self.pk_val)
obj.save(using=self.instance._state.db)
obj.save()
add.alters_data = True
def remove(self, *objs):
......
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
......
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
......
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 @@ for alias, database in settings.DATABASES.items():
connections = ConnectionHandler(settings.DATABASES)
router = ConnectionRouter(settings.DATABASE_ROUTERS)
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
# 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_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 @@ class Model(object):
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 @@ class Model(object):
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 @@ class Model(object):
# 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
......
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 @@ class Manager(object):
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 @@ class Manager(object):
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.
......
......@@ -4,7 +4,7 @@ The main QuerySet implementation. This provides the public API for the ORM.
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 @@ class QuerySet(object):
self._result_cache = None
self._iter = None
self._sticky_filter = False
self._for_write = False
########################
# PYTHON MAGIC METHODS #
......@@ -345,6 +346,7 @@ class QuerySet(object):
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 @@ class QuerySet(object):
'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 @@ class QuerySet(object):
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 @@ class QuerySet(object):
"""
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 @@ class QuerySet(object):
@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 @@ class QuerySet(object):
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 @@ class DateQuerySet(QuerySet):
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 @@ class RawQuerySet(object):
@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):
"""
......
......@@ -5,6 +5,8 @@ from django.conf import settings
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 @@ class ConnectionHandler(object):
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 @@ class ConnectionHandler(object):
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
......@@ -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
......
This diff is collapsed.
......@@ -2,7 +2,7 @@ from django.conf import settings
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment