При сопровождении и доработке конфигураций на платформе 1С:Предприятие разработчики часто сталкиваются с неочевидным поведением платформы и СУБД. Классический пример — использование агрегатных функций МИНИМУМ или МАКСИМУМ по ссылочному полю в запросах. В этой статье мы подробно разберем, почему возникает такая проблема, как работают механизмы СУБД при сравнении уникальных идентификаторов (GUID), и рассмотрим практические способы исправления ситуации без изменения кода основной конфигурации.
Рассмотрим типичную ситуацию на примере запроса из конфигурации «Управление производственным предприятием» (УПП). В системе существует предопределенный вид расчета (например, «Командировка» с кодом 1283). Бухгалтер копирует этот вид расчета, создает дубликат с кодом 1284, чтобы настроить для него специфическое отражение в учете. В этот момент ломается логика типового алгоритма.
Типовой запрос выбирает минимальный вид расчета среди подходящих под условия:
ВЫБРАТЬ
МИНИМУМ(ОсновныеНачисления.Ссылка) КАК ВидРасчета,
ВидыДоговоровСФизЛицами.Ссылка КАК ВидДоговора
ПОМЕСТИТЬ ВТНачислениеКомандировки
ИЗ
Перечисление.ВидыДоговоровСФизЛицами КАК ВидыДоговоровСФизЛицами
ВНУТРЕННЕЕ СОЕДИНЕНИЕ
ПланВидовРасчета.ОсновныеНачисленияОрганизаций КАК ОсновныеНачисления
ПО
(ОсновныеНачисления.СпособРасчета = ЗНАЧЕНИЕ(Перечисление.СпособыРасчетаОплатыТруда.ПоСреднемуЗаработку))
И (ОсновныеНачисления.ВидВремени = ЗНАЧЕНИЕ(Перечисление.ВидыВремени.ЦелодневноеНеотработанное))
И (ОсновныеНачисления.ОбозначениеВТабелеУчетаРабочегоВремени = ЗНАЧЕНИЕ(Справочник.КлассификаторИспользованияРабочегоВремени.Командировка))
СГРУППИРОВАТЬ ПО
ВидыДоговоровСФизЛицами.Ссылка
До копирования запрос всегда возвращал предопределенный вид расчета. После копирования результатом функции МИНИМУМ(Ссылка) внезапно становится новый элемент (код 1284). Это происходит потому, что платформа 1С на уровне СУБД сравнивает бинарные данные ссылок — их уникальные идентификаторы (GUID). Если GUID нового элемента оказывается «меньше» с точки зрения СУБД, то система выбирает именно его, нарушая бизнес-логику программы.
Выясним причину, почему так происходит, проанализировав поведение различных систем управления базами данных.
---
В базе данных СУБД ссылки 1С хранятся в шестнадцатеричном бинарном формате (тип данных binary(16) в MS SQL или uuid в PostgreSQL). Сравнение этих данных не эквивалентно лексикографическому сравнению строк, к которому привыкли программисты.
1. Поведение в MS SQL Server
MS SQL Server использует для хранения ссылок тип данных uniqueidentifier. При выполнении операции сравнения или сортировки (включая агрегатные функции MIN/MAX) MS SQL Server применяет специфический алгоритм: он оценивает группы байтов справа налево. Наибольший приоритет при сравнении имеют последние 6 байт GUID (так называемая группа "node").
Это означает, что если у нового элемента последние байты GUID окажутся меньше, чем у старого предопределенного элемента, то в SQL-запросе функция MIN(_IDRRef) гарантированно вернет ссылку на новый элемент, независимо от даты его создания, кода или наименования.
2. Поведение в PostgreSQL
В СУБД PostgreSQL тип данных uuid сравнивается стандартным побайтовым методом слева направо (аналог функции memcmp). Результат выполнения одного и того же запроса с функцией МИНИМУМ(Ссылка) на MS SQL Server и на PostgreSQL может кардинально отличаться именно из-за разницы алгоритмов сравнения на уровне СУБД!
3. Последовательные GUID (Sequential UUIDs) в 1С
Начиная с версии платформы 8.3, 1С:Предприятие генерирует упорядоченные (последовательные) уникальные идентификаторы для уменьшения фрагментации индексов таблиц СУБД при добавлении новых записей. Обычно новые элементы получают GUID, который больше ранее созданных. Однако при ручном копировании элементов, обмене данными или миграциях баз эта последовательность нарушается, что и приводит к подобным коллизиям.
---
Рассмотрим по шагам несколько практических способов исправления ситуации, когда нам категорически нельзя или крайне нежелательно редактировать код типового общего модуля напрямую.
Поскольку напрямую отредактировать GUID существующего элемента в базе данных невозможно, нам необходимо создать новый объект с правильным (заведомо «большим») идентификатором и перенести на него все ссылки. Разберем этот процесс по шагам.
УстановитьСсылкуНового(). Нам нужно сгенерировать такой GUID, который при сравнении в вашей СУБД гарантированно будет «больше» GUID предопределенного элемента.Пример кода для внешней обработки по генерации и записи элемента с новым GUID:
// 1. Получаем GUID предопределенного элемента, который должен остаться минимальным
ПредопределенныйЭлемент = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.Командировка;
УИДПредопределенного = ПредопределенныйЭлемент.УникальныйИдентификатор();
// 2. Генерируем новый GUID, который гарантированно больше старого для нашей СУБД
// Для надежности можно использовать циклическую генерацию, пока не получим нужные байты
НовыйУИД = НайтиПравильныйGUID(УИДПредопределенного);
// 3. Создаем новый элемент плана видов расчета с принудительной установкой ссылки
НоваяСсылка = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.ПолучитьСсылка(НовыйУИД);
НовыйОбъект = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.СоздатьВидРасчета();
НовыйОбъект.УстановитьСсылкуНового(НоваяСсылка);
// Заполняем реквизиты нового элемента данными из временного объекта
ЗаполнитьЗначенияСвойств(НовыйОбъект, ВременныйЭлемент);
НовыйОбъект.Код = "1284";
НовыйОбъект.Записать();
После записи нового элемента снова запустите обработку «Поиск и замена значений» и перенесите все данные с элемента «Временный Командировка» на наш вновь созданный элемент. Временный элемент удалите. Теперь GUID нового элемента физически больше, и запрос вернет предопределенный элемент.
---
Для того чтобы гарантированно сгенерировать GUID, который СУБД будет считать большим, чем GUID предопределенного элемента, воспользуемся алгоритмом циклической проверки байтов. Напишем функцию для сравнения уникальных идентификаторов по правилам MS SQL Server:
Функция GUIDБольшеДляMSSQL(GUID1, GUID2) Экспорт
Строка1 = НРег(СтрЗаменить(Строка(GUID1), "-", ""));
Строка2 = НРег(СтрЗаменить(Строка(GUID2), "-", ""));
// MS SQL Server сравнивает группы байтов в следующем порядке:
// Подстроки: [10-15] байты (символы 20-32), затем [8-9] (16-20), [6-7] (12-16), [0-5] (0-12)
Группа1_1 = Сред(Строка1, 21, 12);
Группа2_1 = Сред(Строка2, 21, 12);
Если Группа1_1 <> Группа2_1 Тогда
Возврат Группа1_1 > Группа2_1;
КонецЕсли;
Группа1_2 = Сред(Строка1, 17, 4);
Группа2_2 = Сред(Строка2, 17, 4);
Если Группа1_2 <> Группа2_2 Тогда
Возврат Группа1_2 > Группа2_2;
КонецЕсли;
Группа1_3 = Сред(Строка1, 13, 4);
Группа2_3 = Сред(Строка2, 13, 4);
Если Группа1_3 <> Группа2_3 Тогда
Возврат Группа1_3 > Группа2_3;
КонецЕсли;
Группа1_4 = Лев(Строка1, 12);
Группа2_4 = Лев(Строка2, 12);
Возврат Группа1_4 > Группа2_4;
КонецФункции
С помощью этой функции в цикле генерируйте новые уникальные идентификаторы через метод Новый УникальныйИдентификатор() до тех пор, пока функция GUIDБольшеДляMSSQL не вернет значение Истина по отношению к GUID предопределенного элемента. Обычно для этого требуется не более нескольких десятков итераций, что занимает доли секунды.
---
Если ваша база данных работает на платформе 1С:Предприятие 8.3 (даже если сама конфигурация работает в режиме совместимости со старыми версиями), лучшим решением будет использование механизма Расширений конфигурации. Это позволит не трогать данные в СУБД и сохранить конфигурацию на поддержке.
Разберем этот метод по шагам:
АнализНеявокПереопределяемый), и добавьте его в расширение.&Вместо для функции, возвращающей текст запроса:
&Вместо("ВернутьТекстВТНачислениеКомандировки")
Функция Расш_ВернутьТекстВТНачислениеКомандировки()
// Мы можем полностью переопределить текст запроса, избавившись от ненадежной функции МИНИМУМ
// и заменив ее на явный отбор по предопределенному значению
ТекстЗапроса =
"ВЫБРАТЬ
| ЗНАЧЕНИЕ(ПланВидовРасчета.ОсновныеНачисленияОрганизаций.Командировка) КАК ВидРасчета,
| ВидыДоговоровСФизЛицами.Ссылка КАК ВидДоговора
|ПОМЕСТИТЬ ВТНачислениеКомандировки
|ИЗ
| Перечисление.ВидыДоговоровСФизЛицами КАК ВидыДоговоровСФизЛицами";
Возврат ТекстЗапроса;
КонецФункции
Этот способ полностью решает проблему, исключает риск повторения ситуации при создании других копий видов расчета и гарантирует стабильную работу программы при последующих обновлениях конфигурации поставщика.