Общие вопросы

Массовое создание и обновление наборов запросов

django-simple-history функционирует, сохраняя историю с помощью сигнала post_save каждый раз, когда сохраняется объект с историей. Однако для некоторых массовых операций, таких как bulk_create, bulk_update и queryset updates, сигналы не посылаются, и история не сохраняется автоматически. Однако django-simple-history предоставляет служебные функции для обхода этой проблемы.

Bulk Создание модели с историей

Начиная с версии django-simple-history 2.2.0, мы можем использовать служебную функцию bulk_create_with_history для массового создания объектов с сохранением их истории:

>>> from simple_history.utils import bulk_create_with_history
>>> from simple_history.tests.models import Poll
>>> from django.utils.timezone import now
>>>
>>> data = [Poll(id=x, question='Question ' + str(x), pub_date=now()) for x in range(1000)]
>>> objs = bulk_create_with_history(data, Poll, batch_size=500)
>>> Poll.objects.count()
1000
>>> Poll.history.count()
1000

Если вы хотите указать причину изменения или пользователя истории для каждой записи при массовом создании, вы можете добавить _change_reason, _history_user<< 1 >>>history_date на каждом экземпляре:

>>> for poll in data:
        poll._change_reason = 'reason'
        poll._history_user = my_user
        poll._history_date = some_date
>>> objs = bulk_create_with_history(data, Poll, batch_size=500)
>>> Poll.history.get(id=data[0].id).history_change_reason
'reason'

Вы также можете указать пользователя по умолчанию или причину изменения по умолчанию, ответственную за изменение (_change_reason, _history_user<< 1 >>>history_date имеют приоритет).

>>> user = User.objects.create_user("tester", "tester@example.com")
>>> objs = bulk_create_with_history(data, Poll, batch_size=500, default_user=user)
>>> Poll.history.get(id=data[0].id).history_user == user
True

Массовое обновление модели с историей (новое)

Массовое обновление было введено в Django 2.2. Мы можем использовать функцию bulk_update_with_history для массового обновления объектов с помощью функции Django bulk_update, сохраняя при этом историю объектов:

>>> from simple_history.utils import bulk_update_with_history
>>> from simple_history.tests.models import Poll
>>> from django.utils.timezone import now
>>>
>>> data = [Poll(id=x, question='Question ' + str(x), pub_date=now()) for x in range(1000)]
>>> objs = bulk_create_with_history(data, Poll, batch_size=500)
>>> for obj in objs: obj.question = 'Duplicate Questions'
>>> bulk_update_with_history(objs, Poll, ['question'], batch_size=500)
>>> Poll.objects.first().question
'Duplicate Question``

Если ваши модели требуют использования альтернативного менеджера моделей (обычно потому, что менеджер по умолчанию возвращает отфильтрованный набор), вы можете указать, какой менеджер использовать, с помощью аргумента manager:

>>> from simple_history.utils import bulk_update_with_history
>>> from simple_history.tests.models import PollWithAlternativeManager
>>>
>>> data = [PollWithAlternativeManager(id=x, question='Question ' + str(x), pub_date=now()) for x in range(1000)]
>>> objs = bulk_create_with_history(data, PollWithAlternativeManager, batch_size=500, manager=PollWithAlternativeManager.all_polls)

Обновление QuerySet с помощью истории (обновлено в Django 2.2)

В отличие от bulk_create, queryset updates выполняет SQL-запрос на обновление кверисета и никогда не возвращает фактические обновленные объекты (которые были бы необходимы для вставок в историческую таблицу). Таким образом, мы говорим, что обновления queryset не сохранят историю (поскольку не посылается сигнал post_save). Как говорится в документации Django:

If you want to update a bunch of records for a model that has a custom
``save()`` method, loop over them and call ``save()``, like this:
for e in Entry.objects.filter(pub_date__year=2010):
    e.comments_on = False
    e.save()

Примечание: Django 2.2 теперь позволяет bulk_update. Сигналы pre_save или post_save по-прежнему не посылаются.

Отслеживание пользовательских пользователей

  • fields.E300:

    ERRORS:
    custom_user.HistoricalCustomUser.history_user: (fields.E300) Field defines a relation with model 'custom_user.CustomUser', which is either not installed, or is abstract.
    

    Используйте register() для отслеживания изменений в пользовательской модели пользователя вместо того, чтобы устанавливать HistoricalRecords непосредственно на модели.

    Причина этого в том, что, к сожалению, HistoricalRecords не может быть установлен непосредственно на подмененную пользовательскую модель из-за внешнего ключа user для отслеживания пользователя, вносящего изменения.

Использование django-webtest с Middleware

При использовании django-webtest для тестирования вашего Django проекта с промежуточным программным обеспечением django-simple-history, вы можете столкнуться с ошибкой, подобной следующей:

django.db.utils.IntegrityError: (1452, 'Cannot add or update a child row: a foreign key constraint fails (`test_env`.`core_historicaladdress`, CONSTRAINT `core_historicaladdress_history_user_id_0f2bed02_fk_user_user_id` FOREIGN KEY (`history_user_id`) REFERENCES `user_user` (`id`))')

Эта ошибка возникает из-за того, что django-webtest устанавливает DEBUG_PROPAGATE_EXCEPTIONS в true, не позволяя промежуточному ПО очистить запрос. Чтобы решить эту проблему, добавьте следующий код в любой метод clean_environment или << 3 >>>, который вы используете:

from simple_history.middleware import HistoricalRecords
if hasattr(HistoricalRecords.context, 'request'):
    del HistoricalRecords.context.request

Использование выражений F()

Выражения F(), как описано здесь, не работают на моделях, имеющих историю. Простая история вставляет новую запись в историческую таблицу для любой обновляемой модели. Однако выражения F() работают только для обновлений. Таким образом, когда выражение F() используется в модели с исторической таблицей, историческая модель пытается вставить запись с помощью выражения F() и вызывает ошибку ValueError.

Зарезервированные имена полей

Для каждой базовой модели, история которой отслеживается с помощью django-simple-history, создается соответствующая историческая модель. Таким образом, если мы имеем:

class BaseModel(models.Model):
    history = HistoricalRecords()

Также создается модель Django под названием HistoricalBaseModel со всеми полями из BaseModel, плюс несколько дополнительных полей и методов, которые есть во всех исторических моделях.

Поскольку эти поля и методы присутствуют во всех исторических моделях, любые имена полей и методов в базовой модели, которые не совпадают с этими именами, не будут присутствовать в исторической модели (и, следовательно, не будут отслеживаться). Ниже приведены зарезервированные исторические имена полей и методов:

  • history_id

  • history_date

  • history_change_reason

  • history_type

  • history_object

  • history_user

  • history_user_id

  • instance

  • instance_type

  • next_record

  • prev_record

  • revert_url

  • __str__

Так что если у нас есть:

class BaseModel(models.Model):
    instance = models.CharField(max_length=255)
    history = HistoricalRecords()

поле instance фактически не будет отслеживаться в таблице истории, поскольку оно находится в зарезервированном наборе терминов.

Наследование нескольких таблиц

django-simple-history поддерживает отслеживание истории на моделях, использующих многотабличное наследование, таких как:

class ParentModel(models.Model):
    parent_field = models.CharField(max_length=255)
    history = HistoricalRecords()

class ChildModel(ParentModel):
    child_field = models.CharField(max_length=255)
    history = HistoricalRecords()

Несколько замечаний:

  • В дочерней модели экземпляр HistoricalRecords не наследуется от родительской модели. Это означает, что вы можете выбрать отслеживание изменений только в родительской модели, только в дочерней модели или в обеих.

  • Таблица истории дочерней модели содержит все поля из дочерней модели, а также все поля из родительской модели.

  • Обновление дочернего экземпляра обновляет только таблицу истории дочернего экземпляра, но не таблицу истории родительского экземпляра.

Использование с django-modeltranslation

Если у вас установлен django-modeltranslation, вам необходимо использовать метод register() для моделирования перевода, как описано here.

Указание на модель

Иногда вам нужно указать на модель исторических записей. Примером могут служить общие представления Django или сериализаторы Django REST framework. Вы можете попасть туда через менеджер HistoricalRecords, который вы определили в своей модели. Согласно нашему примеру:

class PollHistoryListView(ListView): # or PollHistorySerializer(ModelSerializer):
    class Meta:
        model = Poll.history.model
       # ...

Работа с конвейерами BitBucket

При использовании BitBucket Pipelines для тестирования вашего Django проекта с промежуточным ПО django-simple-history вы столкнетесь с ошибкой, связанной с отсутствием миграций, относящихся к исторической модели User из приложения auth. Это происходит потому, что файл миграции не хранится ни в вашем проекте, ни в django-simple-history. Чтобы обойти эту ошибку, вам нужно добавить `python manage.py makemigrations auth` шаг в ваш YML файл перед запуском тестов.

Вернуться на верх