При разработке и поддержке конфигураций на платформе 1С:Предприятие 7.7 программисты часто сталкиваются со странным поведением объектов, полученных из запросов. Типичный сценарий: мы делаем запрос к базе данных, выбираем реквизит типа "Документ", обходим группировку, но при попытке использовать полученное значение в методах позиционирования (таких как НайтиДокумент() или ВыбратьПодчиненныеДокументы()) система внезапно аварийно завершает работу с ошибкой "Недопустимая операция".
При этом функция ТипЗначенияСтр() уверенно утверждает, что тип полученной переменной — "Документ". Однако при детальном анализе выясняется, что свойства этого документа ведут себя некорректно: например, реквизит ДатаДок возвращает строковое представление вместо полноценного типа Дата, а метод ТекущийДокумент() просто отказывается работать. В сообществе разработчиков этот феномен получил название "полудокумент" или "псевдообъект".
В этой статье мы подробно разберем внутренние механизмы платформы 1С 7.7, выясним фундаментальные причины такого поведения SQL-движка и рассмотрим три элегантных способа решения этой проблемы без использования громоздких "костылей".
Проанализируем исходный запрос, который вызвал затруднения у автора темы:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса=
"//{{ЗАПРОС(Запрос)
|Период с Дата1 по Дата2;
|Обрабатывать НеПомеченныеНаУдаление;
|ОбрабатыватьДокументы Проведенные;
|Заказ = Документ.ОказаниеУслуг.Заказ,
| Документ.РасходнаяНакладная.Заказ;
|ВидОплаты = Документ.ОказаниеУслуг.Заказ.ВидОплаты,
| Документ.РасходнаяНакладная.Заказ.ВидОплаты;
|Условие (ВидОплаты <> ВидОплатыВнутреннийЗачет);
|Группировка Заказ;
|"//}}ЗАПРОС ;
Обратите внимание на строку инициализации переменной Заказ. Здесь происходит объединение (слияние) путей из разных видов документов: Документ.ОказаниеУслуг.Заказ и Документ.РасходнаяНакладная.Заказ. Даже если в конфигураторе оба этих реквизита жестко типизированы и ссылаются на один и тот же вид документа (например, Документ.Заказ), компилятор запросов 1С 7.7 обрабатывает такую конструкцию иначе.
Для платформы это объединение означает, что тип результирующего поля является неопределенным. Система создает так называемый "Документ неопределенного вида" (полиморфную ссылку). При обходе результатов запроса через Запрос.Группировка() движок базы данных оптимизирует потребление памяти и сетевого трафика. Вместо создания полноценного, тяжелого COM-объекта ссылки 1С, он возвращает облегченный псевдообъект группировки.
Этот псевдообъект содержит в себе только текстовые и числовые проекции значений, хранящиеся в выборке SQL/DBF. Именно поэтому:
ДатаДок возвращает строку (внутренний формат даты в БД — YYYYMMDD), так как полноценного приведения типов не произошло.ТекущийДокумент() не может отработать, так как псевдообъект "не знает" своего точного физического адреса и типа в таблицах базы данных.НайтиДокумент() и ВыбратьПодчиненныеДокументы() обращаются к ядру платформы (C++ коду), ожидая получить на вход валидный 32-битный бинарный идентификатор типа и объекта. Не обнаружив его в структуре псевдообъекта, платформа генерирует исключение "Недопустимая операция".Давайте рассмотрим, как решить эту проблему красиво и эффективно.
Самый быстрый и лаконичный способ превратить "сырой" псевдообъект запроса в полноценную, абсолютно чистую ссылку на документ — использовать механизмы системной сериализации во внутреннюю строку платформы. Это классический прием опытных разработчиков 1С 7.7.
Когда мы передаем объект в функцию ЗначениеВСтрокуВнутр(), платформа вынуждена обратиться к метаданным, определить реальный вид документа и записать его уникальный идентификатор в специальном текстовом формате (содержащем идентификатор типа и уникальный ID объекта в базе данных). Последующий вызов ЗначениеИзСтрокиВнутр() воссоздает в оперативной памяти полноценную типизированную ссылку.
Посмотрим на пример реализации этого метода:
Пока Запрос.Группировка("Заказ") = 1 Цикл
// "Очищаем" ссылку и приводим ее к полноценному типу
ЗаказСсылка = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(Запрос.Заказ));
// Теперь ЗаказСсылка - это полноценный документ.
// Мы можем безопасно позиционировать другие объекты
ЗаказДляПоиска = СоздатьОбъект("Документ");
Если ЗаказДляПоиска.НайтиДокумент(ЗаказСсылка) = 1 Тогда
// Документ успешно найден, пишем лаконичный код дальше
Сообщить("Успешно позиционирован заказ № " + ЗаказДляПоиска.НомерДок);
КонецЕсли;
КонецЦикла;
Преимущества метода: Мы не меняем структуру запроса и получаем стопроцентно работающий код всего в одну строку. Данный способ сохраняет лаконичность модуля и избавляет от любых архитектурных "костылей".
Если мы хотим написать максимально правильный с точки зрения архитектуры код, нам следует изменить сам подход к построению запроса. Проблема "полудокумента" возникает из-за того, что мы группируем данные непосредственно по реквизиту Заказ, который выбирается из разных таблиц.
Вместо этого мы можем сгруппировать запрос по самим физическим документам-регистраторам (в нашем случае это ОказаниеУслуг и РасходнаяНакладная), а уже в цикле обхода получать их реквизит Заказ. Поскольку реквизит будет извлекаться непосредственно из спозиционированного объекта регистратора, платформа вернет нам полноценную ссылку без потери типизации.
Разберем этот вариант по шагам:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса=
"//{{ЗАПРОС(Запрос)
|Период с Дата1 по Дата2;
|Обрабатывать НеПомеченныеНаУдаление;
|ОбрабатыватьДокументы Проведенные;
|Док = Документ.ОказаниеУслуг.ТекущийДокумент,
| Документ.РасходнаяНакладная.ТекущийДокумент;
|Заказ = Документ.ОказаниеУслуг.Заказ,
| Документ.РасходнаяНакладная.Заказ;
|ВидОплаты = Документ.ОказаниеУслуг.Заказ.ВидОплаты,
| Документ.РасходнаяНакладная.Заказ.ВидОплаты;
|Условие (ВидОплаты <> ВидОплатыВнутреннийЗачет);
|Группировка Док; // Группируем по самому документу, а не по его реквизиту
|"//}}ЗАПРОС ;
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
Пока Запрос.Группировка("Док") = 1 Цикл
// Получаем объект документа-основания
ТекДок = Запрос.Док;
// Извлекаем реквизит "Заказ" из реального документа.
// В переменную РеальныйЗаказ запишется полноценная ссылка!
РеальныйЗаказ = ТекДок.Заказ;
// Теперь мы можем без проблем выбирать подчиненные документы
ПодчДок = СоздатьОбъект("Документ");
Если ПодчДок.ВыбратьПодчиненныеДокументы(Дата1, Дата2, РеальныйЗаказ) = 1 Тогда
// Обрабатываем подчиненные документы
Пока ПодчДок.ПолучитьДокумент() = 1 Цикл
Сообщить("Найден подчиненный документ: " + ПодчДок.ТекущийДокумент());
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецЕсли;
Преимущества метода: Этот подход является наиболее "чистым" с точки зрения стандартов разработки 1С. Мы не нагружаем систему лишними операциями сериализации строк и гарантируем корректную типизацию на уровне СУБД.
Существует еще один интересный обходной путь. Поскольку псевдообъект Запрос.Заказ корректно возвращает строковые значения номера (через Заказ.НомерДок), даты (в виде строки в Заказ.ДатаДок) и вида документа (через Заказ.Вид), мы можем использовать стандартный метод позиционирования по номеру.
Для этого нам потребуется создать пустой объект типа "Документ" неопределенного вида и вызвать метод НайтиПоНомеру(), передав туда строковую дату, приведенную к стандартному типу Дата, и строковый вид документа. Проанализируем реализацию:
Пока Запрос.Группировка("Заказ") = 1 Цикл
// Преобразуем строковую дату "YYYYMMDD" в стандартную дату 1С
// 1С 7.7 умеет автоматически приводить строку вида "ДД.ММ.ГГГГ" или "ГГГГММДД" к типу Дата
РеальнаяДата = Дата(Запрос.Заказ.ДатаДок);
НомерДокумента = Запрос.Заказ.НомерДок;
ВидДокумента = Запрос.Заказ.Вид;
// Создаем объект "Документ" неопределенного вида
ЗаказДляПоиска = СоздатьОбъект("Документ");
// Ищем документ в базе по трем параметрам
Если ЗаказДляПоиска.НайтиПоНомеру(НомерДокумента, РеальнаяДата, ВидДокумента) = 1 Тогда
// Документ найден и успешно спозиционирован!
// Теперь у нас есть полноценный объект, метод ТекущийДокумент() отработает корректно
РеальныйЗаказ = ЗаказДляПоиска.ТекущийДокумент();
// Можем передавать его в любые другие процедуры
ПодчДок = СоздатьОбъект("Документ");
ПодчДок.ВыбратьПодчиненныеДокументы(Дата1, Дата2, РеальныйЗаказ);
КонецЕсли;
КонецЦикла;
Преимущества метода: Позволяет обойти ограничения платформы, если по каким-то причинам использование ЗначениеИзСтрокиВнутр() нежелательно (например, при жестких требованиях к читаемости кода для начинающих разработчиков).
Давайте объединим полученные знания и перепишем исходный код автора (из сообщения №16) с использованием Способа №1 (сериализация), как самого быстрого и не требующего переделки структуры сложного запроса. Мы добавим необходимые проверки на пустое значение, чтобы программа работала стабильно и надежно в любых ситуациях.
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса=
"//{{ЗАПРОС(Запрос)
|Период с Дата1 по Дата2;
|Обрабатывать НеПомеченныеНаУдаление;
|ОбрабатыватьДокументы Проведенные;
|Заказ = Документ.ОказаниеУслуг.Заказ,
| Документ.РасходнаяНакладная.Заказ;
|ВидОплаты = Документ.ОказаниеУслуг.Заказ.ВидОплаты,
| Документ.РасходнаяНакладная.Заказ.ВидОплаты;
|Условие (ВидОплаты <> ВидОплатыВнутреннийЗачет);
|Группировка Заказ;
|"//}}ЗАПРОС ;
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Предупреждение("Ошибка выполнения запроса!");
Возврат;
КонецЕсли;
Пока Запрос.Группировка("Заказ") = 1 Цикл
// Проверяем на пустое значение, чтобы избежать лишней работы
Если ПустоеЗначение(Запрос.Заказ) = 1 Тогда
Продолжить;
КонецЕсли;
// Применяем магию приведения типа через внутреннюю строку
ЧистыйЗаказ = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(Запрос.Заказ));
ПодчДок = СоздатьОбъект("Документ");
// Теперь метод ВыбратьПодчиненныеДокументы отработает без ошибок типизации!
Если ПодчДок.ВыбратьПодчиненныеДокументы(Дата1, Дата2, ЧистыйЗаказ) = 1 Тогда
ДокументПервойОтгрузки = ПолучитьПустоеЗначение("Документ");
Пока ПодчДок.ПолучитьДокумент() = 1 Цикл
Если ПодчДок.ПометкаУдаления() = 1 Тогда
Продолжить;
КонецЕсли;
Если ПодчДок.Проведен() = 0 Тогда
Продолжить;
КонецЕсли;
Если (ПодчДок.Вид() = "ОказаниеУслуг") Или (ПодчДок.Вид() = "РасходнаяНакладная") Тогда
ДокументПервойОтгрузки = ПодчДок.ТекущийДокумент();
Прервать; // Нашли самый ранний документ, прерываем цикл
КонецЕсли;
КонецЦикла;
// Проверяем, нашли ли мы нужный документ отгрузки
Если ПустоеЗначение(ДокументПервойОтгрузки) = 0 Тогда
ДатаДокументаПервойОтгрузки = ДокументПервойОтгрузки.ДатаДок;
// Выполняем дальнейшую бизнес-логику
Сообщить("Для заказа " + ЧистыйЗаказ + " первая отгрузка осуществлена: " + ДатаДокументаПервойОтгрузки);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Использование функции ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр()) полностью решает проблему несовместимости типов. Наш код стал надежным, защищенным от падений и при этом сохранил первоначальную лаконичность и логику работы.