Fixed #13227 -- Modified ForeignKeys to fully honor the db_prep/prep separation introduced by multidb. This was required to ensure that model instances aren't deepcopied as a result of being involved in a filter clause. Thanks to claudep for the report, and Alex Gaynor for the help on the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12865 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-03-27 15:16:27 +00:00
parent 5256a805ff
commit b31b2d4da3
3 changed files with 64 additions and 30 deletions

View File

@ -121,32 +121,24 @@ class RelatedField(object):
if not cls._meta.abstract: if not cls._meta.abstract:
self.contribute_to_related_class(other, self.related) self.contribute_to_related_class(other, self.related)
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): def get_prep_lookup(self, lookup_type, value):
# If we are doing a lookup on a Related Field, we must be if hasattr(value, 'prepare'):
# comparing object instances. The value should be the PK of value, return value.prepare()
# not value itself. if hasattr(value, '_prepare'):
def pk_trace(value): return value._prepare()
# Value may be a primary key, or an object held in a relation. # FIXME: lt and gt are explicitly allowed to make
# If it is an object, then we need to get the primary key value for # get_(next/prev)_by_date work; other lookups are not allowed since that
# that object. In certain conditions (especially one-to-one relations), # gets messy pretty quick. This is a good candidate for some refactoring
# the primary key may itself be an object - so we need to keep drilling # in the future.
# down until we hit a value that can be used for a comparison. if lookup_type in ['exact', 'gt', 'lt', 'gte', 'lte']:
v, field = value, None return self._pk_trace(value, 'get_prep_lookup', lookup_type)
try:
while True:
v, field = getattr(v, v._meta.pk.name), v._meta.pk
except AttributeError:
pass
if field:
if lookup_type in ('range', 'in'): if lookup_type in ('range', 'in'):
v = [v] return [self._pk_trace(v, 'get_prep_lookup', lookup_type) for v in value]
v = field.get_db_prep_lookup(lookup_type, v, elif lookup_type == 'isnull':
connection=connection, prepared=prepared) return []
if isinstance(v, list): raise TypeError("Related Field has invalid lookup: %s" % lookup_type)
v = v[0]
return v
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
if not prepared: if not prepared:
value = self.get_prep_lookup(lookup_type, value) value = self.get_prep_lookup(lookup_type, value)
if hasattr(value, 'get_compiler'): if hasattr(value, 'get_compiler'):
@ -162,18 +154,50 @@ class RelatedField(object):
sql, params = value._as_sql(connection=connection) sql, params = value._as_sql(connection=connection)
return QueryWrapper(('(%s)' % sql), params) return QueryWrapper(('(%s)' % sql), params)
# FIXME: lt and gt are explicitally allowed to make # FIXME: lt and gt are explicitly allowed to make
# get_(next/prev)_by_date work; other lookups are not allowed since that # get_(next/prev)_by_date work; other lookups are not allowed since that
# gets messy pretty quick. This is a good candidate for some refactoring # gets messy pretty quick. This is a good candidate for some refactoring
# in the future. # in the future.
if lookup_type in ['exact', 'gt', 'lt', 'gte', 'lte']: if lookup_type in ['exact', 'gt', 'lt', 'gte', 'lte']:
return [pk_trace(value)] return [self._pk_trace(value, 'get_db_prep_lookup', lookup_type,
connection=connection, prepared=prepared)]
if lookup_type in ('range', 'in'): if lookup_type in ('range', 'in'):
return [pk_trace(v) for v in value] return [self._pk_trace(v, 'get_db_prep_lookup', lookup_type,
connection=connection, prepared=prepared)
for v in value]
elif lookup_type == 'isnull': elif lookup_type == 'isnull':
return [] return []
raise TypeError("Related Field has invalid lookup: %s" % lookup_type) raise TypeError("Related Field has invalid lookup: %s" % lookup_type)
def _pk_trace(self, value, prep_func, lookup_type, **kwargs):
# Value may be a primary key, or an object held in a relation.
# If it is an object, then we need to get the primary key value for
# that object. In certain conditions (especially one-to-one relations),
# the primary key may itself be an object - so we need to keep drilling
# down until we hit a value that can be used for a comparison.
v = value
try:
while True:
v = getattr(v, v._meta.pk.name)
except AttributeError:
pass
except exceptions.ObjectDoesNotExist:
v = None
field = self
while field.rel:
if hasattr(field.rel, 'field_name'):
field = field.rel.to._meta.get_field(field.rel.field_name)
else:
field = field.rel.to._meta.pk
if lookup_type in ('range', 'in'):
v = [v]
v = getattr(field, prep_func)(lookup_type, v, **kwargs)
if isinstance(v, list):
v = v[0]
return v
def _get_related_query_name(self, opts): def _get_related_query_name(self, opts):
# This method defines the name that can be used to identify this # This method defines the name that can be used to identify this
# related object in a table-spanning query. It uses the lower-cased # related object in a table-spanning query. It uses the lower-cased

View File

@ -155,7 +155,8 @@ class Q(tree.Node):
def _combine(self, other, conn): def _combine(self, other, conn):
if not isinstance(other, Q): if not isinstance(other, Q):
raise TypeError(other) raise TypeError(other)
obj = deepcopy(self) obj = type(self)()
obj.add(self, conn)
obj.add(other, conn) obj.add(other, conn)
return obj return obj
@ -166,7 +167,8 @@ class Q(tree.Node):
return self._combine(other, self.AND) return self._combine(other, self.AND)
def __invert__(self): def __invert__(self):
obj = deepcopy(self) obj = type(self)()
obj.add(self, self.AND)
obj.negate() obj.negate()
return obj return obj

View File

@ -5,6 +5,7 @@ Various complex queries that have been problematic in the past.
import datetime import datetime
import pickle import pickle
import sys import sys
import threading
from django.conf import settings from django.conf import settings
from django.db import models, DEFAULT_DB_ALIAS from django.db import models, DEFAULT_DB_ALIAS
@ -45,6 +46,13 @@ class Note(models.Model):
def __unicode__(self): def __unicode__(self):
return self.note return self.note
def __init__(self, *args, **kwargs):
super(Note, self).__init__(*args, **kwargs)
# Regression for #13227 -- having an attribute that
# is unpickleable doesn't stop you from cloning queries
# that use objects of that type as an argument.
self.lock = threading.Lock()
class Annotation(models.Model): class Annotation(models.Model):
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
tag = models.ForeignKey(Tag) tag = models.ForeignKey(Tag)