Представим реальную практическую ситуацию: в вашу базу данных «1С:Предприятие» поступает пакет изменений через внешний обмен. Обмен разрабатывался сторонними подрядчиками, и у вас нет возможности напрямую вмешаться в его код или изменить логику принимающего узла. При этом возникает жесткое бизнес-требование: изменения определенных объектов (например, некоторых критически важных справочников или документов) нельзя допускать в базу данных.
Если мы попытаемся пойти стандартным путем и пропишем в обработчике ПередЗаписью конструкцию Отказ = Истина, это приведет к прерыванию транзакции. Внешний обмен получит системную ошибку, аварийно завершит свою работу и, скорее всего, заблокирует последующую загрузку остальных данных. Нам же необходимо «обмануть» систему: сделать так, чтобы платформа выполнила запись, не зафиксировав при этом никаких изменений, и рапортовала внешнему обмену об успешном завершении операции.
В этой статье мы подробно разберем элегантное техническое решение этой задачи, проанализируем скрытые риски его использования на уровне СУБД, рассмотрим влияние на планы обмена и изучим альтернативные архитектурные паттерны.
Решение, к которому пришли опытные разработчики, заключается в использовании предопределенного метода Прочитать() прикладного объекта в обработчике события ПередЗаписью(). Этот метод выполняет чтение сохраненного состояния объекта непосредственно из базы данных и заменяет им все значения реквизитов, которые в данный момент находятся в оперативной памяти (в объекте).
Рассмотрим, как выглядит этот код на практике в модуле объекта (или в подписке на событие):
Процедура ПередЗаписью(Отказ)
// Обязательно проверяем, что запись происходит в режиме загрузки данных обмена
Если ОбменДанными.Загрузка Тогда
// Проверяем наши специфические условия, при которых изменения вносить нельзя
Если Не РазрешеноИзменятьОбъект() Тогда
// Ключевой прием: перечитываем состояние объекта из базы данных,
// затирая все пришедшие из обмена изменения в оперативной памяти
ЭтотОбъект.Прочитать();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Давайте подробно разберем по шагам, что происходит во время выполнения этого кода:
Записать().ПередЗаписью().ЭтотОбъект.Прочитать(). Он выполняет запрос к СУБД, извлекает старые значения реквизитов и замещает ими новые измененные данные прямо в памяти.Чтобы понять всю глубину этого приема, выясним причину, почему обычное присвоение реквизитов из базы (например, через ручное чтение ссылки) может работать некорректно или вызывать конфликты блокировок.
У каждого ссылочного объекта в платформе «1С:Предприятие 8» присутствует скрытый системный реквизит ВерсияДанных. Он используется для реализации механизма оптимистической блокировки. Когда сторонний обмен присылает измененный объект, у него в оперативной памяти может содержаться устаревшая или вообще незаполненная версия данных.
Если мы попытаемся перезаписать такой объект стандартным способом, платформа выполнит сравнение версий. Не обнаружив соответствия между версией в памяти и версией в СУБД, система выбросит критическое исключение: «Операция не может быть выполнена из-за несоответствия версии...».
Вызов метода Прочитать() решает эту проблему на системном уровне. Он не просто восстанавливает значения реквизитов, но и актуализирует ВерсиюДанных объекта в оперативной памяти до текущей версии в СУБД. Благодаря этому последующий шаг записи проходит гладко, без конфликтов версий и блокировок.
Проанализируем серьезный риск, который часто упускают разработчики при использовании данного «костыля». Речь идет о механизме авторегистрации изменений на узлах планов обмена.
Даже если после вызова Прочитать() данные объекта в памяти никак не изменились по сравнению с СУБД, сам факт успешного выполнения метода Записать() инициирует регистрацию этого объекта на узлах планов обмена (если для данного объекта настроена авторегистрация).
В результате возникает следующая проблема:
Прочитать() и успешно записывает старую версию.Как бороться с этой проблемой? В обработчике ПередЗаписью() необходимо принудительно очищать список получателей обмена для записываемого объекта:
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Если Не РазрешеноИзменятьОбъект() Тогда
ЭтотОбъект.Прочитать();
// Очищаем регистрацию изменений для всех узлов планов обмена
ЭтотОбъект.ОбменДанными.Получатели.Очистить();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
С точки зрения архитектуры высоконагруженных систем данный метод является «антипаттерном» (anti-pattern) и должен применяться только в крайних случаях, когда другие пути решения полностью заблокированы. Рассмотрим подробнее почему:
ПередЗаписью() всегда выполняется внутри неявной транзакции записи, открываемой платформой.Прочитать() внутри транзакции порождает дополнительный синхронный запрос SELECT к СУБД.Deadlocks) или ошибок ожидания блокировок.Если у вас есть возможность повлиять на архитектуру обмена данными, рекомендуется отказаться от использования метода Прочитать() в пользу более чистых решений:
Отказ = Истина на уровне правил конвертации просто пропустит этот объект в XML/JSON-пакете, предотвращая даже попытку открытия транзакции записи в СУБД.ДополнительныеСвойства. В подписке проверять этот маркер и, если объект не должен меняться, управлять ходом записи на более ранних этапах.Стоит отметить, что аналогичный трюк с методом Прочитать() часто используют в штатных механизмах 1С, но не для обмана обменов, а для решения проблемы удаления записей в регистрах сведений.
Когда происходит удаление набора записей регистра сведений, в обработчик ПередЗаписью() передается абсолютно пустой набор (Количество() = 0). Из-за этого разработчик в коде не может понять, какие именно данные сейчас удаляются из системы (например, чтобы записать их в журнал регистрации или историю изменений).
Если в этот момент вызвать метод ЭтотОбъект.Прочитать(), платформа временно заполнит коллекцию набора теми записями, которые сейчас фактически физически находятся в базе данных и подлежат удалению. Разработчик может проанализировать их, зафиксировать изменения, после чего очистить набор и позволить платформе успешно завершить транзакцию удаления.