Skip to content

Commit 848fbdc

Browse files
committed
Fixed #13432 -- Corrected the logic for router use on OneToOne fields; also added a bunch of tests for OneToOneField queries. Thanks to piquadrat for the report.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@13037 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 2ebf7fb commit 848fbdc

File tree

3 files changed

+138
-2
lines changed

3 files changed

+138
-2
lines changed

django/db/models/fields/related.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def __get__(self, instance, instance_type=None):
222222
return getattr(instance, self.cache_name)
223223
except AttributeError:
224224
params = {'%s__pk' % self.related.field.name: instance._get_pk_val()}
225-
db = router.db_for_read(instance.__class__, instance=instance)
225+
db = router.db_for_read(self.related.model, instance=instance)
226226
rel_obj = self.related.model._base_manager.using(db).get(**params)
227227
setattr(instance, self.cache_name, rel_obj)
228228
return rel_obj

tests/regressiontests/multiple_database/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ class Meta:
4545
ordering = ('title',)
4646

4747
class UserProfile(models.Model):
48-
user = models.OneToOneField(User)
48+
user = models.OneToOneField(User, null=True)
4949
flavor = models.CharField(max_length=100)
5050

51+
class Meta:
52+
ordering = ('flavor',)
53+

tests/regressiontests/multiple_database/tests.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,115 @@ def test_foreign_key_cross_database_protection(self):
498498
self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
499499
[u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
500500

501+
def test_o2o_separation(self):
502+
"OneToOne fields are constrained to a single database"
503+
# Create a user and profile on the default database
504+
alice = User.objects.db_manager('default').create_user('alice', '[email protected]')
505+
alice_profile = UserProfile.objects.using('default').create(user=alice, flavor='chocolate')
506+
507+
# Create a user and profile on the other database
508+
bob = User.objects.db_manager('other').create_user('bob', '[email protected]')
509+
bob_profile = UserProfile.objects.using('other').create(user=bob, flavor='crunchy frog')
510+
511+
# Retrieve related objects; queries should be database constrained
512+
alice = User.objects.using('default').get(username="alice")
513+
self.assertEquals(alice.userprofile.flavor, "chocolate")
514+
515+
bob = User.objects.using('other').get(username="bob")
516+
self.assertEquals(bob.userprofile.flavor, "crunchy frog")
517+
518+
# Check that queries work across joins
519+
self.assertEquals(list(User.objects.using('default').filter(userprofile__flavor='chocolate').values_list('username', flat=True)),
520+
[u'alice'])
521+
self.assertEquals(list(User.objects.using('other').filter(userprofile__flavor='chocolate').values_list('username', flat=True)),
522+
[])
523+
524+
self.assertEquals(list(User.objects.using('default').filter(userprofile__flavor='crunchy frog').values_list('username', flat=True)),
525+
[])
526+
self.assertEquals(list(User.objects.using('other').filter(userprofile__flavor='crunchy frog').values_list('username', flat=True)),
527+
[u'bob'])
528+
529+
# Reget the objects to clear caches
530+
alice_profile = UserProfile.objects.using('default').get(flavor='chocolate')
531+
bob_profile = UserProfile.objects.using('other').get(flavor='crunchy frog')
532+
533+
# Retrive related object by descriptor. Related objects should be database-baound
534+
self.assertEquals(alice_profile.user.username, 'alice')
535+
self.assertEquals(bob_profile.user.username, 'bob')
536+
537+
def test_o2o_cross_database_protection(self):
538+
"Operations that involve sharing FK objects across databases raise an error"
539+
# Create a user and profile on the default database
540+
alice = User.objects.db_manager('default').create_user('alice', '[email protected]')
541+
542+
# Create a user and profile on the other database
543+
bob = User.objects.db_manager('other').create_user('bob', '[email protected]')
544+
545+
# Set a one-to-one relation with an object from a different database
546+
alice_profile = UserProfile.objects.using('default').create(user=alice, flavor='chocolate')
547+
try:
548+
bob.userprofile = alice_profile
549+
self.fail("Shouldn't be able to assign across databases")
550+
except ValueError:
551+
pass
552+
553+
# BUT! if you assign a FK object when the base object hasn't
554+
# been saved yet, you implicitly assign the database for the
555+
# base object.
556+
bob_profile = UserProfile.objects.using('other').create(user=bob, flavor='crunchy frog')
557+
558+
new_bob_profile = UserProfile(flavor="spring surprise")
559+
560+
charlie = User(username='charlie',email='[email protected]')
561+
charlie.set_unusable_password()
562+
563+
# initially, no db assigned
564+
self.assertEquals(new_bob_profile._state.db, None)
565+
self.assertEquals(charlie._state.db, None)
566+
567+
# old object comes from 'other', so the new object is set to use 'other'...
568+
new_bob_profile.user = bob
569+
charlie.userprofile = bob_profile
570+
self.assertEquals(new_bob_profile._state.db, 'other')
571+
self.assertEquals(charlie._state.db, 'other')
572+
573+
# ... but it isn't saved yet
574+
self.assertEquals(list(User.objects.using('other').values_list('username',flat=True)),
575+
[u'bob'])
576+
self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)),
577+
[u'crunchy frog'])
578+
579+
# When saved (no using required), new objects goes to 'other'
580+
charlie.save()
581+
bob_profile.save()
582+
new_bob_profile.save()
583+
self.assertEquals(list(User.objects.using('default').values_list('username',flat=True)),
584+
[u'alice'])
585+
self.assertEquals(list(User.objects.using('other').values_list('username',flat=True)),
586+
[u'bob', u'charlie'])
587+
self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)),
588+
[u'chocolate'])
589+
self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)),
590+
[u'crunchy frog', u'spring surprise'])
591+
592+
# This also works if you assign the O2O relation in the constructor
593+
denise = User.objects.db_manager('other').create_user('denise','[email protected]')
594+
denise_profile = UserProfile(flavor="tofu", user=denise)
595+
596+
self.assertEquals(denise_profile._state.db, 'other')
597+
# ... but it isn't saved yet
598+
self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)),
599+
[u'chocolate'])
600+
self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)),
601+
[u'crunchy frog', u'spring surprise'])
602+
603+
# When saved, the new profile goes to 'other'
604+
denise_profile.save()
605+
self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)),
606+
[u'chocolate'])
607+
self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)),
608+
[u'crunchy frog', u'spring surprise', u'tofu'])
609+
501610
def test_generic_key_separation(self):
502611
"Generic fields are constrained to a single database"
503612
# Create a book and author on the default database
@@ -1103,6 +1212,30 @@ def test_m2m_cross_database_protection(self):
11031212
bob, created = dive.authors.get_or_create(name='Bob')
11041213
self.assertEquals(bob._state.db, 'default')
11051214

1215+
def test_o2o_cross_database_protection(self):
1216+
"Operations that involve sharing FK objects across databases raise an error"
1217+
# Create a user and profile on the default database
1218+
alice = User.objects.db_manager('default').create_user('alice', '[email protected]')
1219+
1220+
# Create a user and profile on the other database
1221+
bob = User.objects.db_manager('other').create_user('bob', '[email protected]')
1222+
1223+
# Set a one-to-one relation with an object from a different database
1224+
alice_profile = UserProfile.objects.create(user=alice, flavor='chocolate')
1225+
try:
1226+
bob.userprofile = alice_profile
1227+
except ValueError:
1228+
self.fail("Assignment across master/slave databases with a common source should be ok")
1229+
1230+
# Database assignments of original objects haven't changed...
1231+
self.assertEquals(alice._state.db, 'default')
1232+
self.assertEquals(alice_profile._state.db, 'default')
1233+
self.assertEquals(bob._state.db, 'other')
1234+
1235+
# ... but they will when the affected object is saved.
1236+
bob.save()
1237+
self.assertEquals(bob._state.db, 'default')
1238+
11061239
def test_generic_key_cross_database_protection(self):
11071240
"Generic Key operations can span databases if they share a source"
11081241
# Create a book and author on the default database

0 commit comments

Comments
 (0)