В практике разработчика 1С регулярно возникают задачи по обработке существующих документов и заполнению их реквизитов на основе определенных правил. Типичный пример такой задачи — необходимость определить контрагента для нового или существующего документа на основе префикса его номера. Например, если номер документа начинается с "ВО32", то контрагентом должна стать "Перспектива", а если с "С24" — "Позитив". При этом количество таких соответствий может достигать нескольких десятков, а сами префиксы могут иметь разную длину (от 1 до 4 и более символов).
В этой статье мы подробно разберем, как правильно реализовать такую логику в системе 1С:Предприятие 8. Мы сопоставим различные коллекции значений (Структура, Соответствие, СписокЗначений), проанализируем ошибки начинающих разработчиков и рассмотрим высокопроизводительные варианты решения этой задачи с помощью запросов.
Начнем с анализа классической ошибки, которую часто совершают начинающие программисты. При попытке связать префикс с конкретным контрагентом первая мысль, которая приходит в голову — использовать объект Структура или бесконечные конструкции Если...ИначеЕсли...КонецЕсли.
Рассмотрим, почему использование объекта Структура в данной ситуации является грубой ошибкой. Дело в том, что ключами структуры в 1С могут быть только строки, соответствующие правилам идентификатора (они не должны содержать пробелов, спецсимволов и не могут начинаться с цифр). Если ваш префикс выглядит как "1С" или "А-1", система вызовет исключение при попытке добавить такой ключ в структуру. Для хранения произвольных связок "Ключ — Значение" в оперативной памяти 1С существует специализированный объект — Соответствие.
Давайте сопоставим основные коллекции значений 1С для этой задачи:
Соответствие работает чрезвычайно быстро.НайтиСтроки()), так и для передачи в качестве параметра в запрос.Рассмотрим подробнее, как решить задачу сопоставления префиксов и контрагентов тремя разными способами.
Если перед нами стоит разовая задача обработки небольшого объема данных в памяти без сложных манипуляций на уровне СУБД, использование объекта Соответствие является самым простым и лаконичным решением. Мы один раз заполняем коллекцию, а затем в цикле быстро получаем нужного контрагента.
Давайте разберем пример кода, реализующий этот подход:
// Создаем соответствие "Префикс - Код контрагента"
КартаПрефиксов = Новый Соответствие;
КартаПрефиксов.Вставить("ВО32", "Ф00001701");
КартаПрефиксов.Вставить("Л11", "Ф00001701");
КартаПрефиксов.Вставить("Б8", "Ф00001701");
КартаПрефиксов.Вставить("С24", "Ф00001695");
КартаПрефиксов.Вставить("Т27", "Ф00001695");
КартаПрефиксов.Вставить("В2", "Ф00000248");
КартаПрефиксов.Вставить("Л17", "Ф00000285");
// Чтобы избежать выполнения запросов в цикле (проблема N+1 запросов),
// мы сначала получим ссылки всех контрагентов одним запросом
СписокКодов = Новый Массив;
Для Каждого КлючИЗначение Из КартаПрефиксов Цикл
СписокКодов.Добавить(КлючИЗначение.Значение);
КонецЦикла;
ЗапросКонтрагенты = Новый Запрос;
ЗапросКонтрагенты.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Ссылка,
| Контрагенты.Код КАК Код
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Код В (&СписокКодов)";
ЗапросКонтрагенты.УстановитьПараметр("СписокКодов", СписокКодов);
ТаблицаКонтрагентов = ЗапросКонтрагенты.Выполнить().Выгрузить();
// Создаем быстрое соответствие "Префикс - Ссылка на контрагента"
СоответствиеКонтрагентов = Новый Соответствие;
Для Каждого КлючИЗначение Из КартаПрефиксов Цикл
СтрокаКонтрагента = ТаблицаКонтрагентов.Найти(КлючИЗначение.Value, "Код");
Если СтрокаКонтрагента <> Неопределено Тогда
СоответствиеКонтрагентов.Вставить(КлючИЗначение.Ключ, СтрокаКонтрагента.Ссылка);
КонецЕсли;
КонецЦикла;
// Теперь обрабатываем выборку документов
Для Каждого СтрокаДокумента Из ТЧРеализации Цикл
НомерДокумента = СтрокаДокумента.Номер;
НайденныйКонтрагент = Неопределено;
// Проверяем префиксы разной длины от 4 до 1 символа
Для ДлинаПрефикса = ИсходнаяДлинаПоиска По 1 Шаг -1 Цикл
ТекущийПрефикс = Лев(НомерДокумента, ДлинаПрефикса);
НайденныйКонтрагент = СоответствиеКонтрагентов[ТекущийПрефикс];
Если НайденныйКонтрагент <> Неопределено Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Если НайденныйКонтрагент <> Неопределено Тогда
СтрокаДокумента.НовыйКонтрагент = НайденныйКонтрагент;
Иначе
Сообщить("Не удалось определить контрагента для документа номер: " + НомерДокумента);
КонецЕсли;
КонецЦикла;
Важный нюанс: Обратите внимание на обход префиксов по длинам от большего к меньшему (Шаг -1). Если в вашей системе есть префикс "В" и префикс "В2", то при проверке номера "В2-001" программа сначала должна проверить префикс длины 2 ("В2"), сопоставить его, и только если соответствие не найдено, переходить к префиксу длины 1 ("В"). В противном случае более короткий префикс перекроет длинный, и логика сопоставления нарушится.
Если документов много (более нескольких сотен), обработка в памяти может работать неэффективно. Самым быстрым способом сопоставления является использование механизмов СУБД. Профессиональные разработчики никогда не зашивают правила сопоставления в текст запроса жестким кодом (hardcode) через ВЫБОР КОГДА ... ТОГДА, как это показано в Сообщении 39. Вместо этого они передают таблицу правил в запрос в качестве параметра.
Давайте проанализируем и реализуем этот элегантный подход по шагам:
Шаг 1. Создание таблицы правил в коде 1С. Мы объявим структуру таблицы, содержащую префикс, его длину и код целевого контрагента. Мы можем хранить эти правила в текстовой строке, что делает их очень удобными для редактирования прямо в модуле или во внешней обработке:
// Формируем текстовое описание правил (Префикс;КодКонтрагента)
ТекстПравил = "
|ВО32;Ф00001701
|Л11;Ф00001701
|Б8;Ф00001701
|С24;Ф00001695
|Т27;Ф00001695
|В2;Ф00000248
|Л17;Ф00000285";
ТаблицаПравил = Новый ТаблицаЗначений;
ТаблицаПравил.Колонки.Добавить("Префикс", Новый ОписаниеТипов("Строка",,,, Новый КвалификаторыСтроки(10)));
ТаблицаПравил.Колонки.Добавить("ДлинаПрефикса", Новый ОписаниеТипов("Число",,,, Новый КвалификаторыЧисла(2, 0)));
ТаблицаПравил.Колонки.Добавить("КодКонтрагента", Новый ОписаниеТипов("Строка",,,, Новый КвалификаторыСтроки(9)));
Для ИндексСтроки = 1 По СтрЧислоСтрок(ТекстПравил) Цикл
ТекущаяСтрока = СтрПолучитьСтроку(ТекстПравил, ИндексСтроки);
Если ПустаяСтрока(СокрЛП(ТекущаяСтрока)) Тогда
Продолжить;
КонецЕсли;
РазделеннаяСтрока = СтрРазделить(ТекущаяСтрока, ";");
Префикс = СокрЛП(РазделеннаяСтрока[0]);
НоваяСтрока = ТаблицаПравил.Добавить();
НоваяСтрока.Префикс = Префикс;
НоваяСтрока.ДлинаПрефикса = СтрДлина(Префикс);
НоваяСтрока.КодКонтрагента = СокрЛП(РазделеннаяСтрока[1]);
КонецЦикла;
Шаг 2. Разработка пакетного запроса сопоставления. Мы передадим эту таблицу в запрос, преобразуем ее во временную таблицу, свяжем коды контрагентов с реальными ссылками на справочник Ссылка.Контрагенты и выполним соединение с таблицей документов по длинам префиксов.
Проанализируем текст запроса:
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаПравил", ТаблицаПравил);
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаКонец", ДатаКонец);
Запрос.Текст =
"ВЫБРАТЬ
| Правила.Префикс КАК Префикс,
| Правила.ДлинаПрефикса КАК ДлинаПрефикса,
| Правила.КодКонтрагента КАК КодКонтрагента
|ПОМЕСТИТЬ ВТ_Правила
|ИЗ
| &ТаблицаПравил КАК Правила
|;
|
|////////////////////////////////////////////////////////////////////////////////
|// Шаг 2.1. Соединяем коды с реальными ссылками контрагентов
|ВЫБРАТЬ
| ВТ_Правила.Префикс КАК Префикс,
| ВТ_Правила.ДлинаПрефикса КАК ДлинаПрефикса,
| ЕСТЬNULL(Контрагенты.Ссылка, ЗНАЧЕНИЕ(Справочник.Contragenty.EmptyRef)) КАК Контрагент
|ПОМЕСТИТЬ ВТ_ПравилаСКонтрагентами
|ИЗ
| ВТ_Правила КАК ВТ_Правила
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
| ПО ВТ_Правила.КодКонтрагента = Контрагенты.Код
|;
|
|////////////////////////////////////////////////////////////////////////////////
|// Шаг 2.2. Получаем список документов за период
|ВЫБРАТЬ
| Реализация.Ссылка КАК Документ,
| Реализация.Номер КАК Номер
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаКонец
| И НЕ Реализация.ПометкаУдаления
|;
|
|////////////////////////////////////////////////////////////////////////////////
|// Шаг 2.3. Выполняем сопоставление по длинам префиксов.
|// Конструкция ЕСТЬNULL позволяет корректно обработать приоритет длинных префиксов над короткими.
|ВЫБРАТЬ
| Документы.Документ КАК Документ,
| Документы.Номер КАК Номер,
| ЕСТЬNULL(ВТ_Правила4.Контрагент,
| ЕСТЬNULL(ВТ_Правила3.Contragent,
| ЕСТЬNULL(ВТ_Правила2.Контрагент, ВТ_Правила1.Контрагент)
| )
| ) КАК НовыйКонтрагент
|ИЗ
| ВТ_Документы КАК Документы
| // Сопоставляем по префиксам длиной 4 символа
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПравилаСКонтрагентами КАК ВТ_Правила4
| ПО ВТ_Правила4.ДлинаПрефикса = 4
| И ПОДСТРОКА(Документы.Номер, 1, 4) = ВТ_Правила4.Префикс
|
| // Сопоставляем по префиксам длиной 3 символа
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПравилаСКонтрагентами КАК ВТ_Правила3
| ПО ВТ_Правила3.ДлинаПрефикса = 3
| И ПОДСТРОКА(Документы.Номер, 1, 3) = ВТ_Правила3.Префикс
|
| // Сопоставляем по префиксам длиной 2 символа
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПравилаСКонтрагентами КАК ВТ_Правила2
| ПО ВТ_Правила2.ДлинаПрефикса = 2
| И ПОДСТРОКА(Документы.Номер, 1, 2) = ВТ_Правила2.Префикс
|
| // Сопоставляем по префиксам длиной 1 символ
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПравилаСКонтрагентами КАК ВТ_Правила1
| ПО ВТ_Правила1.ДлинаПрефикса = 1
| И ПОДСТРОКА(Документы.Номер, 1, 1) = ВТ_Правила1.Префикс";
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Рассмотрим подробнее преимущества этого метода. Во-первых, он полностью избавляет нас от использования медленной конструкции ПОДОБНО (LIKE в СУБД) на стороне базы данных. Оператор точного равенства = работает в разы быстрее, так как СУБД не тратит ресурсы на синтаксический анализ шаблонов поиска.
Во-вторых, в результате выполнения этого запроса мы сразу получаем готовую таблицу, в которой каждому документу сопоставлен необходимый элемент справочника Контрагенты. Нам остается лишь загрузить эти данные в форму или запустить транзакцию для изменения документов в базе данных.
Рассмотрим также вариант, к которому в итоге пришел автор обсуждения в Сообщении 39. Это классический пример жестко закодированного запроса. Такой подход имеет право на жизнь только в том случае, если задача действительно "разовая", правила сопоставления никогда больше не изменятся, а у программиста нет времени на создание динамических таблиц правил.
Проанализируем синтаксис структуры ВЫБОР КОГДА ... ТОГДА ... КОНЕЦ в запросах 1С. В коде автора это выглядит следующим образом:
ВЫБОР
КОГДА ПОДСТРОКА(РеализацияТоваровУслугУслуги.Ссылка.Номер, 1, 4) ПОДОБНО "ВО32"
ИЛИ ПОДСТРОКА(РеализацияТоваровУслугУслуги.Ссылка.Номер, 1, 3) ПОДОБНО "Л11"
ИЛИ ПОДСТРОКА(РеализацияТоваровУслугУслуги.Ссылка.Номер, 1, 3) ПОДОБНО "Б8"
ТОГДА "Ф00001701"
КОГДА ПОДСТРОКА(РеализацияТоваровУслугУслуги.Ссылка.Номер, 1, 3) ПОДОБНО "С24"
ИЛИ ПОДСТРОКА(РеализацияТоваровУслугУслуги.Ссылка.Номер, 1, 3) ПОДОБНО "Т27"
ТОГДА "Ф00001695"
КОНЕЦ КАК Префикс
Выясним причину, почему этот код работает медленно на больших базах данных:
ПОДОБНО вместо =. СУБД вынуждена выполнять сравнение по шаблону, даже когда мы ищем точное совпадение начала строки. Правильнее использовать знак равенства: ПОДСТРОКА(...) = "ВО32".ВЫБОР КОГДА. Если этот запрос содержит секцию СГРУППИРОВАТЬ ПО, СУБД будет вынуждена вычислять значение ВЫБОР КОГДА для каждой строки таблицы базы данных, что заблокирует использование индексов таблиц и приведет к полному сканированию таблицы (Table Scan).ВЫБОР КОГДА — первый раз в секции ВЫБРАТЬ, а второй раз в секции СГРУППИРОВАТЬ ПО. Это увеличивает риск совершения синтаксических ошибок и делает код нечитаемым.Для того чтобы ваша обработка не просто работала корректно, но и выполнялась максимально быстро, придерживайтесь следующих правил при разработке подобных алгоритмов:
Индексируйте временные таблицы. Если вы используете пакетные запросы и создаете временную таблицу с документами или правилами, обязательно добавляйте к ней индекс по тем колонкам, по которым будет происходить соединение. В нашем примере во временной таблице ВТ_ПравилаСКонтрагентами полезно добавить индекс по полям Префикс и ДлинаПрефикса.
Минимизируйте строковые операции в СУБД. Функции работы со строками (такие как ПОДСТРОКА) на уровне базы данных выполняются относительно медленно. Старайтесь ограничивать выборку документов на самых ранних этапах запроса (например, жестким фильтром по периоду или виду операции), чтобы строковые соединения выполнялись только для тех строк, которые действительно необходимо обработать.
Избегайте записи документов в цикле без транзакции. Если целью вашей обработки является не просто вывод данных на форму, а изменение реквизита "Контрагент" в самих документах базы данных, обязательно используйте транзакции. Запись документов пакетами по 50-100 штук внутри одной транзакции способна ускорить процесс записи в десятки раз за счет снижения накладных расходов СУБД на фиксацию транзакций.