mirror of https://github.com/django/django.git
Fixed #16039 -- Made post_syncdb handlers multi-db aware.
Also reverted 8fb7a90026
. Refs #17055.
This commit is contained in:
parent
ea6b95dbec
commit
a026e480da
|
@ -10,6 +10,7 @@ import unicodedata
|
||||||
from django.contrib.auth import models as auth_app, get_user_model
|
from django.contrib.auth import models as auth_app, get_user_model
|
||||||
from django.core import exceptions
|
from django.core import exceptions
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
|
from django.db import DEFAULT_DB_ALIAS, router
|
||||||
from django.db.models import get_models, signals
|
from django.db.models import get_models, signals
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.six.moves import input
|
from django.utils.six.moves import input
|
||||||
|
@ -57,7 +58,10 @@ def _check_permission_clashing(custom, builtin, ctype):
|
||||||
(codename, ctype.app_label, ctype.model_class().__name__))
|
(codename, ctype.app_label, ctype.model_class().__name__))
|
||||||
pool.add(codename)
|
pool.add(codename)
|
||||||
|
|
||||||
def create_permissions(app, created_models, verbosity, **kwargs):
|
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
|
||||||
|
if not router.allow_syncdb(db, auth_app.Permission):
|
||||||
|
return
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
app_models = get_models(app)
|
app_models = get_models(app)
|
||||||
|
@ -68,7 +72,9 @@ def create_permissions(app, created_models, verbosity, **kwargs):
|
||||||
# The codenames and ctypes that should exist.
|
# The codenames and ctypes that should exist.
|
||||||
ctypes = set()
|
ctypes = set()
|
||||||
for klass in app_models:
|
for klass in app_models:
|
||||||
ctype = ContentType.objects.get_for_model(klass)
|
# Force looking up the content types in the current database
|
||||||
|
# before creating foreign keys to them.
|
||||||
|
ctype = ContentType.objects.db_manager(db).get_for_model(klass)
|
||||||
ctypes.add(ctype)
|
ctypes.add(ctype)
|
||||||
for perm in _get_all_permissions(klass._meta, ctype):
|
for perm in _get_all_permissions(klass._meta, ctype):
|
||||||
searched_perms.append((ctype, perm))
|
searched_perms.append((ctype, perm))
|
||||||
|
@ -76,21 +82,21 @@ def create_permissions(app, created_models, verbosity, **kwargs):
|
||||||
# Find all the Permissions that have a context_type for a model we're
|
# Find all the Permissions that have a context_type for a model we're
|
||||||
# looking for. We don't need to check for codenames since we already have
|
# looking for. We don't need to check for codenames since we already have
|
||||||
# a list of the ones we're going to create.
|
# a list of the ones we're going to create.
|
||||||
all_perms = set(auth_app.Permission.objects.filter(
|
all_perms = set(auth_app.Permission.objects.using(db).filter(
|
||||||
content_type__in=ctypes,
|
content_type__in=ctypes,
|
||||||
).values_list(
|
).values_list(
|
||||||
"content_type", "codename"
|
"content_type", "codename"
|
||||||
))
|
))
|
||||||
|
|
||||||
objs = [
|
perms = [
|
||||||
auth_app.Permission(codename=codename, name=name, content_type=ctype)
|
auth_app.Permission(codename=codename, name=name, content_type=ctype)
|
||||||
for ctype, (codename, name) in searched_perms
|
for ctype, (codename, name) in searched_perms
|
||||||
if (ctype.pk, codename) not in all_perms
|
if (ctype.pk, codename) not in all_perms
|
||||||
]
|
]
|
||||||
auth_app.Permission.objects.bulk_create(objs)
|
auth_app.Permission.objects.using(db).bulk_create(perms)
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
for obj in objs:
|
for perm in perms:
|
||||||
print("Adding permission '%s'" % obj)
|
print("Adding permission '%s'" % perm)
|
||||||
|
|
||||||
|
|
||||||
def create_superuser(app, created_models, verbosity, db, **kwargs):
|
def create_superuser(app, created_models, verbosity, db, **kwargs):
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import DEFAULT_DB_ALIAS, router
|
||||||
from django.db.models import get_apps, get_models, signals
|
from django.db.models import get_apps, get_models, signals
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.six.moves import input
|
from django.utils.six.moves import input
|
||||||
|
|
||||||
def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates content types for models in the given app, removing any model
|
Creates content types for models in the given app, removing any model
|
||||||
entries that no longer have a matching model class.
|
entries that no longer have a matching model class.
|
||||||
"""
|
"""
|
||||||
|
if not router.allow_syncdb(db, ContentType):
|
||||||
|
return
|
||||||
|
|
||||||
ContentType.objects.clear_cache()
|
ContentType.objects.clear_cache()
|
||||||
app_models = get_models(app)
|
app_models = get_models(app)
|
||||||
if not app_models:
|
if not app_models:
|
||||||
|
@ -19,10 +23,11 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
||||||
(model._meta.object_name.lower(), model)
|
(model._meta.object_name.lower(), model)
|
||||||
for model in app_models
|
for model in app_models
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all the content types
|
# Get all the content types
|
||||||
content_types = dict(
|
content_types = dict(
|
||||||
(ct.model, ct)
|
(ct.model, ct)
|
||||||
for ct in ContentType.objects.filter(app_label=app_label)
|
for ct in ContentType.objects.using(db).filter(app_label=app_label)
|
||||||
)
|
)
|
||||||
to_remove = [
|
to_remove = [
|
||||||
ct
|
ct
|
||||||
|
@ -30,7 +35,7 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
||||||
if model_name not in app_models
|
if model_name not in app_models
|
||||||
]
|
]
|
||||||
|
|
||||||
cts = ContentType.objects.bulk_create([
|
cts = [
|
||||||
ContentType(
|
ContentType(
|
||||||
name=smart_text(model._meta.verbose_name_raw),
|
name=smart_text(model._meta.verbose_name_raw),
|
||||||
app_label=app_label,
|
app_label=app_label,
|
||||||
|
@ -38,7 +43,8 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
||||||
)
|
)
|
||||||
for (model_name, model) in six.iteritems(app_models)
|
for (model_name, model) in six.iteritems(app_models)
|
||||||
if model_name not in content_types
|
if model_name not in content_types
|
||||||
])
|
]
|
||||||
|
ContentType.objects.using(db).bulk_create(cts)
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
for ct in cts:
|
for ct in cts:
|
||||||
print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
|
print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
|
||||||
|
|
|
@ -551,6 +551,20 @@ with the :meth:`~django.forms.Form.is_valid()` method and not with the
|
||||||
presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute
|
presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute
|
||||||
on the form.
|
on the form.
|
||||||
|
|
||||||
|
Behavior of :djadmin:`syncdb` with multiple databases
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:djadmin:`syncdb` now queries the database routers to determine if content
|
||||||
|
types (when :mod:`~django.contrib.contenttypes` is enabled) and permissions
|
||||||
|
(when :mod:`~django.contrib.auth` is enabled) should be created in the target
|
||||||
|
database. Previously, it created them in the default database, even when
|
||||||
|
another database was specified with the :djadminopt:`--database` option.
|
||||||
|
|
||||||
|
If you use :djadmin:`syncdb` on multiple databases, you should ensure that
|
||||||
|
your routers allow synchronizing content types and permissions to only one of
|
||||||
|
them. See the docs on the :ref:`behavior of contrib apps with multiple
|
||||||
|
databases <contrib_app_multiple_databases>` for more information.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -630,3 +630,49 @@ However, if you're using SQLite or MySQL with MyISAM tables, there is
|
||||||
no enforced referential integrity; as a result, you may be able to
|
no enforced referential integrity; as a result, you may be able to
|
||||||
'fake' cross database foreign keys. However, this configuration is not
|
'fake' cross database foreign keys. However, this configuration is not
|
||||||
officially supported by Django.
|
officially supported by Django.
|
||||||
|
|
||||||
|
.. _contrib_app_multiple_databases:
|
||||||
|
|
||||||
|
Behavior of contrib apps
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Several contrib apps include models, and some apps depend on others. Since
|
||||||
|
cross-database relationships are impossible, this creates some restrictions on
|
||||||
|
how you can split these models across databases:
|
||||||
|
|
||||||
|
- each one of ``contenttypes.ContentType``, ``sessions.Session`` and
|
||||||
|
``sites.Site`` can be stored in any database, given a suitable router.
|
||||||
|
- ``auth`` models — ``User``, ``Group`` and ``Permission`` — are linked
|
||||||
|
together and linked to ``ContentType``, so they must be stored in the same
|
||||||
|
database as ``ContentType``.
|
||||||
|
- ``admin`` and ``comments`` depend on ``auth``, so their models must be in
|
||||||
|
the same database as ``auth``.
|
||||||
|
- ``flatpages`` and ``redirects`` depend on ``sites``, so their models must be
|
||||||
|
in the same database as ``sites``.
|
||||||
|
|
||||||
|
In addition, some objects are automatically created just after
|
||||||
|
:djadmin:`syncdb` creates a table to hold them in a database:
|
||||||
|
|
||||||
|
- a default ``Site``,
|
||||||
|
- a ``ContentType`` for each model (including those not stored in that
|
||||||
|
database),
|
||||||
|
- three ``Permission`` for each model (including those not stored in that
|
||||||
|
database).
|
||||||
|
|
||||||
|
.. versionchanged:: 1.5
|
||||||
|
Previously, ``ContentType`` and ``Permission`` instances were created only
|
||||||
|
in the default database.
|
||||||
|
|
||||||
|
For common setups with multiple databases, it isn't useful to have these
|
||||||
|
objects in more than one database. Common setups include master / slave and
|
||||||
|
connecting to external databases. Therefore, it's recommended:
|
||||||
|
|
||||||
|
- either to run :djadmin:`syncdb` only for the default database;
|
||||||
|
- or to write :ref:`database router<topics-db-multi-db-routing>` that allows
|
||||||
|
synchronizing these three models only to one database.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If you're synchronizing content types to more that one database, be aware
|
||||||
|
that their primary keys may not match across databases. This may result in
|
||||||
|
data corruption or data loss.
|
||||||
|
|
|
@ -16,16 +16,6 @@ from django.utils.six import StringIO
|
||||||
from .models import Book, Person, Pet, Review, UserProfile
|
from .models import Book, Person, Pet, Review, UserProfile
|
||||||
|
|
||||||
|
|
||||||
def copy_content_types_from_default_to_other():
|
|
||||||
# On post_syncdb, content types are created in the 'default' database.
|
|
||||||
# However, tests of generic foreign keys require them in 'other' too.
|
|
||||||
# The problem is masked on backends that defer constraints checks: at the
|
|
||||||
# end of each test, there's a rollback, and constraints are never checked.
|
|
||||||
# It only appears on MySQL + InnoDB.
|
|
||||||
for ct in ContentType.objects.using('default').all():
|
|
||||||
ct.save(using='other')
|
|
||||||
|
|
||||||
|
|
||||||
class QueryTestCase(TestCase):
|
class QueryTestCase(TestCase):
|
||||||
multi_db = True
|
multi_db = True
|
||||||
|
|
||||||
|
@ -705,8 +695,6 @@ class QueryTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_separation(self):
|
def test_generic_key_separation(self):
|
||||||
"Generic fields are constrained to a single database"
|
"Generic fields are constrained to a single database"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
# Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
pro = Book.objects.create(title="Pro Django",
|
pro = Book.objects.create(title="Pro Django",
|
||||||
published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
@ -734,8 +722,6 @@ class QueryTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_reverse_operations(self):
|
def test_generic_key_reverse_operations(self):
|
||||||
"Generic reverse manipulations are all constrained to a single DB"
|
"Generic reverse manipulations are all constrained to a single DB"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
dive = Book.objects.using('other').create(title="Dive into Python",
|
dive = Book.objects.using('other').create(title="Dive into Python",
|
||||||
published=datetime.date(2009, 5, 4))
|
published=datetime.date(2009, 5, 4))
|
||||||
|
|
||||||
|
@ -780,8 +766,6 @@ class QueryTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_cross_database_protection(self):
|
def test_generic_key_cross_database_protection(self):
|
||||||
"Operations that involve sharing generic key objects across databases raise an error"
|
"Operations that involve sharing generic key objects across databases raise an error"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
# Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
pro = Book.objects.create(title="Pro Django",
|
pro = Book.objects.create(title="Pro Django",
|
||||||
published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
@ -833,8 +817,6 @@ class QueryTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_deletion(self):
|
def test_generic_key_deletion(self):
|
||||||
"Cascaded deletions of Generic Key relations issue queries on the right database"
|
"Cascaded deletions of Generic Key relations issue queries on the right database"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
dive = Book.objects.using('other').create(title="Dive into Python",
|
dive = Book.objects.using('other').create(title="Dive into Python",
|
||||||
published=datetime.date(2009, 5, 4))
|
published=datetime.date(2009, 5, 4))
|
||||||
review = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
|
review = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
|
||||||
|
@ -1402,8 +1384,6 @@ class RouterTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_cross_database_protection(self):
|
def test_generic_key_cross_database_protection(self):
|
||||||
"Generic Key operations can span databases if they share a source"
|
"Generic Key operations can span databases if they share a source"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
# Create a book and author on the default database
|
# Create a book and author on the default database
|
||||||
pro = Book.objects.using('default'
|
pro = Book.objects.using('default'
|
||||||
).create(title="Pro Django", published=datetime.date(2008, 12, 16))
|
).create(title="Pro Django", published=datetime.date(2008, 12, 16))
|
||||||
|
@ -1515,8 +1495,6 @@ class RouterTestCase(TestCase):
|
||||||
|
|
||||||
def test_generic_key_managers(self):
|
def test_generic_key_managers(self):
|
||||||
"Generic key relations are represented by managers, and can be controlled like managers"
|
"Generic key relations are represented by managers, and can be controlled like managers"
|
||||||
copy_content_types_from_default_to_other()
|
|
||||||
|
|
||||||
pro = Book.objects.using('other').create(title="Pro Django",
|
pro = Book.objects.using('other').create(title="Pro Django",
|
||||||
published=datetime.date(2008, 12, 16))
|
published=datetime.date(2008, 12, 16))
|
||||||
|
|
||||||
|
@ -1922,3 +1900,36 @@ class RouterModelArgumentTestCase(TestCase):
|
||||||
pet = Pet.objects.create(owner=person, name='Wart')
|
pet = Pet.objects.create(owner=person, name='Wart')
|
||||||
# test related FK collection
|
# test related FK collection
|
||||||
person.delete()
|
person.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class SyncOnlyDefaultDatabaseRouter(object):
|
||||||
|
def allow_syncdb(self, db, model):
|
||||||
|
return db == DEFAULT_DB_ALIAS
|
||||||
|
|
||||||
|
|
||||||
|
class SyncDBTestCase(TestCase):
|
||||||
|
multi_db = True
|
||||||
|
|
||||||
|
def test_syncdb_to_other_database(self):
|
||||||
|
"""Regression test for #16039: syncdb with --database option."""
|
||||||
|
count = ContentType.objects.count()
|
||||||
|
self.assertGreater(count, 0)
|
||||||
|
|
||||||
|
ContentType.objects.using('other').delete()
|
||||||
|
management.call_command('syncdb', verbosity=0, interactive=False,
|
||||||
|
load_initial_data=False, database='other')
|
||||||
|
|
||||||
|
self.assertEqual(ContentType.objects.using("other").count(), count)
|
||||||
|
|
||||||
|
def test_syncdb_to_other_database_with_router(self):
|
||||||
|
"""Regression test for #16039: syncdb with --database option."""
|
||||||
|
ContentType.objects.using('other').delete()
|
||||||
|
try:
|
||||||
|
old_routers = router.routers
|
||||||
|
router.routers = [SyncOnlyDefaultDatabaseRouter()]
|
||||||
|
management.call_command('syncdb', verbosity=0, interactive=False,
|
||||||
|
load_initial_data=False, database='other')
|
||||||
|
finally:
|
||||||
|
router.routers = old_routers
|
||||||
|
|
||||||
|
self.assertEqual(ContentType.objects.using("other").count(), 0)
|
||||||
|
|
Loading…
Reference in New Issue