Opened 22 months ago

Closed 22 months ago

Last modified 22 months ago

#33838 closed Bug (duplicate)

Querying issue with ForeignKeys where db_constraint=False

Reported by: Alex Bailey Owned by: nobody
Component: Database layer (models, ORM) Version: 4.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Alex Bailey)

models.py

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=25)

class Book(models.Model):
    foo = models.ForeignKey(Person, db_constraint=False, on_delete=models.DO_NOTHING, related_name='foo')
    bar = models.ForeignKey(Person, db_constraint=False, on_delete=models.DO_NOTHING, related_name='bar')

setup

from .models import Book, Person

alex = Person.objects.create(name='Alex')

Book.objects.create(foo=alex, bar=alex)
Book.objects.create(foo=alex, bar_id=999)
Book.objects.create(foo_id=999, bar=alex)

shell

>>> from django.db.models import Q

#Correct: Returns both books where foo__name == 'Alex'
>>> Book.objects.filter(foo__name='Alex') 
<QuerySet [<Book: Book object (1)>, <Book: Book object (2)>]>

#Correct: Returns both books where bar__name == 'Alex'
>>> Book.objects.filter(bar__name='Alex')
<QuerySet [<Book: Book object (1)>, <Book: Book object (3)>]>

#Only returns one book, expected to return all 3. 
>>> Book.objects.filter(Q(foo__name='Alex') | Q(bar__name='Alex'))
<QuerySet [<Book: Book object (1)>]>

I am working with an existing DB where it is possible for a Person to no longer exist, hence the lack of db_constraint in the ForeignKey.

Books can refer to non-existent Persons but it still seems like expected behavior would be for the queryset OR to return all 3 books. I believe this is coming from the Django ORM doing an INNER JOIN. The 3rd query should be a simple union of the first 2 queries.

All 3 books fit into the expected query of books WHERE foo__name = 'Alex' OR bar__name = 'Alex' but the Django behavior does not reflect this.

Change History (3)

comment:1 by Alex Bailey, 22 months ago

Description: modified (diff)

comment:2 by Alex Bailey, 22 months ago

I just noticed that adding null=True to the ForeignKeys produces the expected result on the last query:

class Book(models.Model):
    foo = models.ForeignKey(Person, db_constraint=False, on_delete=models.DO_NOTHING, related_name='foo', null=True)
    bar = models.ForeignKey(Person, db_constraint=False, on_delete=models.DO_NOTHING, related_name='bar', null=True)
>>> Book.objects.filter(Q(foo__name='Alex') | Q(bar__name='Alex'))
<QuerySet [<Book: Book object (1)>, <Book: Book object (2)>, <Book: Book object (3)>]>

Django switched to using LEFT OUTER JOIN in this case. Should this same logic apply to ForeignKeys where db_constraint=False?

comment:3 by Simon Charette, 22 months ago

Resolution: duplicate
Status: newclosed

Closing as duplicate of #33608.

While Django allows for database constraints to be omitted it doesn't make any assumptions with regards to database integrity implications.

In other words, if you break integrity expectations of ForeignKey you must also adjust your code to do so and in your particular case if a Book is still allowed to exist without a person the proper way to represent that is by setting null=True on your field as you've discovered.

Adapting the ORM to special case db_constraint=False like it does with null=True has a few implications that should be debated on the mailing list before a decision is taken because it was not done when the feature was added.

Last edited 22 months ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top