#26162 closed Bug (fixed)
Data loss when ManyToManyField refers to the wrong table
Reported by: | Ian Foote | Owned by: | Simon Charette |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 1.8 |
Severity: | Release blocker | Keywords: | |
Cc: | Simon Charette | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
If I have two identically named models in two different apps, but these models each have a ManyToManyField
to the same model, then one of these ManyToManyField
s will use the other's database table.
App 1:
class Bar(models.Model): name = models.CharField(max_length=255) class Foo(models.Model): name = models.CharField(max_length=255) bars = models.ManyToManyField(Bar, related_name='+')
App 2:
from app1.models import Bar class Foo(models.Model): name = models.CharField(max_length=255) bars = models.ManyToManyField(Bar, related_name='+')
Tests:
from app1.models import Bar, Foo def test_add_bar_to_foo(): bar = Bar.objects.create() foo = Foo.objects.create() foo.bars.add(bar) assert bar in foo.bars.all()
I expect the test to pass, but instead it fails.
A working example of this code can be found at: https://github.com/Ian-Foote/django-bug-sandbox/pull/2
Change History (7)
comment:1 by , 9 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:2 by , 9 years ago
Cc: | added |
---|
I suspect this is caused by colliding related_query_name
s.
When you hide a related manager by setting related_name
to a value ending with '+'
the related_query_name
is still set to its default value (based on the model name).
If we explicit the model definitions created by Django here's what we have:
class Bar(models.Model): class Meta: app_label = 'app1' class Foo(models.Model): bars = models.ManyToManyField(Bar, related_name='+', through='app1.FooBar') class Meta: app_label = 'app1' class FooBar(models.Model): bar = models.ForeignKey( Bar, related_name='+', related_query_name='foobar' ) foo = models.ForeignKey( 'app1.Foo', related_name='+', related_query_name='foobar' ) class Meta: app_label = 'app1' class Foo(models.Model): bars = models.ManyToManyField(Bar, related_name='+', through='app2.FooBar') class Meta: app_label = 'app2' class FooBar(models.Model): bar = models.ForeignKey( Bar, related_name='+', related_query_name='foobar' ) foo = models.ForeignKey( 'app2.Foo', related_name='+', related_query_name='foobar' ) class Meta: app_label = 'app2'
As you can see there's a related_query_name='foobar'
conflict on Bar
that makes foo.bars.all()
(resolving to Bar.object.filter(foobar__foo=foo
) ambiguous. This clash is not detected by the check framework because the relationships are hidden.
In order to fix this I suggest we start by fixing the clash checks to run even if the relationship is hidden, we just have to skip E302
and E304
in this case.
Then we should consider making implicit intermediary model creation more smart about this possible conflict by namespacing related_query_name
by application label. This is a backward incompatible change in some way since users might have relied on the automatically generated queryname even if unlikely. The backward incompatibility could be lessened by only namespacing the queryname if the referenced model is from a different application.
comment:3 by , 9 years ago
Has patch: | set |
---|---|
Owner: | changed from | to
Status: | new → assigned |
Here's a PR with the checks adjustments.
comment:4 by , 9 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
Django 1.6 gave a validation error for this case:
but this ceased after 9f76ea1eaad0db0183fa3b5bade16392db0cafbd.
This might be related to #24505 and its duplicate, #25486.