Permalink
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...
1 parent acc095c commit 1b3dc8ad9a28486542f766ff93318aa6b4f5999b @freakboy3742 freakboy3742 committed Jan 22, 2010
@@ -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
@@ -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
@@ -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):
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,15 +414,15 @@ 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
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,23 +520,24 @@ 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,
})
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()
@@ -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.
Oops, something went wrong.

0 comments on commit 1b3dc8a

Please sign in to comment.