Skip to content

Commit

Permalink
[soc2009/multidb] Cleaned up the interaction between managers and the…
Browse files Browse the repository at this point in the history
… using() method by the addition of a db_manager() method.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11908 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
alex committed Dec 18, 2009
1 parent 601db0d commit e95bc7b
Show file tree
Hide file tree
Showing 13 changed files with 78 additions and 50 deletions.
2 changes: 0 additions & 2 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ Required for v1.2
* Modify the admin interface to support multiple databases (doh).
- Document how it is done

* Make sure we can't get rid of using= arguments everywhere

* Resolve uses of _default_manager
* django/contrib/databrowse/datastructures.py
* django/contrib/databrowse/fieldchoices.py
Expand Down
18 changes: 7 additions & 11 deletions django/contrib/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@
from django.utils.hashcompat import md5_constructor, sha_constructor
from django.utils.translation import ugettext_lazy as _

UNUSABLE_PASSWORD = '!' # This will never be a valid hash

try:
set
except NameError:
from sets import Set as set # Python 2.3 fallback
UNUSABLE_PASSWORD = '!' # This will never be a valid hash

def get_hexdigest(algorithm, salt, raw_password):
"""
Expand Down Expand Up @@ -48,8 +44,8 @@ class SiteProfileNotAvailable(Exception):
pass

class PermissionManager(models.Manager):
def get_by_natural_key(self, codename, app_label, model, using=None):
return self.using(using).get(
def get_by_natural_key(self, codename, app_label, model):
return self.get(
codename=codename,
content_type=ContentType.objects.get_by_natural_key(app_label, model)
)
Expand Down Expand Up @@ -106,23 +102,23 @@ def __unicode__(self):
return self.name

class UserManager(models.Manager):
def create_user(self, username, email, password=None, using=None):
def create_user(self, username, email, password=None):
"Creates and saves a User with the given username, e-mail and password."
now = datetime.datetime.now()
user = self.model(None, username, '', '', email.strip().lower(), 'placeholder', False, True, False, now, now)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save(using=using)
user.save(using=self.db)
return user

def create_superuser(self, username, email, password, using=None):
def create_superuser(self, username, email, password):
u = self.create_user(username, email, password)
u.is_staff = True
u.is_active = True
u.is_superuser = True
u.save(using=using)
u.save(using=self.db)
return u

def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
Expand Down
6 changes: 3 additions & 3 deletions django/contrib/contenttypes/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ def get_content_type(self, obj=None, id=None, using=None):
# using this model
ContentType = get_model("contenttypes", "contenttype")
if obj:
return ContentType.objects.get_for_model(obj, using=obj._state.db)
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
elif id:
return ContentType.objects.get_for_id(id, using=using)
return ContentType.objects.db_manager(using).get_for_id(id)
else:
# This should never happen. I love comments like this, don't you?
raise Exception("Impossible arguments to GFK.get_content_type!")
Expand Down Expand Up @@ -201,7 +201,7 @@ def __get__(self, instance, instance_type=None):
join_table = qn(self.field.m2m_db_table()),
source_col_name = qn(self.field.m2m_column_name()),
target_col_name = qn(self.field.m2m_reverse_name()),
content_type = ContentType.objects.get_for_model(instance, using=instance._state.db),
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance),
content_type_field_name = self.field.content_type_field_name,
object_id_field_name = self.field.object_id_field_name
)
Expand Down
26 changes: 11 additions & 15 deletions django/contrib/contenttypes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,51 @@ class ContentTypeManager(models.Manager):
# This cache is shared by all the get_for_* methods.
_cache = {}

def get_by_natural_key(self, app_label, model, using=None):
db = using or DEFAULT_DB_ALIAS
def get_by_natural_key(self, app_label, model):
try:
ct = self.__class__._cache[db][(app_label, model)]
ct = self.__class__._cache[self.db][(app_label, model)]
except KeyError:
ct = self.using(db).get(app_label=app_label, model=model)
ct = self.get(app_label=app_label, model=model)
return ct

def get_for_model(self, model, using=None):
def get_for_model(self, model):
"""
Returns the ContentType object for a given model, creating the
ContentType if necessary. Lookups are cached so that subsequent lookups
for the same model don't hit the database.
"""
db = using or DEFAULT_DB_ALIAS
opts = model._meta
while opts.proxy:
model = opts.proxy_for_model
opts = model._meta
key = (opts.app_label, opts.object_name.lower())
try:
ct = self.__class__._cache[db][key]
ct = self.__class__._cache[self.db][key]
except KeyError:
# Load or create the ContentType entry. The smart_unicode() is
# needed around opts.verbose_name_raw because name_raw might be a
# django.utils.functional.__proxy__ object.
ct, created = self.using(db).get_or_create(
ct, created = self.get_or_create(
app_label = opts.app_label,
model = opts.object_name.lower(),
defaults = {'name': smart_unicode(opts.verbose_name_raw)},
)
self._add_to_cache(db, ct)
self._add_to_cache(self.db, ct)

return ct

def get_for_id(self, id, using=None):
def get_for_id(self, id):
"""
Lookup a ContentType by ID. Uses the same shared cache as get_for_model
(though ContentTypes are obviously not created on-the-fly by get_by_id).
"""
db = using or DEFAULT_DB_ALIAS
try:
ct = self.__class__._cache[db][id]

ct = self.__class__._cache[self.db][id]
except KeyError:
# This could raise a DoesNotExist; that's correct behavior and will
# make sure that only correct ctypes get stored in the cache dict.
ct = self.using(db).get(pk=id)
self._add_to_cache(db, ct)
ct = self.get(pk=id)
self._add_to_cache(self.db, ct)
return ct

def clear_cache(self):
Expand Down
4 changes: 2 additions & 2 deletions django/core/serializers/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def Deserializer(object_list, **options):
if hasattr(field.rel.to._default_manager, 'get_by_natural_key'):
def m2m_convert(value):
if hasattr(value, '__iter__'):
return field.rel.to._default_manager.get_by_natural_key(*value, **{'using':db}).pk
return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk
else:
return smart_unicode(field.rel.to._meta.pk.to_python(value))
else:
Expand All @@ -109,7 +109,7 @@ def m2m_convert(value):
if field_value is not None:
if hasattr(field.rel.to._default_manager, 'get_by_natural_key'):
if hasattr(field_value, '__iter__'):
obj = field.rel.to._default_manager.get_by_natural_key(*field_value, **{'using':db})
obj = field.rel.to._default_manager.db_manager(db).get_by_natural_key(*field_value)
value = getattr(obj, field.rel.field_name)
else:
value = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value)
Expand Down
6 changes: 1 addition & 5 deletions django/core/serializers/pyyaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@
"""

from StringIO import StringIO
import decimal
import yaml

try:
import decimal
except ImportError:
from django.utils import _decimal as decimal # Python 2.3 fallback

from django.db import models
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.python import Deserializer as PythonDeserializer
Expand Down
4 changes: 2 additions & 2 deletions django/core/serializers/xml_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def _handle_fk_field_node(self, node, field):
if keys:
# If there are 'natural' subelements, it must be a natural key
field_value = [getInnerText(k).strip() for k in keys]
obj = field.rel.to._default_manager.get_by_natural_key(*field_value, **{'using':self.db})
obj = field.rel.to._default_manager.db_manager(self.db).get_by_natural_key(*field_value)
obj_pk = getattr(obj, field.rel.field_name)
else:
# Otherwise, treat like a normal PK
Expand All @@ -240,7 +240,7 @@ def m2m_convert(n):
if keys:
# If there are 'natural' subelements, it must be a natural key
field_value = [getInnerText(k).strip() for k in keys]
obj_pk = field.rel.to._default_manager.get_by_natural_key(*field_value, **{'using':self.db}).pk
obj_pk = field.rel.to._default_manager.db_manager(self.db).get_by_natural_key(*field_value).pk
else:
# Otherwise, treat like a normal PK value.
obj_pk = field.rel.to._meta.pk.to_python(n.getAttribute('pk'))
Expand Down
21 changes: 18 additions & 3 deletions django/db/models/manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import django.utils.copycompat as copy
from django.utils import copycompat as copy

from django.db import DEFAULT_DB_ALIAS
from django.db.models.query import QuerySet, EmptyQuerySet, insert_query
from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
Expand Down Expand Up @@ -49,6 +51,7 @@ def __init__(self):
self._set_creation_counter()
self.model = None
self._inherited = False
self._db = None

def contribute_to_class(self, model, name):
# TODO: Use weakref because of possible memory leak / circular reference.
Expand Down Expand Up @@ -84,6 +87,15 @@ def _copy_to_model(self, model):
mgr._inherited = True
return mgr

def db_manager(self, alias):
obj = copy.copy(self)
obj._db = alias
return obj

@property
def db(self):
return self._db or DEFAULT_DB_ALIAS

#######################
# PROXIES TO QUERYSET #
#######################
Expand All @@ -95,7 +107,10 @@ def get_query_set(self):
"""Returns a new QuerySet object. Subclasses can override this method
to easily customize the behavior of the Manager.
"""
return QuerySet(self.model)
qs = QuerySet(self.model)
if self._db is not None:
qs = qs.using(self._db)
return qs

def none(self):
return self.get_empty_query_set()
Expand Down Expand Up @@ -174,7 +189,7 @@ def only(self, *args, **kwargs):

def using(self, *args, **kwargs):
return self.get_query_set().using(*args, **kwargs)

def exists(self, *args, **kwargs):
return self.get_query_set().exists(*args, **kwargs)

Expand Down
27 changes: 27 additions & 0 deletions docs/topics/db/multi-db.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,30 @@ the ``'legacy_users'`` database to the ``'new_users'`` database you might do::

>>> user_obj.save(using='new_users')
>>> user_obj.delete(using='legacy_users')


Using ``Managers`` with Multiple Databases
==========================================

When you call ``using()`` Django returns a ``QuerySet`` that will be evaluated
against that database. However, sometimes you want to chain ``using()``
together with a cusotm manager method that doesn't return a ``QuerySet``,
such as the ``get_by_natural_key`` method. To solve this issue you can use the
``db_manager()`` method on a manager. This method returns a copy of the
*manager* bound to that specific database. This let's you do things like::

>>> Book.objects.db("other").get_by_natural_key(...)

If you are overiding ``get_query_set()`` on your manager you must be sure to
either, a) call the method on the parent (using ``super()``), or b) do the
appropriate handling of the ``_db`` attribute on the manager. For example if
you wanted 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):
qs = CustomQuerySet(self.model)
if self._db is not None:
qs = qs.using(self._db)
return qs
4 changes: 2 additions & 2 deletions tests/modeltests/fixtures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def __unicode__(self):
self.tagged, self.name)

class PersonManager(models.Manager):
def get_by_natural_key(self, name, using=None):
return self.using(using).get(name=name)
def get_by_natural_key(self, name):
return self.get(name=name)

class Person(models.Model):
objects = PersonManager()
Expand Down
4 changes: 2 additions & 2 deletions tests/regressiontests/fixtures_regress/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ class Meta:
# Check for forward references in FKs and M2Ms with natural keys

class TestManager(models.Manager):
def get_by_natural_key(self, key, using=None):
return self.using(using).get(name=key)
def get_by_natural_key(self, key):
return self.get(name=key)

class Store(models.Model):
objects = TestManager()
Expand Down
4 changes: 2 additions & 2 deletions tests/regressiontests/multiple_database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class Meta:
ordering = ('source',)

class PersonManager(models.Manager):
def get_by_natural_key(self, name, using=None):
return self.using(using).get(name=name)
def get_by_natural_key(self, name):
return self.get(name=name)

class Person(models.Model):
objects = PersonManager()
Expand Down
2 changes: 1 addition & 1 deletion tests/regressiontests/multiple_database/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def tearDown(self):
def test_user_profiles(self):

alice = User.objects.create_user('alice', 'alice@example.com')
bob = User.objects.create_user('bob', 'bob@example.com', using='other')
bob = User.objects.db_manager('other').create_user('bob', 'bob@example.com')

alice_profile = UserProfile(user=alice, flavor='chocolate')
alice_profile.save()
Expand Down

0 comments on commit e95bc7b

Please sign in to comment.