Задача: нужно подставлять значения по-умолчанию из базы данных, не зашивая их в моделях.
Mysql предоставляет такую информацию через запрос:
mysql> SELECT cast( DEFAULT( StopTime ) as char(256) ) FROM Order LIMIT 1;
+------------------------------------------+
| cast( DEFAULT( StopTime ) as char(256) ) |
+------------------------------------------+
| 0000-00-00 00:00:00 |
+------------------------------------------+
1 row in set (0.00 sec)
Хочется в Django тоже иметь такую возможность. Для этого требуется переопределить классы требуемых типов полей. Может потребоваться переопределить слишком много классов. Потому предлагаются использовать декораторы.
Забирать значения по умолчанию из базы можно по-разному:
- один раз при валидации моделей;
- при каждом запросе на сохранение объекта.
Первый вариант плох потому, что приложение может жить годами без перезапуска, а менять значения в базе могут достаточно часто. Второй вариант создаст много паразитных запросов на каждое сохранение объекта. Самым правильным вариантом, кажется, будет опрашивать базу или по таймауту или через определенное число запросов.
Для этого, мы опишем следующую функцию:
def get_default_from_db(cls, self, connection):
'''
Возвращает значение поля по-умолчанию из базы данных.
Обращения к описанием полей кешируются по числу обращений.
Количество обращений задается константой
DEFAULT_FIELDS_TIMEOUT_TIMES
'''
## Добавляем словарь со значениями по-умолчанию.
if(not hasattr(self.model, 'default_fields_from_db')):
self.model.default_fields_from_db = {}
## Добавляем словарь с числом обращений.
if(not hasattr(self.model, 'default_fields_times')):
self.model.default_fields_times = {}
## Достаем из словаря количество обращений к полю.
## Если в словаре ничего нет, то считаем, нуль обращений.
self.model.default_fields_times[self.db_column] = \
self.model.default_fields_times.get(
self.db_column, 0
)
## Если число обращений больше некоторой константы обнуляем это число.
## Значение по умолчанию в этом случае считаем неопределенным.
## Иначе увеличиваем число обращений.
if(DEFAULT_FIELDS_TIMEOUT_TIMES == self.model.default_fields_times[self.db_column]):
self.model.default_fields_times[self.db_column] = 0
default = Undefined
else:
self.model.default_fields_times[self.db_column] += 1;
default = self.model.default_fields_from_db.get(self.db_column, Undefined)
## Если значение по умолчанию не определено,
## то запрашиваем его в базе данных.
if(Undefined == default):
c = connection.cursor()
try:
##
## Хитрая работа с cast( DEFAULT( %s ) as char(256) )
## связана с тем, что без привидения может быть возвращен
## не тот объект, который описан в базе,
## а его представление в python.
## Это не всегда оказывается приемлемым.
## Например, дата 0000-00-00 00:00:00 распознается как None.
## И при вставке в базу вставляется как NULL.
##
c.execute("SELECT cast( DEFAULT( %s ) as char(256) ) FROM %s LIMIT 1;"%(
self.db_column,
self.model._meta.db_table
))
(default,) = c.fetchone()
finally:
c.close()
## Добавляем новое значение в кеш-словарь.
self.model.default_fields_from_db[self.db_column] = default
return default
Далее нам требуется привязать значения полей по умолчанию к логике работы полей. Для этого нужно научится указывать, что мы хотим использовать поле по умолчанию из базы.
Мы решили использовать специальное строковое значение поля default. Наприер 'get-default-from-db'
. Возможно, это не самое удачное решение, но оно достаточно простое и понятное. Более того, хочется сделать так, что если значение по умолчанию не указано, то брать его из базы.
Для этого можно написать не очень сложный класс-декоратор:
class DbFieldDecorator(object):
def __call__(slf, dclass):
class NewClass(dclass):
def __init__(self, default = 'get-default-from-db', *args, **kwargs):
dclass.__init__(
self,
default = default,
*args,
**kwargs
)
def to_python(self, value):
if('get-default-from-db' == value):
if (not self.empty_strings_allowed):
return None
return ""
return super(NewClass, self).to_python(value)
def get_prep_value(self, value):
if('get-default-from-db' == value):
if (not self.empty_strings_allowed or (self.null and
not connection.features.interprets_empty_strings_as_nulls)):
return None
return ""
return super(NewClass, self).get_prep_value(value)
def get_db_prep_value(self, value, connection, prepared=False):
if('get-default-from-db' == value):
return DbFieldDecorator.get_default_from_db(self, connection)
return super(NewClass, self).get_db_prep_value(
value,
connection,
prepared
)
return NewClass
В новом классе мы определяем необходимые методы полей. Подробнее: /rel1.6/howto/custom-model-fields.html
Таким образом, мы получили весь пример целиком:
# -*- coding: utf-8 -*-
from django.db import connection
from django.db import models
DEFAULT_FIELDS_TIMEOUT_TIMES = 100
GET_DEFAULT_FROM_DB = 'get-default-from-db'
class Undefined:
pass
class DbFieldDecorator(object):
def __call__(slf, dclass):
class NewClass(dclass):
def __init__(self, default = GET_DEFAULT_FROM_DB, *args, **kwargs):
dclass.__init__(
self,
default = default,
*args,
**kwargs
)
def to_python(self, value):
if(GET_DEFAULT_FROM_DB == value):
if (not self.empty_strings_allowed):
return None
return ""
return super(NewClass, self).to_python(value)
def get_prep_value(self, value):
if(GET_DEFAULT_FROM_DB == value):
if (not self.empty_strings_allowed or (self.null and
not connection.features.interprets_empty_strings_as_nulls)):
return None
return ""
return super(NewClass, self).get_prep_value(value)
def get_db_prep_value(self, value, connection, prepared=False):
if(GET_DEFAULT_FROM_DB == value):
return DbFieldDecorator.get_default_from_db(self, connection)
return super(NewClass, self).get_db_prep_value(
value,
connection,
prepared
)
return NewClass
@classmethod
def get_default_from_db(cls, self, connection):
if(not hasattr(self.model, 'default_fields_from_db')):
self.model.default_fields_from_db = {}
if(not hasattr(self.model, 'default_fields_times')):
self.model.default_fields_times = {}
self.model.default_fields_times[self.db_column] = \
self.model.default_fields_times.get(
self.db_column, 0
)
if(DEFAULT_FIELDS_TIMEOUT_TIMES == self.model.default_fields_times[self.db_column]):
self.model.default_fields_times[self.db_column] = 0
default = Undefined
else:
self.model.default_fields_times[self.db_column] += 1;
default = self.model.default_fields_from_db.get(self.db_column, Undefined)
if(Undefined == default):
c = connection.cursor()
try:
c.execute("SELECT cast( DEFAULT( %s ) as char(256) ) FROM %s LIMIT 1;"%(
self.db_column,
self.model._meta.db_table
))
(default,) = c.fetchone()
finally:
c.close()
self.model.default_fields_from_db[self.db_column] = default
return default
@classmethod
def get_default_from_db(cls, self, connection):
'''
Возвращает значение поля по-умолчанию из базы данных.
Обращения к описанием полей кешируются по числу обращений.
Количество обращений задается константой
DEFAULT_FIELDS_TIMEOUT_TIMES
'''
## Добавляем словарь со значениями по-умолчанию.
if(not hasattr(self.model, 'default_fields_from_db')):
self.model.default_fields_from_db = {}
## Добавляем словарь с числом обращений.
if(not hasattr(self.model, 'default_fields_times')):
self.model.default_fields_times = {}
## Достаем из словаря количество обращений к полю.
## Если в словаре ничего нет, то считаем, нуль обращений.
self.model.default_fields_times[self.db_column] = \
self.model.default_fields_times.get(
self.db_column, 0
)
## Если число обращений больше некоторой константы обнуляем это число.
## Значение по умолчанию в этом случае считаем неопределенным.
## Иначе увеличиваем число обращений.
if(DEFAULT_FIELDS_TIMEOUT_TIMES == self.model.default_fields_times[self.db_column]):
self.model.default_fields_times[self.db_column] = 0
default = Undefined
else:
self.model.default_fields_times[self.db_column] += 1;
default = self.model.default_fields_from_db.get(self.db_column, Undefined)
## Если значение по умолчанию не определено,
## то запрашиваем его в базе данных.
if(Undefined == default):
c = connection.cursor()
try:
##
## Хитрая работа с cast( DEFAULT( %s ) as char(256) )
## связана с тем, что без привидения может быть возвращен
## не тот объект, который описан в базе,
## а его представление в python.
## Это не всегда оказывается приемлемым.
## Например, дата 0000-00-00 00:00:00 распознается как None.
## И при вставке в базу вставляется как NULL.
##
c.execute("SELECT cast( DEFAULT( %s ) as char(256) ) FROM %s LIMIT 1;"%(
self.db_column,
self.model._meta.db_table
))
(default,) = c.fetchone()
finally:
c.close()
## Добавляем новое значение в кеш-словарь.
self.model.default_fields_from_db[self.db_column] = default
return default
@DbFieldDecorator()
class DateTimeField(models.DateTimeField):
def get_db_prep_value(self, value, connection, prepared=False):
if(None == value):
return 0
if(0 == value):
return 0
if('0000-00-00 00:00:00' == value):
return 0
if('0' == value):
return 0
return super(DateTimeField, self).get_db_prep_value(
value,
connection,
prepared
)
@DbFieldDecorator()
class IntegerField(models.IntegerField):
pass