Каждый разработчик 1С рано или поздно сталкивается с ситуацией, когда рабочий код в режиме отладки преподносит неприятный сюрприз: при попытке просмотреть результат выполнения запроса в табло или вычислить выражение платформа выдает загадочную фразу «Ошибка чтения значения». Ситуация усугубляется тем, что в консоли запросов тот же самый запрос может работать абсолютно корректно и возвращать данные.
В этой статье мы подробно разберем, почему возникает эта ошибка, как устроен механизм выборки данных в 1С, а также проанализируем две классические архитектурные ошибки в запросах, которые приводят к пустому результату и, как следствие, к этой проблеме в отладчике.
Для начала давайте разберемся с поведением самого отладчика 1С. Когда мы выполняем запрос и получаем выборку, мы работаем с объектом ВыборкаИзРезультатаЗапроса. Этот объект представляет собой последовательный однонаправленный курсор базы данных. По умолчанию, сразу после инициализации выборки (до первого вызова метода Следующий()), этот курсор позиционирован перед первой записью.
Посмотрим на классический пример проблемного кода:
Выбор = Запрос.Выполнить().Выбрать();
Если Выбор.Следующий() Тогда // Точка останова здесь
Возврат Выбор.Руководитель;
КонецЕсли;
Если мы остановим отладчик на строке Если Выбор.Следующий() Тогда и попытаемся посмотреть значение свойства Выбор.Руководитель, платформа всегда вернет нам «Ошибка чтения значения». Происходит это по двум причинам:
Следующий() еще не был выполнен, а значит, текущая строка выборки неопределена. Платформа физически не знает, из какой строки базы данных считывать поле Руководитель.Выбор.Следующий() вернет значение Ложь. Курсор так и не перейдет на первую запись, и на любой последующей строке (например, в блоке Иначе или после КонецЕсли) попытка прочесть данные выборки вызовет ту же самую ошибку чтения.Практический совет по отладке: Чтобы быстро проверить, вернул ли ваш запрос хоть какие-то данные, не заглядывая внутрь выборки, добавьте в табло отладчика выражение Выбор.Количество() или проверяйте состояние результата запроса напрямую через метод РезультатЗапроса.Пустой(). Если вы хотите детально изучить структуру и данные в процессе отладки, временно выгрузите результат в таблицу значений:
Результат = Запрос.Выполнить();
Если Не Результат.Пустой() Тогда
ТаблицаДанных = Результат.Выгрузить(); // Эту таблицу очень удобно смотреть в отладчике
КонецЕсли;
---
Рассмотрим первую типичную ошибку, из-за которой запрос возвращает пустой результат. Разберем исходный код запроса, в котором разработчик пытался получить ответственное лицо организации:
Запрос.Текст = "ВЫБРАТЬ
| ОтветственныеЛица.Руководитель КАК Руководитель,
| ОтветственныеЛицаПолномочия.Должность КАК Должность
|ИЗ
| РегистрСведений.СведенияОбОтветственныхЛицах.СрезПоследних КАК ОтветственныеЛица
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОснованияПолномочийОтветственныхЛиц КАК ОтветственныеЛицаПолномочия
| ПО ОтветственныеЛица.Организация = ОтветственныеЛицаПолномочия.Организация
|ГДЕ
| ОтветственныеЛица.Период <= &Период
| И ОтветственныеЛица.Организация = &Организация";
На первый взгляд, логика кажется верной: мы хотим получить срез последних и ограничить его нашей датой и организацией через секцию ГДЕ. Однако с точки зрения СУБД этот запрос работает совершенно иначе и приводит к логической ошибке.
Выясним причину. Когда мы не указываем параметры в самой виртуальной таблице СрезПоследних(), система 1С сначала выполняет расчет среза на самую последнюю актуальную дату в системе (по умолчанию это конец времени — 3999 год). И только после того, как срез на «конец времен» рассчитан и сформирована временная таблица, СУБД начинает применять к ней условия из секции ГДЕ.
Если в системе самая свежая запись по организации была сделана, к примеру, 01.12.2025, а мы передаем в параметр &Период дату 01.06.2025, то происходит следующее:
ГДЕ проверяет условие: 01.12.2025 <= 01.06.2025.Ложь, и единственная строка отсекается фильтром. Запрос возвращает пустой результат.Правильное решение: Условия по периоду и измерениям (в данном случае по организации) необходимо передавать строго в параметры самой виртуальной таблицы. Это заставит платформу рассчитать срез последних непосредственно на указанную дату и сразу отсечь лишние организации на уровне базы данных, что также существенно повысит производительность.
Перепишем запрос правильно:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ОтветственныеЛица.Руководитель КАК Руководитель,
| ОтветственныеЛицаПолномочия.Должность КАК Должность,
| ОтветственныеЛицаПолномочия.ОснованиеПодписи КАК ОснованиеПодписи
|ИЗ
| РегистрСведений.СведенияОбОтветственныхЛицах.СрезПоследних(&Период, Организация = &Организация) КАК ОтветственныеЛица
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОснованияПолномочийОтветственныхЛиц КАК ОтветственныеЛицаПолномочия
| ПО ОтветственныеЛица.Организация = ОтветственныеЛицаПолномочия.Организация
| И ОтветственныеЛица.Руководитель = ОтветственныеЛицаПолномочия.ФизическоеЛицо";
Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Период", Дата);
---
Вторая распространенная ошибка, приводящая к пустому результату и последующей «Ошибке чтения значения», связана с путаницей в ссылочных типах данных. Проанализируем пример второго запроса автора, где он пытается получить ФИО человека:
Функция ПолучитьФИО(Физлицо, Период)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ФИОФизическихЛицСрезПоследних.Фамилия КАК Фамилия,
| ФИОФизическихЛицСрезПоследних.Имя КАК Имя,
| ФИОФизическихЛицСрезПоследних.Отчество КАК Отчество
|ИЗ
| РегистрСведений.ФИОФизическихЛиц.СрезПоследних(&Период, ФизическоеЛицо = &ФизЛицо) КАК ФИОФизическихЛицСрезПоследних";
Запрос.УстановитьПараметр("Период", Период);
Запрос.УстановитьПараметр("ФизЛицо", Физлицо); // Здесь кроется критическая ошибка!
...
КонецФункции
При вызове этой функции в параметр Физлицо передавался элемент из справочника Справочник.Сотрудники. В консоли запросов разработчик тестировал запрос, подставляя реальное физическое лицо из справочника Справочник.ФизическиеЛица, и все работало. Но в программном коде типы разошлись.
В типовых конфигурациях 1С (таких как ЗУП, ERP, Комплексная автоматизация) сущности «Сотрудник» и «Физическое лицо» строго разделены:
Измерение ФизическоеЛицо в регистре сведений ФИОФизическихЛиц имеет тип СправочникСсылка.ФизическиеЛица. Когда мы передаем в параметр &ФизЛицо ссылку типа СправочникСсылка.Сотрудники, СУБД пытается сравнить идентификаторы (GUID) из совершенно разных таблиц. Естественно, совпадений найдено не будет, и запрос вернет пустой результат, спровоцировав ошибку чтения в отладчике.
Правильное решение: Перед передачей параметра в запрос необходимо получить объект физического лица, связанный с данным сотрудником. Сделать это можно через реквизит справочника сотрудников:
// Извлекаем правильный тип данных перед установкой параметра
ФизическоеЛицоСсылка = Неопределено;
Если ЗначениеЗаполнено(Физлицо) Тогда
// Реквизит "ФизическоеЛицо" есть у каждого сотрудника в типовых базах
ФизическоеЛицоСсылка = Физлицо.ФизическоеЛицо;
КонецЕсли;
Запрос.УстановитьПараметр("ФизЛицо", ФизическоеЛицоСсылка);
---
Если вы работаете в современной конфигурации, построенной на базе БСП (Библиотеки стандартных подсистем), писать прямые запросы к регистрам сведений для получения кадровых данных или ФИО вручную не рекомендуется. Платформа предоставляет готовые оптимизированные механизмы, которые учитывают историю изменений, права доступа (RLS) и исключают типичные ошибки разработчиков.
Например, для получения ФИО физического лица или сотрудника на определенную дату намного надежнее использовать функции общего модуля кадрового учета:
// Получаем кадровые данные программно с помощью БСП
КадровыеДанные = КадровыйУчет.КадровыеДанныеФизическихЛиц(Истина, Физлицо, "Фамилия, Имя, Отчество", Период);
Если КадровыеДанные.Количество() > 0 Тогда
СтрокаДанных = КадровыеДанные[0];
Возврат Новый Структура("Фамилия, Имя, Отчество", СтрокаДанных.Фамилия, СтрокаДанных.Имя, СтрокаДанных.Отчество);
КонецЕсли;
Этот метод самостоятельно определит тип переданного объекта (будь то сотрудник или физлицо), корректно обратится к нужным регистрам сведений и вернет результат без лишнего написания тяжелых запросов.