Fixed #12672 -- Added the ability to configure which applications are available on which database.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12290 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-01-25 12:23:30 +00:00
parent 6755a039eb
commit 14116bc53e
8 changed files with 107 additions and 67 deletions

View File

@ -8,15 +8,10 @@ from django.conf import settings
from django.core import serializers from django.core import serializers
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management.color import no_style from django.core.management.color import no_style
from django.db import connections, transaction, DEFAULT_DB_ALIAS from django.db import connections, router, transaction, DEFAULT_DB_ALIAS
from django.db.models import get_apps from django.db.models import get_apps
from django.utils.itercompat import product from django.utils.itercompat import product
try:
set
except NameError:
from sets import Set as set # Python 2.3 fallback
try: try:
import bz2 import bz2
has_bz2 = True has_bz2 = True
@ -31,13 +26,10 @@ class Command(BaseCommand):
make_option('--database', action='store', dest='database', make_option('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load '
'fixtures into. Defaults to the "default" database.'), 'fixtures into. Defaults to the "default" database.'),
make_option('-e', '--exclude', dest='exclude',action='append', default=[],
help='App to exclude (use multiple --exclude to exclude multiple apps).'),
) )
def handle(self, *fixture_labels, **options): def handle(self, *fixture_labels, **options):
using = options.get('database', DEFAULT_DB_ALIAS) using = options.get('database', DEFAULT_DB_ALIAS)
excluded_apps = options.get('exclude', [])
connection = connections[using] connection = connections[using]
self.style = no_style() self.style = no_style()
@ -171,7 +163,7 @@ class Command(BaseCommand):
try: try:
objects = serializers.deserialize(format, fixture, using=using) objects = serializers.deserialize(format, fixture, using=using)
for obj in objects: for obj in objects:
if obj.object._meta.app_label not in excluded_apps: if router.allow_syncdb(using, obj.object.__class__):
objects_in_fixture += 1 objects_in_fixture += 1
models.add(obj.object.__class__) models.add(obj.object.__class__)
obj.save(using=using) obj.save(using=using)

View File

@ -5,7 +5,7 @@ from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal
from django.db import connections, transaction, models, DEFAULT_DB_ALIAS from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
from django.utils.importlib import import_module from django.utils.importlib import import_module
@ -16,8 +16,6 @@ class Command(NoArgsCommand):
make_option('--database', action='store', dest='database', make_option('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. ' default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
'Defaults to the "default" database.'), 'Defaults to the "default" database.'),
make_option('-e', '--exclude', dest='exclude',action='append', default=[],
help='App to exclude (use multiple --exclude to exclude multiple apps).'),
) )
help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created." help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
@ -26,7 +24,6 @@ class Command(NoArgsCommand):
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
interactive = options.get('interactive') interactive = options.get('interactive')
show_traceback = options.get('traceback', False) show_traceback = options.get('traceback', False)
exclude = options.get('exclude', [])
self.style = no_style() self.style = no_style()
@ -59,13 +56,16 @@ class Command(NoArgsCommand):
created_models = set() created_models = set()
pending_references = {} pending_references = {}
excluded_apps = set(models.get_app(app_label) for app_label in exclude) # Build the manifest of apps and models that are to be synchronized
included_apps = set(app for app in models.get_apps() if app not in excluded_apps) manifest = dict(
(app.__name__.split('.')[-2],
[m for m in models.get_models(app, include_auto_created=True)
if router.allow_syncdb(db, m)])
for app in models.get_apps()
)
# Create the tables for each model # Create the tables for each model
for app in included_apps: for app_name, model_list in manifest.items():
app_name = app.__name__.split('.')[-2]
model_list = models.get_models(app, include_auto_created=True)
for model in model_list: for model in model_list:
# Create the model's database table, if it doesn't already exist. # Create the model's database table, if it doesn't already exist.
if verbosity >= 2: if verbosity >= 2:
@ -101,9 +101,8 @@ class Command(NoArgsCommand):
# Install custom SQL for the app (but only if this # Install custom SQL for the app (but only if this
# is a model we've just created) # is a model we've just created)
for app in included_apps: for app_name, model_list in manifest.items():
app_name = app.__name__.split('.')[-2] for model in model_list:
for model in models.get_models(app):
if model in created_models: if model in created_models:
custom_sql = custom_sql_for_model(model, self.style, connection) custom_sql = custom_sql_for_model(model, self.style, connection)
if custom_sql: if custom_sql:
@ -126,9 +125,8 @@ class Command(NoArgsCommand):
print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name) print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name)
# Install SQL indicies for all newly created models # Install SQL indicies for all newly created models
for app in included_apps: for app_name, model_list in manifest.items():
app_name = app.__name__.split('.')[-2] for model in model_list:
for model in models.get_models(app):
if model in created_models: if model in created_models:
index_sql = connection.creation.sql_indexes_for_model(model, self.style) index_sql = connection.creation.sql_indexes_for_model(model, self.style)
if index_sql: if index_sql:
@ -145,4 +143,4 @@ class Command(NoArgsCommand):
transaction.commit_unless_managed(using=db) transaction.commit_unless_managed(using=db)
from django.core.management import call_command from django.core.management import call_command
call_command('loaddata', 'initial_data', verbosity=verbosity, exclude=exclude, database=db) call_command('loaddata', 'initial_data', verbosity=verbosity, database=db)

View File

@ -121,3 +121,10 @@ class ConnectionRouter(object):
if allow is not None: if allow is not None:
return allow return allow
return obj1._state.db == obj2._state.db return obj1._state.db == obj2._state.db
def allow_syncdb(self, db, model):
for router in self.routers:
allow = router.allow_syncdb(db, model)
if allow is not None:
return allow
return True

View File

@ -284,17 +284,11 @@ class DjangoTestSuiteRunner(object):
Returns the number of tests that failed. Returns the number of tests that failed.
""" """
self.setup_test_environment() self.setup_test_environment()
suite = self.build_suite(test_labels, extra_tests) suite = self.build_suite(test_labels, extra_tests)
old_config = self.setup_databases() old_config = self.setup_databases()
result = self.run_suite(suite) result = self.run_suite(suite)
self.teardown_databases(old_config) self.teardown_databases(old_config)
self.teardown_test_environment() self.teardown_test_environment()
return self.suite_result(result) return self.suite_result(result)
def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None):

View File

@ -423,25 +423,6 @@ define the fixture ``mydata.master.json`` or
have specified that you want to load data onto the ``master`` have specified that you want to load data onto the ``master``
database. database.
Excluding applications from loading
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 1.2
The :djadminopt:`--exclude` option may be provided to prevent specific
applications from being loaded.
For example, if you wanted to exclude models from ``django.contrib.auth``
from being loaded into your database, you would call::
django-admin.py loaddata mydata.json --exclude auth
This will look for for a JSON fixture called ``mydata`` in all the
usual locations - including the ``fixtures`` directory of the
``django.contrib.auth`` application. However, any fixture object that
identifies itself as belonging to the ``auth`` application (e.g.,
instance of ``auth.User``) would be ignored by loaddata.
makemessages makemessages
------------ ------------

View File

@ -66,13 +66,9 @@ all databases in our example, you would need to call::
$ ./manage.py syncdb --database=users $ ./manage.py syncdb --database=users
If you don't want every application to be synchronized onto a If you don't want every application to be synchronized onto a
particular database. you can specify the :djadminopt:`--exclude` particular database, you can define a :ref:`database
argument to :djadmin:`syncdb`. The :djadminopt:`--exclude` option lets router<topics-db-multi-db-routing>` that implements a policy
you prevent a specific application or applications from being constraining the availability of particular models.
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
Alternatively, if you want fine-grained control of synchronization, Alternatively, if you want fine-grained control of synchronization,
you can pipe all or part of the output of :djadmin:`sqlall` for a you can pipe all or part of the output of :djadmin:`sqlall` for a
@ -103,7 +99,7 @@ routing scheme.
Database routers Database routers
---------------- ----------------
A database Router is a class that provides three methods: A database Router is a class that provides four methods:
.. method:: db_for_read(model, **hints) .. method:: db_for_read(model, **hints)
@ -137,6 +133,14 @@ A database Router is a class that provides three methods:
used by foreign key and many to many operations to determine if a used by foreign key and many to many operations to determine if a
relation should be allowed between two objects. relation should be allowed between two objects.
.. method:: allow_syncdb(db, model)
Determine if the ``model`` should be synchronized onto the
database with alias ``db``. Return True if the model should be
synchronized, False if it should not be synchronized, or None if
the router has no opinion. This method can be used to determine
the availability of a model on a given database.
.. _topics-db-multi-db-hints: .. _topics-db-multi-db-hints:
Hints Hints
@ -221,6 +225,13 @@ master/slave relationship between the databases 'master', 'slave1' and
return True return True
return None return None
def allow_syncdb(self, db, model):
"Make sure the auth app only appears on the 'credentials' db"
if db == 'credentials':
return model._meta.app_label == 'auth'
elif model._meta.app_label == 'auth':
return False
return None
class MasterSlaveRouter(object): class MasterSlaveRouter(object):
"""A router that sets up a simple master/slave configuration""" """A router that sets up a simple master/slave configuration"""
@ -240,11 +251,26 @@ master/slave relationship between the databases 'master', 'slave1' and
return True return True
return None return None
def allow_syncdb(self, db, model):
"Explicitly put all models on all databases."
return True
Then, in your settings file, add the following (substituting ``path.to.`` with Then, in your settings file, add the following (substituting ``path.to.`` with
the actual python path to the module where you define the routers):: the actual python path to the module where you define the routers)::
DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.MasterSlaveRouter'] DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.MasterSlaveRouter']
The order in which routers are processed is significant. Routers will
be queried in the order the are listed in the
:setting:`DATABASE_ROUTERS` setting . In this example, the
``AuthRouter`` is processed before the ``MasterSlaveRouter``, and as a
result, decisions concerning the models in ``auth`` are processed
before any other decision is made. If the :setting:`DATABASE_ROUTERS`
setting listed the two routers in the other order,
``MasterSlaveRouter.allow_syncdb()`` would be processed first. The
catch-all nature of the MasterSlaveRouter implementation would mean
that all models would be available on all databases.
With this setup installed, lets run some Django code:: With this setup installed, lets run some Django code::
>>> # This retrieval will be performed on the 'credentials' database >>> # This retrieval will be performed on the 'credentials' database
@ -270,6 +296,7 @@ With this setup installed, lets run some Django code::
>>> # ... but if we re-retrieve the object, it will come back on a slave >>> # ... but if we re-retrieve the object, it will come back on a slave
>>> mh = Book.objects.get(title='Mostly Harmless') >>> mh = Book.objects.get(title='Mostly Harmless')
Manually selecting a database Manually selecting a database
============================= =============================

View File

@ -289,14 +289,6 @@ Multiple fixtures named 'fixture5' in '...fixtures'. Aborting.
>>> management.call_command('flush', verbosity=0, interactive=False) >>> management.call_command('flush', verbosity=0, interactive=False)
# Try to load fixture 1, but this time, exclude the 'fixtures' app.
>>> management.call_command('loaddata', 'fixture1', verbosity=0, exclude='fixtures')
>>> Article.objects.all()
[<Article: Python program becomes self aware>]
>>> Category.objects.all()
[]
# Load back in fixture 1, we need the articles from it # Load back in fixture 1, we need the articles from it
>>> management.call_command('loaddata', 'fixture1', verbosity=0) >>> management.call_command('loaddata', 'fixture1', verbosity=0)

View File

@ -655,6 +655,25 @@ class TestRouter(object):
def allow_relation(self, obj1, obj2, **hints): def allow_relation(self, obj1, obj2, **hints):
return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other') return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other')
def allow_syncdb(self, db, model):
return True
class AuthRouter(object):
# Another test router. This one doesn't do anything interesting
# other than validate syncdb behavior
def db_for_read(self, model, **hints):
return None
def db_for_write(self, model, **hints):
return None
def allow_relation(self, obj1, obj2, **hints):
return None
def allow_syncdb(self, db, model):
if db == 'other':
return model._meta.app_label == 'auth'
elif model._meta.app_label == 'auth':
return False
return None
class RouterTestCase(TestCase): class RouterTestCase(TestCase):
multi_db = True multi_db = True
@ -677,6 +696,35 @@ class RouterTestCase(TestCase):
self.assertEquals(Book.objects.db_manager('default').db, 'default') self.assertEquals(Book.objects.db_manager('default').db, 'default')
self.assertEquals(Book.objects.db_manager('default').all().db, 'default') self.assertEquals(Book.objects.db_manager('default').all().db, 'default')
def test_syncdb_selection(self):
"Synchronization behaviour is predicatable"
self.assertTrue(router.allow_syncdb('default', User))
self.assertTrue(router.allow_syncdb('default', Book))
self.assertTrue(router.allow_syncdb('other', User))
self.assertTrue(router.allow_syncdb('other', Book))
# Add the auth router to the chain.
# TestRouter is a universal synchronizer, so it should have no effect.
router.routers = [TestRouter(), AuthRouter()]
self.assertTrue(router.allow_syncdb('default', User))
self.assertTrue(router.allow_syncdb('default', Book))
self.assertTrue(router.allow_syncdb('other', User))
self.assertTrue(router.allow_syncdb('other', Book))
# Now check what happens if the router order is the other way around
router.routers = [AuthRouter(), TestRouter()]
self.assertFalse(router.allow_syncdb('default', User))
self.assertTrue(router.allow_syncdb('default', Book))
self.assertTrue(router.allow_syncdb('other', User))
self.assertFalse(router.allow_syncdb('other', Book))
def test_database_routing(self): def test_database_routing(self):
marty = Person.objects.using('default').create(name="Marty Alchin") marty = Person.objects.using('default').create(name="Marty Alchin")
pro = Book.objects.using('default').create(title="Pro Django", pro = Book.objects.using('default').create(title="Pro Django",
@ -1046,6 +1094,7 @@ class UserProfileTestCase(TestCase):
self.assertEquals(alice.get_profile().flavor, 'chocolate') self.assertEquals(alice.get_profile().flavor, 'chocolate')
self.assertEquals(bob.get_profile().flavor, 'crunchy frog') self.assertEquals(bob.get_profile().flavor, 'crunchy frog')
class FixtureTestCase(TestCase): class FixtureTestCase(TestCase):
multi_db = True multi_db = True
fixtures = ['multidb-common', 'multidb'] fixtures = ['multidb-common', 'multidb']