From 0ca0ed0453665aeb6bba892a19d7760a57774915 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 3 Dec 2009 06:25:45 +0000 Subject: [PATCH] [soc2009/multidb] Cleaned up the double processing required by validate() by splitting get_db_prep_* functions into db-specific and non-db-specific parts. Patch from Russell Keith-Magee. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11786 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- TODO | 4 - django/db/models/fields/__init__.py | 164 +++++++++++-------------- django/db/models/fields/files.py | 7 +- django/db/models/fields/related.py | 8 +- django/db/models/fields/subclassing.py | 47 +++++-- django/db/models/query.py | 14 +-- django/db/models/related.py | 5 +- django/db/models/sql/compiler.py | 2 +- django/db/models/sql/expressions.py | 3 + django/db/models/sql/query.py | 3 + django/db/models/sql/where.py | 19 +-- docs/howto/custom-model-fields.txt | 86 +++++++++---- docs/internals/deprecation.txt | 5 + docs/releases/1.2.txt | 68 ++++++++++ 14 files changed, 279 insertions(+), 156 deletions(-) diff --git a/TODO b/TODO index 82fdfcd9f4..4ddbeb6a50 100644 --- a/TODO +++ b/TODO @@ -7,10 +7,6 @@ Required for v1.2 * Finalize the sql.Query internals * Clean up the use of db.backend.query_class() * Verify it still works with GeoDjango - * Cleanup of new API entry points - * validate() on a field - * name/purpose clash with Honza? - * any overlap with existing methods? Optional for v1.2 ~~~~~~~~~~~~~~~~~ diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 8acd04d19c..97af08612f 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -180,21 +180,56 @@ class Field(object): "Returns field's value just before saving." return getattr(model_instance, self.attname) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): + "Perform preliminary non-db specific value checks and conversions." + return value + + def get_db_prep_value(self, value, connection, prepared=False): """Returns field's value prepared for interacting with the database backend. Used by the default implementations of ``get_db_prep_save``and `get_db_prep_lookup``` """ + if not prepared: + value = self.get_prep_value(value) return value def get_db_prep_save(self, value, connection): "Returns field's value prepared for saving into a database." - return self.get_db_prep_value(value, connection=connection) + return self.get_db_prep_value(value, connection=connection, prepared=False) - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_prep_lookup(self, lookup_type, value): + "Perform preliminary non-db specific lookup checks and conversions" + if hasattr(value, 'prepare'): + return value.prepare() + if hasattr(value, '_prepare'): + return value._prepare() + + if lookup_type in ( + 'regex', 'iregex', 'month', 'day', 'week_day', 'search', + 'contains', 'icontains', 'iexact', 'startswith', 'istartswith', + 'endswith', 'iendswith', 'isnull' + ): + return value + elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'): + return self.get_prep_value(value) + elif lookup_type in ('range', 'in'): + return [self.get_prep_value(v) for v in value] + elif lookup_type == 'year': + try: + return int(value) + except ValueError: + raise ValueError("The __year lookup type requires an integer argument") + + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." + if not prepared: + value = self.get_prep_lookup(lookup_type, value) + if hasattr(value, 'get_compiler'): + value = value.get_compiler(connection=connection) if hasattr(value, 'as_sql') or hasattr(value, '_as_sql'): # If the value has a relabel_aliases method, it will need to # be invoked before the final SQL is evaluated @@ -206,13 +241,12 @@ class Field(object): sql, params = value._as_sql(connection=connection) return QueryWrapper(('(%s)' % sql), params) - if lookup_type in ('regex', 'iregex', 'month', 'day', 'week_day', 'search'): return [value] elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'): - return [self.get_db_prep_value(value, connection=connection)] + return [self.get_db_prep_value(value, connection=connection, prepared=prepared)] elif lookup_type in ('range', 'in'): - return [self.get_db_prep_value(v, connection=connection) for v in value] + return [self.get_db_prep_value(v, connection=connection, prepared=prepared) for v in value] elif lookup_type in ('contains', 'icontains'): return ["%%%s%%" % connection.ops.prep_for_like_query(value)] elif lookup_type == 'iexact': @@ -224,36 +258,11 @@ class Field(object): elif lookup_type == 'isnull': return [] elif lookup_type == 'year': - try: - value = int(value) - except ValueError: - raise ValueError("The __year lookup type requires an integer argument") - if self.get_internal_type() == 'DateField': return connection.ops.year_lookup_bounds_for_date_field(value) else: return connection.ops.year_lookup_bounds(value) - raise TypeError("Field has invalid lookup: %s" % lookup_type) - - def validate(self, lookup_type, value): - """ - Validate that the data is valid, as much so as possible without knowing - what connection we are using. Returns True if the value was - successfully validated and false if the value wasn't validated (this - doesn't consider whether the value was actually valid, an exception is - raised in those circumstances). - """ - if hasattr(value, 'validate') or hasattr(value, '_validate'): - if hasattr(value, 'validate'): - value.validate() - else: - value._validate() - return True - if lookup_type == 'isnull': - return True - return False - def has_default(self): "Returns a boolean of whether this field has a default value." return self.default is not NOT_PROVIDED @@ -376,22 +385,11 @@ class AutoField(Field): raise exceptions.ValidationError( _("This value must be an integer.")) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): if value is None: return None return int(value) - def validate(self, lookup_type, value): - if super(AutoField, self).validate(lookup_type, value): - return - if value is None or hasattr(value, 'as_sql'): - return - if lookup_type in ('range', 'in'): - for val in value: - int(val) - else: - int(value) - def contribute_to_class(self, cls, name): assert not cls._meta.has_auto_field, "A model can't have more than one AutoField." super(AutoField, self).contribute_to_class(cls, name) @@ -419,24 +417,16 @@ class BooleanField(Field): raise exceptions.ValidationError( _("This value must be either True or False.")) - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_prep_lookup(self, lookup_type, value): # Special-case handling for filters coming from a web request (e.g. the # admin interface). Only works for scalar values (not lists). If you're # passing in a list, you might as well make things the right type when # constructing the list. if value in ('1', '0'): value = bool(int(value)) - return super(BooleanField, self).get_db_prep_lookup(lookup_type, value, - connection=connection) + return super(BooleanField, self).get_prep_lookup(lookup_type, value) - def validate(self, lookup_type, value): - if super(BooleanField, self).validate(lookup_type, value): - return - if value in ('1', '0'): - value = int(value) - bool(value) - - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): if value is None: return None return bool(value) @@ -539,31 +529,21 @@ class DateField(Field): setattr(cls, 'get_previous_by_%s' % self.name, curry(cls._get_next_or_previous_by_FIELD, field=self, is_next=False)) - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_prep_lookup(self, lookup_type, value): # For "__month", "__day", and "__week_day" lookups, convert the value # to an int so the database backend always sees a consistent type. if lookup_type in ('month', 'day', 'week_day'): - return [int(value)] - return super(DateField, self).get_db_prep_lookup(lookup_type, value, - connection=connection) + return int(value) + return super(DateField, self).get_prep_lookup(lookup_type, value) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): + return self.to_python(value) + + def get_db_prep_value(self, value, connection, prepared=False): # Casts dates into the format expected by the backend - return connection.ops.value_to_db_date(self.to_python(value)) - - def validate(self, lookup_type, value): - if super(DateField, self).validate(lookup_type, value): - return - if value is None: - return - if lookup_type in ('month', 'day', 'year', 'week_day'): - int(value) - return - if lookup_type in ('in', 'range'): - for val in value: - self.to_python(val) - return - self.to_python(value) + if not prepared: + value = self.get_prep_value(value) + return connection.ops.value_to_db_date(value) def value_to_string(self, obj): val = self._get_val_from_obj(obj) @@ -619,9 +599,14 @@ class DateTimeField(DateField): raise exceptions.ValidationError( _('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.')) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): + return self.to_python(value) + + def get_db_prep_value(self, value, connection, prepared=False): # Casts dates into the format expected by the backend - return connection.ops.value_to_db_datetime(self.to_python(value)) + if not prepared: + value = self.get_prep_value(value) + return connection.ops.value_to_db_datetime(value) def value_to_string(self, obj): val = self._get_val_from_obj(obj) @@ -679,7 +664,7 @@ class DecimalField(Field): return connection.ops.value_to_db_decimal(self.to_python(value), self.max_digits, self.decimal_places) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): return self.to_python(value) def formfield(self, **kwargs): @@ -723,7 +708,7 @@ class FilePathField(Field): class FloatField(Field): empty_strings_allowed = False - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): if value is None: return None return float(value) @@ -747,7 +732,7 @@ class FloatField(Field): class IntegerField(Field): empty_strings_allowed = False - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): if value is None: return None return int(value) @@ -800,22 +785,16 @@ class NullBooleanField(Field): raise exceptions.ValidationError( _("This value must be either None, True or False.")) - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_prep_lookup(self, lookup_type, value): # Special-case handling for filters coming from a web request (e.g. the # admin interface). Only works for scalar values (not lists). If you're # passing in a list, you might as well make things the right type when # constructing the list. if value in ('1', '0'): value = bool(int(value)) - return super(NullBooleanField, self).get_db_prep_lookup(lookup_type, - value, connection=connection) + return super(NullBooleanField, self).get_prep_lookup(lookup_type, value) - def validate(self, lookup_type, value): - if value in ('1', '0'): - value = int(value) - bool(value) - - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): if value is None: return None return bool(value) @@ -931,9 +910,14 @@ class TimeField(Field): else: return super(TimeField, self).pre_save(model_instance, add) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): + return self.to_python(value) + + def get_db_prep_value(self, value, connection, prepared=False): # Casts times into the format expected by the backend - return connection.ops.value_to_db_time(self.to_python(value)) + if not prepared: + value = self.get_prep_value(value) + return connection.ops.value_to_db_time(value) def value_to_string(self, obj): val = self._get_val_from_obj(obj) diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index f112bbd593..2eb22271cb 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -232,13 +232,12 @@ class FileField(Field): def get_internal_type(self): return "FileField" - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_prep_lookup(self, lookup_type, value): if hasattr(value, 'name'): value = value.name - return super(FileField, self).get_db_prep_lookup(lookup_type, value, - connection=connection) + return super(FileField, self).get_prep_lookup(lookup_type, value) - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): "Returns field's value prepared for saving into a database." # Need to convert File objects provided via a form to unicode for database insertion if value is None: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5249c951cb..f8ae5b1f4e 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -120,7 +120,7 @@ class RelatedField(object): if not cls._meta.abstract: self.contribute_to_related_class(other, self.related) - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): # If we are doing a lookup on a Related Field, we must be # comparing object instances. The value should be the PK of value, # not value itself. @@ -140,14 +140,16 @@ class RelatedField(object): if field: if lookup_type in ('range', 'in'): v = [v] - v = field.get_db_prep_lookup(lookup_type, v, connection=connection) + v = field.get_db_prep_lookup(lookup_type, v, + connection=connection, prepared=prepared) if isinstance(v, list): v = v[0] return v + if not prepared: + value = self.get_prep_lookup(lookup_type, value) if hasattr(value, 'get_compiler'): value = value.get_compiler(connection=connection) - if hasattr(value, 'as_sql') or hasattr(value, '_as_sql'): # If the value has a relabel_aliases method, it will need to # be invoked before the final SQL is evaluated diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py index 5a27325cde..bd11675ad3 100644 --- a/django/db/models/fields/subclassing.py +++ b/django/db/models/fields/subclassing.py @@ -11,25 +11,53 @@ from warnings import warn def call_with_connection(func): arg_names, varargs, varkwargs, defaults = getargspec(func) - takes_connection = 'connection' in arg_names or varkwargs - if not takes_connection: - warn("A Field class whose %s method doesn't take connection has been " - "defined. Please add a connection argument" % func.__name__, + updated = ('connection' in arg_names or varkwargs) + if not updated: + warn("A Field class whose %s method hasn't been updated to take a " + "`connection` argument." % func.__name__, PendingDeprecationWarning, stacklevel=2) + def inner(*args, **kwargs): if 'connection' not in kwargs: from django.db import connection kwargs['connection'] = connection - warn("%s has been called without providing a connection argument. " - "Please provide one" % func.__name__, PendingDeprecationWarning, + warn("%s has been called without providing a connection argument. " % + func.__name__, PendingDeprecationWarning, stacklevel=1) - if takes_connection: + if updated: return func(*args, **kwargs) if 'connection' in kwargs: del kwargs['connection'] return func(*args, **kwargs) return inner +def call_with_connection_and_prepared(func): + arg_names, varargs, varkwargs, defaults = getargspec(func) + updated = ( + ('connection' in arg_names or varkwargs) and + ('prepared' in arg_names or varkwargs) + ) + if not updated: + warn("A Field class whose %s method hasn't been updated to take " + "`connection` and `prepared` arguments." % func.__name__, + PendingDeprecationWarning, stacklevel=2) + + def inner(*args, **kwargs): + if 'connection' not in kwargs: + from django.db import connection + kwargs['connection'] = connection + warn("%s has been called without providing a connection argument. " % + func.__name__, PendingDeprecationWarning, + stacklevel=1) + if updated: + return func(*args, **kwargs) + if 'connection' in kwargs: + del kwargs['connection'] + if 'prepared' in kwargs: + del kwargs['prepared'] + return func(*args, **kwargs) + return inner + class LegacyConnection(type): """ A metaclass to normalize arguments give to the get_db_prep_* and db_type @@ -37,9 +65,10 @@ class LegacyConnection(type): """ def __new__(cls, names, bases, attrs): new_cls = super(LegacyConnection, cls).__new__(cls, names, bases, attrs) - for attr in ('db_type', 'get_db_prep_save', 'get_db_prep_lookup', - 'get_db_prep_value'): + for attr in ('db_type', 'get_db_prep_save'): setattr(new_cls, attr, call_with_connection(getattr(new_cls, attr))) + for attr in ('get_db_prep_lookup', 'get_db_prep_value'): + setattr(new_cls, attr, call_with_connection_and_prepared(getattr(new_cls, attr))) return new_cls class SubfieldBase(LegacyConnection): diff --git a/django/db/models/query.py b/django/db/models/query.py index f1035837ef..747f5c118c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -739,6 +739,9 @@ class QuerySet(object): self.query.add_fields(field_names, False) self.query.set_group_by() + def _prepare(self): + return self + def _as_sql(self, connection): """ Returns the internal query's SQL and parameters (as a tuple). @@ -748,13 +751,6 @@ class QuerySet(object): return obj.query.get_compiler(connection=connection).as_nested_sql() raise ValueError("Can't do subqueries with queries on different DBs.") - def _validate(self): - """ - A normal QuerySet is always valid when used as the RHS of a filter, - since it automatically gets filtered down to 1 field. - """ - pass - # When used as part of a nested query, a queryset will never be an "always # empty" result. value_annotation = True @@ -877,7 +873,7 @@ class ValuesQuerySet(QuerySet): return obj.query.get_compiler(connection=connection).as_nested_sql() raise ValueError("Can't do subqueries with queries on different DBs.") - def _validate(self): + def _prepare(self): """ Validates that we aren't trying to do a query like value__in=qs.values('value1', 'value2'), which isn't valid. @@ -886,7 +882,7 @@ class ValuesQuerySet(QuerySet): (not self._fields and len(self.model._meta.fields) > 1)): raise TypeError('Cannot use a multi-field %s as a filter value.' % self.__class__.__name__) - + return self class ValuesListQuerySet(ValuesQuerySet): def iterator(self): diff --git a/django/db/models/related.py b/django/db/models/related.py index 9df143550e..afdf3f7b61 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -18,9 +18,10 @@ class RelatedObject(object): self.name = '%s:%s' % (self.opts.app_label, self.opts.module_name) self.var_name = self.opts.object_name.lower() - def get_db_prep_lookup(self, lookup_type, value, connection): + def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): # Defer to the actual field definition for db prep - return self.field.get_db_prep_lookup(lookup_type, value) + return self.field.get_db_prep_lookup(lookup_type, value, + connection=connection, prepared=prepared) def editable_fields(self): "Get the fields in this class that should be edited inline." diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 21ae779000..e0a4208fb4 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -837,7 +837,7 @@ class SQLUpdateCompiler(SQLCompiler): self.query.related_ids = idents else: # The fast path. Filters and updates in one query. - self.query.add_filter(('pk__in', query.get_compiler(self.using))) + self.query.add_filter(('pk__in', query)) for alias in self.query.tables[1:]: self.query.alias_refcount[alias] = 0 diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index 3309710c6c..9bbc16ec8a 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -11,6 +11,9 @@ class SQLEvaluator(object): self.contains_aggregate = False self.expression.prepare(self, query, allow_joins) + def prepare(self): + return self + def as_sql(self, qn, connection): return self.expression.evaluate(self, qn, connection) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index b36193197e..d1217bf048 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -141,6 +141,9 @@ class Query(object): self.__dict__.update(obj_dict) + def prepare(self): + return self + def get_compiler(self, using=None, connection=None): if using is None and connection is None: raise ValueError("Need either using or connection") diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 6db819d915..4aa2351f17 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -62,8 +62,8 @@ class WhereNode(tree.Node): else: annotation = bool(value) - if hasattr(obj, "process"): - obj.validate(lookup_type, value) + if hasattr(obj, "prepare"): + value = obj.prepare(lookup_type, value) super(WhereNode, self).add((obj, lookup_type, annotation, value), connector) return @@ -143,7 +143,7 @@ class WhereNode(tree.Node): raise EmptyResultSet else: params = Field().get_db_prep_lookup(lookup_type, params_or_value, - connection=connection) + connection=connection, prepared=True) if isinstance(lvalue, tuple): # A direct database column lookup. field_sql = self.sql_for_columns(lvalue, qn, connection) @@ -262,6 +262,11 @@ class Constraint(object): def __init__(self, alias, col, field): self.alias, self.col, self.field = alias, col, field + def prepare(self, lookup_type, value): + if self.field: + return self.field.get_prep_lookup(lookup_type, value) + return value + def process(self, lookup_type, value, connection): """ Returns a tuple of data suitable for inclusion in a WhereNode @@ -272,14 +277,14 @@ class Constraint(object): try: if self.field: params = self.field.get_db_prep_lookup(lookup_type, value, - connection=connection) + connection=connection, prepared=True) db_type = self.field.db_type(connection=connection) else: # This branch is used at times when we add a comparison to NULL # (we don't really want to waste time looking up the associated # field object at the calling location). params = Field().get_db_prep_lookup(lookup_type, value, - connection=connection) + connection=connection, prepared=True) db_type = None except ObjectDoesNotExist: raise EmptyShortCircuit @@ -289,7 +294,3 @@ class Constraint(object): def relabel_aliases(self, change_map): if self.alias in change_map: self.alias = change_map[self.alias] - - def validate(self, lookup_type, value): - if hasattr(self.field, 'validate'): - self.field.validate(lookup_type, value) diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 801c9ceeef..e1f04fe664 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -396,36 +396,58 @@ Python object type we want to store in the model's attribute. called when it is created, you should be using `The SubfieldBase metaclass`_ mentioned earlier. Otherwise :meth:`to_python` won't be called automatically. -Converting Python objects to database values -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Converting Python objects to query values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: get_db_prep_value(self, value, connection) +.. method:: get_prep_value(self, value) -This is the reverse of :meth:`to_python` when working with the database backends -(as opposed to serialization). The ``value`` parameter is the current value of -the model's attribute (a field has no reference to its containing model, so it -cannot retrieve the value itself), and the method should return data in a format -that can be used as a parameter in a query for the database backend. The -specific connection that will be used for the query is passed as the -``connection`` parameter, this allows you to generate the value in a backend -specific mannner if necessary. +This is the reverse of :meth:`to_python` when working with the +database backends (as opposed to serialization). The ``value`` +parameter is the current value of the model's attribute (a field has +no reference to its containing model, so it cannot retrieve the value +itself), and the method should return data in a format that has been +prepared for use as a parameter in a query. + +This conversion should *not* include any database-specific +conversions. If database-specific conversions are required, they +should be made in the call to :meth:`get_db_prep_value`. For example:: class HandField(models.Field): # ... - def get_db_prep_value(self, value, connection): + def get_prep_value(self, value): return ''.join([''.join(l) for l in (value.north, value.east, value.south, value.west)]) +Converting query values to database values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. method:: get_db_prep_value(self, value, connection, prepared=False) + +Some data types (for example, dates) need to be in a specific format +before they can be used by a database backend. +:meth:`get_db_prep_value` is the method where those conversions should +be made. The specific connection that will be used for the query is +passed as the ``connection`` parameter. This allows you to use +backend-specific conversion logic if it is required. + +The ``prepared`` argument describes whether or not the value has +already been passed through :meth:`get_prep_value` conversions. When +``prepared`` is False, the default implementation of +:meth:`get_db_prep_value` will call :meth:`get_prep_value` to do +initial data conversions before performing any database-specific +processing. + .. method:: get_db_prep_save(self, value, connection) -Same as the above, but called when the Field value must be *saved* to the -database. As the default implementation just calls ``get_db_prep_value``, you -shouldn't need to implement this method unless your custom field needs a -special conversion when being saved that is not the same as the conversion used -for normal query parameters (which is implemented by ``get_db_prep_value``). +Same as the above, but called when the Field value must be *saved* to +the database. As the default implementation just calls +``get_db_prep_value``, you shouldn't need to implement this method +unless your custom field needs a special conversion when being saved +that is not the same as the conversion used for normal query +parameters (which is implemented by ``get_db_prep_value``). Preprocessing values before saving ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -453,7 +475,13 @@ correct value. Preparing values for use in database lookups ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: get_db_prep_lookup(self, lookup_type, value, connection) +As with value conversions, preparing a value for database lookups is a +two phase process. + +.. method:: get_prep_lookup(self, lookup_type, value) + +:meth:`get_prep_lookup` performs the first phase of lookup preparation, +performing generic data validity checks Prepares the ``value`` for passing to the database when used in a lookup (a ``WHERE`` constraint in SQL). The ``lookup_type`` will be one of the valid @@ -470,34 +498,42 @@ by with handling the lookup types that need special handling for your field and pass the rest to the :meth:`get_db_prep_lookup` method of the parent class. If you needed to implement ``get_db_prep_save()``, you will usually need to -implement ``get_db_prep_lookup()``. If you don't, ``get_db_prep_value`` will be +implement ``get_prep_lookup()``. If you don't, ``get_prep_value`` will be called by the default implementation, to manage ``exact``, ``gt``, ``gte``, ``lt``, ``lte``, ``in`` and ``range`` lookups. You may also want to implement this method to limit the lookup types that could be used with your custom field type. -Note that, for ``range`` and ``in`` lookups, ``get_db_prep_lookup`` will receive +Note that, for ``range`` and ``in`` lookups, ``get_prep_lookup`` will receive a list of objects (presumably of the right type) and will need to convert them to a list of things of the right type for passing to the database. Most of the -time, you can reuse ``get_db_prep_value()``, or at least factor out some common +time, you can reuse ``get_prep_value()``, or at least factor out some common pieces. -For example, the following code implements ``get_db_prep_lookup`` to limit the +For example, the following code implements ``get_prep_lookup`` to limit the accepted lookup types to ``exact`` and ``in``:: class HandField(models.Field): # ... - def get_db_prep_lookup(self, lookup_type, value): + def get_prep_lookup(self, lookup_type, value): # We only handle 'exact' and 'in'. All others are errors. if lookup_type == 'exact': - return [self.get_db_prep_value(value)] + return [self.get_prep_value(value)] elif lookup_type == 'in': - return [self.get_db_prep_value(v) for v in value] + return [self.get_prep_value(v) for v in value] else: raise TypeError('Lookup type %r not supported.' % lookup_type) +.. method:: get_db_prep_lookup(self, lookup_type, value, connection, prepared=False) + +Performs any database-specific data conversions required by a lookup. +As with :meth:`get_db_prep_value`, the specific connection that will +be used for the query is passed as the ``connection`` parameter. +The ``prepared`` argument describes whether the value has already been +prepared with :meth:`get_prep_lookup`. + Specifying the form field for a model field ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 04f3a12c70..3ab71e97b4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -35,6 +35,11 @@ their deprecation, as per the :ref:`Django deprecation policy (i.e., ``sqlite3`` instead of ``django.db.backends.sqlite3``) will be removed. + * The ``get_db_prep_save``, ``get_db_prep_value`` and + ``get_db_prep_lookup`` methods on Field were modified in 1.2 to support + multiple databases. In 1.4, the support functions that allow methods + with the old prototype to continue working will be removed. + * 2.0 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 3c56abbe54..48bfb29ba0 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -141,6 +141,74 @@ appear in ``__dict__`` for a model instance. If your code relies on iterating over __dict__ to obtain a list of fields, you must now filter out ``_state`` attribute of out ``__dict__``. +``get_db_prep_*()`` methods on Field +------------------------------------ + +Prior to v1.2, a custom field had the option of defining several +functions to support conversion of Python values into +database-compatible values. A custom field might look something like:: + + class CustomModelField(models.Field): + # ... + + def get_db_prep_save(self, value): + # ... + + def get_db_prep_value(self, value): + # ... + + def get_db_prep_lookup(self, lookup_type, value): + # ... + +In 1.2, these three methods have undergone a change in prototype, and +two extra methods have been introduced:: + + class CustomModelField(models.Field): + # ... + + def get_prep_value(self, value): + # ... + + def get_prep_lookup(self, lookup_type, value): + # ... + + def get_db_prep_save(self, value, connection): + # ... + + def get_db_prep_value(self, value, connection, prepared=False): + # ... + + def get_prep_lookup(self, lookup_type, value, connection, prepared=False): + # ... + +These changes are required to support multiple databases - +``get_db_prep_*`` can no longer make any assumptions regarding the +database for which it is preparing. The ``connection`` argument now +provides the preparation methods with the specific connection for +which the value is being prepared. + +The two new methods exist to differentiate general data preparation +requirements, and requirements that are database-specific. The +``prepared`` argument is used to indicate to the database preparation +methods whether generic value preparation has been performed. If +an unprepared (i.e., ``prepared=False``) value is provided to the +``get_db_prep_*()`` calls, they should invoke the corresponding +``get_prep_*()`` calls to perform generic data preparation. + +Conversion functions has been provided which will transparently +convert functions adhering to the old prototype into functions +compatible with the new prototype. However, this conversion function +will be removed in Django 1.4, so you should upgrade your Field +definitions to use the new prototype. + +If your ``get_db_prep_*()`` methods made no use of the database +connection, you should be able to upgrade by renaming +``get_db_prep_value()`` to ``get_prep_value()`` and +``get_db_prep_lookup()`` to ``get_prep_lookup()`. If you require +database specific conversions, then you will need to provide an +implementation ``get_db_prep_*`` that uses the ``connection`` +argument to resolve database-specific values. + .. _deprecated-features-1.2: Features deprecated in 1.2