В процессе разработки отчетов или заполнения документов в 1С часто возникает задача: выбрать список контрагентов вместе с их контактной информацией (например, адресами электронной почты). Однако бизнес-логика может требовать исключений. Рассмотрим распространенную ситуацию: нам необходимо заполнить таблицу контрагентами, у которых заполнен Email, но если у контрагента оформлен договор определенного вида (например, «Услуга»), он должен попасть в документ в любом случае — даже если адрес электронной почты у него не указан.
При решении этой задачи разработчики часто сталкиваются с тем, что контрагенты без почты «исчезают» из результатов запроса или строки в документе начинают дублироваться. В этой статье мы подробно проанализируем причины такого поведения СУБД, разберем типичные ошибки и рассмотрим несколько вариантов правильного решения задачи.
Проанализируем ситуацию на примере исходного запроса, с которым столкнулся автор темы. Попытки наложить условие «выводить контрагента, если заполнен Email ИЛИ если вид договора равен "Услуга"» часто приводят к неработающему коду. Выясним причину, разобрав основные ошибки.
Ошибка №1: Несовместимость типов в конструкции ВЫБОР
В попытках вернуть признак валидности адреса разработчики часто пишут следующую конструкцию:
ВЫБОР
КОГДА ВыбДокумент.ДоговорКонтрагента.ВидВзаиморасчетов = &Договор И КонтрагентыКонтактнаяИнформация.АдресЭП = ""
ТОГДА ИСТИНА
ИНАЧЕ КонтрагентыКонтактнаяИнформация.АдресЭП
КОНЕЦ КАК АдресЭП
Это грубая ошибка языка запросов 1С. В рамках одной колонки запроса СУБД не может возвращать значения разных типов. В ветке ТОГДА возвращается Булево (ИСТИНА), а в ветке ИНАЧЕ — Строка (АдресЭП). Такой запрос либо завершится аварийно с ошибкой несовместимости типов, либо приведет к непредсказуемому поведению при попытке СУБД неявно преобразовать типы данных.
Ошибка №2: Некорректная обработка значений NULL при левом соединении
Когда мы соединяем документ со справочником контактной информации с помощью ЛЕВОГО СОЕДИНЕНИЯ, СУБД пытается найти запись с адресом для каждого контрагента. Если у контрагента вообще нет ни одной записи электронной почты в базе, то в полях присоединяемой таблицы (включая АдресЭП) вернется специальное пустое значение — NULL.
В языке SQL и запросах 1С любое прямое сравнение с NULL (например, АдресЭП = "" или НЕ АдресЭП = "") возвращает результат ЛОЖЬ (точнее, "Неизвестно"). В результате контрагенты, у которых физически отсутствует запись Email в базе данных, полностью отсекаются условиями фильтрации в секции ГДЕ.
Ошибка №3: Дублирование строк (картезианское произведение)
Табличная часть контактной информации контрагента содержит множество записей разного типа: телефоны, юридические адреса, фактические адреса и адреса электронной почты. Если связать таблицу документов со справочником контактной информации только по ссылке:
ПО ВыбДокумент.Контрагент = КонтрагентыКонтактнаяИнформация.Ссылка
то СУБД сопоставит документу все имеющиеся у контрагента виды контактной информации. В результате одна строка документа превратится в три или четыре строки в выборке (по одной строке на телефон, Email и адрес). Чтобы этого избежать, условие фильтрации по типу контактной информации нужно накладывать непосредственно в секции связи.
Ошибка №4: Проблема производительности при разыменовании полей
Использование конструкции ВыбДокумент.ДоговорКонтрагента.ВидВзаиморасчетов в условиях запроса заставляет платформу 1С делать неявные левые соединения таблиц через несколько уровней («через точку»). На крупных базах данных это приводит к существенному замедлению выполнения запросов.
---
Рассмотрим подробнее, как правильно составить запрос к табличной части контактной информации справочника контрагентов, избежав дублирования и корректно обработав пустые значения Email.
Разберем по шагам логику построения правильного запроса:
ЛЕВОЕ СОЕДИНЕНИЕ.ПО (параметры связи) обязательно добавляем жесткий фильтр по типу контактной информации: АдресЭлектроннойПочты. Это защитит запрос от дублирования строк.NULL) оборачиваем поле в функцию ЕСТЬNULL(..., "").ГДЕ прописываем комбинированное условие: нам подходят либо контрагенты с заполненным Email, либо любые контрагенты (даже без Email), если у них выбран договор с нужным видом взаиморасчетов.Посмотрим на пример реализации такого запроса:
Функция ПоискКонтрагента(ВидДокумента)
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(Объект.ДатаНач));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(Объект.ДатаКон));
Запрос.УстановитьПараметр("ДоговорУслуга", Объект.ДоговорУслуга); // Передаем нужный вид договора в параметр
Запрос.Текст =
"ВЫБРАТЬ
| РассылкаПоЭлектроннойПочтеТЧСчета.Контрагент КАК Контрагент
|ПОМЕСТИТЬ ВТ_ИсключаемыеКонтрагенты
|ИЗ
| Документ.РассылкаПоЭлектроннойПочте.ТЧСчета КАК РассылкаПоЭлектроннойПочтеТЧСчета
|ГДЕ
| РассылкаПоЭлектроннойПочтеТЧСчета.Отправлять = ИСТИНА
| И НЕ РассылкаПоЭлектроннойПочтеТЧСчета.Ссылка.ПометкаУдаления
| И РассылкаПоЭлектроннойПочтеТЧСчета.Ссылка.Дата МЕЖДУ &НачалоПериода И &КонецПериода
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВыбДокумент.Ссылка КАК Ссылка,
| ВыбДокумент.Контрагент КАК Контрагент,
| ЕСТЬNULL(КонтрагентыКонтактнаяИнформация.АдресЭП, """") КАК АдресЭП
|ИЗ
| Документ." + ВидДокумента + " КАК ВыбДокумент
|
| // Левое соединение строго по типу контактной информации ""АдресЭлектроннойПочты""
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты.КонтактнаяИнформация КАК КонтрагентыКонтактнаяИнформация
| ПО ВыбДокумент.Контрагент = КонтрагентыКонтактнаяИнформация.Ссылка
| И КонтрагентыКонтактнаяИнформация.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.АдресЭлектроннойПочты)
|ГДЕ
| ВыбДокумент.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И НЕ ВыбДокумент.ПометкаУдаления
|
| // Исключаем контрагентов, которые уже обработаны (находятся во временной таблице)
| И НЕ ВыбДокумент.Контрагент В
| (ВЫБРАТЬ
| ВТ_ИсключаемыеКонтрагенты.Контрагент
| ИЗ
| ВТ_ИсключаемыеКонтрагенты КАК ВТ_ИсключаемыеКонтрагенты)
|
| // Ключевое условие: либо Email заполнен, либо договор относится к виду ""Услуга""
| И (ЕСТЬNULL(КонтрагентыКонтактнаяИнформация.АдресЭП, """") <> """"
| ИЛИ ВыбДокумент.ДоговорКонтрагента.ВидВзаиморасчетов = &ДоговорУслуга)";
Возврат Запрос.Выполнить().Выбрать();
КонецФункции
Проанализируем работу условия в секции ГДЕ. Если у контрагента заполнен Email, то выражение ЕСТЬNULL(КонтрагентыКонтактнаяИнформация.АдресЭП, "") <> "" возвращает ИСТИНА, и строка попадает в результат запроса независимо от договора. Если Email не заполнен (значение в базе отсутствует или равно пустой строке), первая часть условия возвращает ЛОЖЬ, но СУБД проверяет вторую часть условия через оператор ИЛИ. Если договор документа соответствует виду договора из параметра &ДоговорУслуга, строка все равно попадет в результат выборки.
---
В современных конфигурациях 1С, построенных на базе Библиотеки стандартных подсистем (БСП), таких как 1С:ERP, 1С:КА, 1С:УТ 11, 1С:Бухгалтерия 3.0, контактная информация хранится в независимом регистре сведений РегистрСведений.КонтактнаяИнформация. Прямое обращение к табличным частям справочников считается устаревшим подходом.
Рассмотрим более производительный и архитектурно верный вариант построения запроса с использованием регистра сведений. Это позволяет избежать неявных левых соединений и ускорить работу на СУБД:
// Пример запроса с использованием регистра сведений контактной информации БСП
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
Запрос.УстановитьПараметр("ДоговорУслуга", ДоговорУслуга);
// Рекомендуется указывать конкретный вид контактной информации (например, Email контрагента)
Запрос.УстановитьПараметр("ВидКИ", Справочники.ВидыКонтактнойИнформации.EmailКонтрагента);
Запрос.Текст =
"ВЫБРАТЬ
| ВыбДокумент.Ссылка КАК Ссылка,
| ВыбДокумент.Контрагент КАК Контрагент,
| ЕСТЬNULL(РегистрКИ.ЗначениеПоля, """") КАК АдресЭП
|ИЗ
| Документ.РеализацияТоваровУслуг КАК ВыбДокумент
|
| // Соединение с регистром сведений по объекту-владельцу и конкретному виду контактной информации
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК РегистрКИ
| ПО ВыбДокумент.Контрагент = РегистрКИ.Объект
| И РегистрКИ.Вид = &ВидКИ
|ГДЕ
| ВыбДокумент.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И НЕ ВыбДокумент.ПометкаУдаления
| И (ЕСТЬNULL(РегистрКИ.ЗначениеПоля, """") <> """"
| ИЛИ ВыбДокумент.ДоговорКонтрагента.ВидВзаиморасчетов = &ДоговорУслуга)";
Посмотрим на преимущества этого метода. Во-первых, обращение к плоской таблице регистра сведений выполняется быстрее, чем к табличной части справочника. Во-вторых, использование параметра &ВидКИ (ссылка на предопределенный элемент справочника ВидыКонтактнойИнформации) гарантирует, что мы выберем строго один конкретный вид почты (например, «Почта для отправки счетов»), полностью исключая дубли строк.
---
Если вам требуется получить контактную информацию программно для дальнейшей обработки объектов в коде (например, перед непосредственной отправкой писем), лучшим решением будет использование встроенного API Библиотеки стандартных подсистем. Это гарантирует корректность работы даже при изменении структуры метаданных в новых версиях конфигурации.
Разберем, как получить Email контрагентов с помощью общего модуля УправлениеКонтактнойИнформацией:
// Получаем массив контрагентов из документов
СписокКонтрагентов = Новый Массив;
Для Каждого СтрокаТЧ Из Объект.ТабличнаяЧасть Цикл
Если ЗначениеЗаполнено(СтрокаТЧ.Контрагент) Тогда
СписокКонтрагентов.Добавить(СтрокаТЧ.Контрагент);
КонецЕсли;
КонецЦикла;
// Получаем контактную информацию пакетным методом БСП
ТаблицаАдресов = УправлениеКонтактнойИнформацией.КонтактнаяИнформацияОбъектов(
СписокКонтрагентов,
Перечисления.ТипыКонтактнойИнформации.АдресЭлектроннойПочты
);
// Теперь мы можем легко сопоставить данные в коде
Для Каждого СтрокаТЧ Из Объект.ТабличнаяЧасть Цикл
// Ищем полученный Email в таблице адресов
ПараметрыОтбора = Новый Структура("Объект", СтрокаТЧ.Контрагент);
НайденныеСтроки = ТаблицаАдресов.НайтиСтроки(ПараметрыОтбора);
ТекущийEmail = "";
Если НайденныеСтроки.Количество() > 0 Тогда
ТекущийEmail = НайденныеСтроки[0].Представление;
КонецЕсли;
// Проверяем условия бизнес-логики
ДоговорУслуга = (СтрокаТЧ.Договор.ВидВзаиморасчетов = Объект.ДоговорУслуга);
Если ЗначениеЗаполнено(ТекущийEmail) ИЛИ ДоговорУслуга Тогда
// Выполняем заполнение или отправку
СтрокаТЧ.АдресЭП = ТекущийEmail;
СтрокаТЧ.ДобавлятьВДокумент = ИСТИНА;
Иначе
СтрокаТЧ.ДобавлятьВДокумент = ЛОЖЬ;
КонецЕсли;
КонецЦикла;
Использование готовых методов БСП избавляет разработчика от необходимости писать сложные запросы с левыми соединениями вручную, автоматически обрабатывает NULL, учитывает историю изменений контактной информации и защищает от дублирования строк на уровне платформы.
---
Для того чтобы ваши запросы к контактной информации всегда работали быстро и корректно, рекомендуем придерживаться следующих правил:
ЕСТЬNULL(Поле, "").ПО. Фильтр по типу контактной информации (например, по типу АдресЭлектроннойПочты или конкретному виду КИ) должен находиться в условиях соединения таблиц, а не в секции ГДЕ.ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ все возвращаемые ветками значения должны иметь строго один тип (например, только строки).КонтактнаяИнформация или программные методы общего модуля УправлениеКонтактнойИнформацией.