Каждый разработчик 1С рано или поздно сталкивается с ситуацией, когда стандартные и давно проверенные механизмы платформы ведут себя непредсказуемо. Одной из таких загадок является работа метода Свернуть() для объекта ТаблицаЗначений. Вы можете получить таблицу из внешнего источника, выполнить сворачивание по строковой колонке, но на выходе обнаружить строки, которые визуально выглядят абсолютно идентично.
Ситуация становится критической, когда при попытке записать эти «свернутые» данные в регистр сведений или в другое индексируемое поле СУБД, система возвращает ошибку нарушения уникальности ключа. В этой статье мы подробно проанализируем, почему платформа 1С считает такие строки разными, а SQL Server — одинаковыми, и разберем эффективные методы решения этой проблемы.
Для того чтобы понять природу этой ошибки, нам необходимо заглянуть под капот механизмов сравнения строк в оперативной памяти 1С и в базе данных под управлением SQL Server. Проблема кроется в принципиальном различии их логики:
Свернуть() работает непосредственно в оперативной памяти вашего сервера приложений или клиента. Для платформы 1С строка — это массив символов Unicode. Сравнение происходит посимвольно и побайтово. Если в одной строке присутствует скрытый, непечатный или управляющий символ, а в другой его нет, 1С однозначно определит эти строки как разные. Метод Свернуть() оставит обе строки в таблице значений.Cyrillic_General_CI_AS). Согласно этим правилам, многие управляющие символы Unicode, спецсимволы разметки или двунаправленного письма игнорируются при сравнении. Для SQL Server строка с невидимым спецсимволом и строка без него — это одна и та же строка. СУБД пытается записать дубль в уникальный индекс и генерирует ошибку.Дополнительной сложностью является стандарт сравнения строк ANSI/ISO SQL-92, согласно которому SQL Server при сравнении строк разной длины фактически дополняет более короткую строку пробелами в конце до длины длинной строки. Для 1С строки "Товар" и "Товар " (с пробелом на конце) — разные, а для SQL Server — одинаковые. И хотя хвостовые пробелы легко удаляются стандартной функцией СокрЛП(), со специальными Unicode-символами всё гораздо сложнее.
Если визуально строки кажутся одинаковыми, а функция СтрДлина() возвращает для них разные значения даже после применения СокрЛП(), значит, в строке присутствуют скрытые спецсимволы. Чаще всего они попадают в систему при копировании данных пользователями из интернет-браузеров, Excel или при интеграции через внешние API.
Проанализируем основные группы таких символов:
8234 по 8238 (в шестнадцатеричном формате Unicode — U+202A по U+202E). К ним относятся LRE, RLE, PDF, LRO, RLO. Именно символ с кодом 8237 (LRO — Left-to-Right Override) часто становится причиной сбоев. Они управляют направлением отображения текста (например, для арабского языка или иврита вперемешку с латиницей) и абсолютно невидимы на экране.160 (U+00A0). Платформа 1С в некоторых контекстах не считает его стандартным пробелом, из-за чего СокрЛП() его не удаляет.65279 (U+FEFF). Часто оказывается в самом начале строки при неаккуратном чтении текстовых файлов или XML.173 (U+00AD). Не отображается, пока слово не переносится на новую строку.0. Может ломать строковые функции и приводить к неожиданным результатам при записи в БД.Попытка использовать стандартную функцию общего модуля БСП ЗаменитьНедопустимыеСимволыXML() в данном случае не поможет. С точки зрения спецификации XML 1.0, BiDi-символы (такие как 8237) являются абсолютно валидными, поэтому функция их игнорирует и оставляет в строке.
Рассмотрим наиболее гибкий и современный способ борьбы со скрытыми символами — использование регулярных выражений. С его помощью мы можем быстро удалить все нежелательные Unicode-диапазоны. Для этого воспользуемся COM-объектом VBScript.RegExp.
Разберем по шагам создание универсальной функции очистки текста:
Функция ОчиститьСтрокуРегулярнымВыражением(ЗначениеСтроки) Экспорт
// Если передано пустое или нестроковое значение, возвращаем как есть
Если Не ЗначениеЗаполнено(ЗначениеСтроки) Или ТипЗнч(ЗначениеСтроки) <> Тип("Строка") Тогда
Возврат ЗначениеСтроки;
КонецЕсли;
РегВыражение = Новый COMОбъект("VBScript.RegExp");
РегВыражение.IgnoreCase = Истина;
РегВыражение.Global = Истина;
// Шаблон находит BiDi-символы, неразрывные пробелы, BOM и другие непечатные управляющие знаки
// Диапазон \u200B-\u200D — это нулевые пробелы, \u202A-\u202E — символы направления письма
РегВыражение.Pattern = "[\u202A-\u202E\u200B-\u200D\uFEFF\u00A0]";
ОчищеннаяСтрока = РегВыражение.Replace(ЗначениеСтроки, " "); // Заменяем на обычный пробел или пустую строку ""
// Дополнительно очищаем обычные хвостовые пробелы
Возврат СокрЛП(ОчищеннаяСтрока);
КонецФункции
Этот способ крайне эффективен, так как позволяет за один проход избавиться от целого спектра скрытых управляющих знаков без необходимости посимвольного перебора строки.
Если использование COM-объектов в вашей среде нежелательно (например, при работе на серверах под управлением ОС Linux, где VBScript.RegExp недоступен без дополнительных библиотек), мы можем применить метод фильтрации по «белому списку» разрешенных символов.
Проанализируем логику этого подхода: вместо того чтобы искать тысячи возможных «плохих» символов Unicode, мы оставим в строке только те символы, коды которых нам точно известны и разрешены (буквы, цифры, стандартные знаки препинания). Посмотрим на программную реализацию:
Функция ОчиститьСтрокуПоБеломуСписку(ЗначениеСтроки) Экспорт
Результат = "";
ДлинаСтроки = СтрДлина(ЗначениеСтроки);
Для Индекс = 1 По ДлинаСтроки Цикл
Символ = Сред(ЗначениеСтроки, Индекс, 1);
Код = КодСимвола(Символ);
// Определяем разрешенные диапазоны:
// Коды 32-126 — базовая латиница, цифры, пробел и знаки препинания
// Коды 1040-1103 — основная кириллица (А-Я, а-я)
// Код 1025 — буква Ё, Код 1105 — буква ё
Если (Код >= 32 И Код <= 126)
Или (Код >= 1040 И Код <= 1103)
Или Код = 1025
Или Код = 1105 Тогда
Результат = Результат + Символ;
Иначе
// Недопустимый символ просто игнорируем или заменяем на пробел
Результат = Результат + " ";
КонецЕсли;
КонецЦикла;
// Сворачиваем лишние двойные пробелы, если они образовались при замене
Возврат СокрЛП(Результат);
КонецФункции
Важное примечание: Этот метод отлично подходит для моноязычных баз данных. Однако, если ваша база мультикультурная (содержит наименования на китайском, казахском, немецком или других языках с расширенной латиницей/иероглифами), вам придется существенно расширить диапазоны разрешенных кодов, что может сделать «белый список» избыточно сложным.
Если вам не хочется писать сложные алгоритмы фильтрации символов, а главная задача — гарантировать, что данные свернутся точно так же, как их воспримет база данных SQL Server при записи, мы можем делегировать операцию свертки самому серверу СУБД.
Для этого мы воспользуемся механизмом временных таблиц в запросе. Запрос выполнит группировку по правилам СУБД, автоматически объединив строки с невидимыми для SQL различиями. Разберем этот алгоритм на примере:
// Предположим, у нас есть исходная ТаблицаЗначений "ИсходныеДанные" с колонкой "НоменклатураНаименование"
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Передаем нашу таблицу значений в качестве параметра запроса
Запрос.УстановитьПараметр("ВходнаяТаблица", ИсходныеДанные);
// Описываем схему временной таблицы. Типы колонок должны быть жестко типизированы
Запрос.Текст =
"ВЫБРАТЬ
| ТекСтрока.НоменклатураНаименование КАК Наименование
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ВходнаяТаблица КАК ТекСтрока
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ИсходныеДанные.Наименование КАК Наименование
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
|
|СГРУППИРОВАТЬ ПО
| ВТ_ИсходныеДанные.Наименование";
// Выполняем запрос. На этом этапе SQL Server применит свои правила сравнения строк (Collation)
РезультатЗапроса = Запрос.Выполнить();
// Выгружаем гарантированно уникальные с точки зрения SQL Server данные обратно в ТЗ
СвернутаяТаблица = РезультатЗапроса.Выгрузить();
Этот метод элегантно решает проблему расхождения уникальности. Все строки, содержащие невидимые символы BiDi, будут сгруппированы СУБД в одну строку (в результат попадет значение первой встреченной строки из группы). После этого запись в регистр сведений пройдет без ошибок дублирования ключа.
Подводя итоги нашего анализа, дадим несколько практических советов для предотвращения подобных ошибок в будущем:
СокрЛП() в современных реалиях недостаточно.СГРУППИРОВАТЬ ПО вместо метода Свернуть(). Это убережет вас от коллизий при последующей записи данных в таблицы СУБД.