При разработке и доработке конфигураций на платформе «1С:Предприятие 8» программисты часто сталкиваются с задачей обработки данных из табличной части (ТЧ) документа. Начинающие разработчики нередко совершают типичную ошибку: они пишут простой запрос к табличной части документа, забывая, что физически в базе данных таблицы ТЧ всех документов этого типа хранятся вместе. В результате запрос возвращает строки из абсолютно всех существующих в системе документов данного вида.
В этой статье мы подробно разберем, как правильно ограничить выборку в запросе текущим документом, рассмотрим частые синтаксические ошибки, проанализируем проблему несохраненных данных и изучим альтернативные способы решения этой задачи без обращения к СУБД.
Давайте проанализируем структуру хранения данных в 1С. Любой документ, имеющий табличную часть, на уровне базы данных (MS SQL Server, PostgreSQL и др.) представляется в виде нескольких таблиц. Первая таблица — это шапка документа (содержит реквизиты самого документа: дату, номер, контрагента и т.д.). Вторая и последующие таблицы хранят строки его табличных частей. Эти таблицы ТЧ содержат служебную колонку Ссылка, которая связывает каждую строку с конкретным документом-владельцем.
Если мы пишем запрос к табличной части без секции ГДЕ, платформа 1С транслирует это в SQL-запрос, который считывает абсолютно все записи из физической таблицы ТЧ. Чтобы получить данные только "нашего" документа, нам необходимо жестко указать условие отбора по полю Ссылка.
Разберем самый популярный и правильный с точки зрения работы с базой данных способ. Чтобы ограничить запрос рамками одного документа, мы должны передать ссылку на этот документ в качестве параметра запроса.
Рассмотрим пример кода, который решает эту задачу:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказНаПроизводствоРесурсы.РесурсПредприятия КАК Сотрудник,
| ЗаказНаПроизводствоРесурсы.Мощность КАК ПроцентПремии
|ИЗ
| Документ.ЗаказНаПроизводство.РесурсыПредприятия КАК ЗаказНаПроизводствоРесурсы
|ГДЕ
| ЗаказНаПроизводствоРесурсы.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
ТаблицаРезультата = Запрос.Выполнить().Выгрузить();
Объект.НачислениеПремии.Загрузить(ТаблицаРезультата);
Давайте разберем этот код по шагам, чтобы понять логику его работы:
ИЗ мы указываем источник данных — табличную часть РесурсыПредприятия документа ЗаказНаПроизводство. Для удобства таблице назначен псевдоним ЗаказНаПроизводствоРесурсы.ГДЕ мы прописываем условие фильтрации: ЗаказНаПроизводствоРесурсы.Ссылка = &Ссылка. Символ амперсанда (&) указывает системе, что слово Ссылка — это именованный параметр, значение которого будет передано позже.УстановитьПараметр() мы связываем параметр "Ссылка" в тексте запроса с реальным значением реквизита Объект.Ссылка текущего документа.Выполнить().Выгрузить() выполняет запрос и возвращает результат в виде объекта ТаблицаЗначений.Загрузить() заполняет другую табличную часть (НачислениеПремии) полученными данными, автоматически сопоставляя имена колонок.Описанный выше классический метод запроса имеет один критически важный недостаток, о котором обязан знать каждый разработчик. Запрос всегда обращается напрямую к базе данных (СУБД), а не к оперативной памяти компьютера!
Из-за этого архитектурного поведения платформы возникают две серьезные проблемы:
Объект.Ссылка будет пустым (Объект.Ссылка.Пустая() = Истина). Запрос к базе данных по пустой ссылке вернет абсолютно пустой результат, так как в СУБД записей этого документа еще физически не существует.Как же решить эту проблему, если нам обязательно нужно использовать запрос (например, для соединения табличной части с другими таблицами или регистрами), но при этом учесть текущие, еще не сохраненные изменения пользователя?
Решением является передача ТЧ в запрос в качестве временной таблицы. Мы берем данные из оперативной памяти клиента/сервера, выгружаем их в таблицу значений и передаем в менеджер временных таблиц запроса:
// 1. Выгружаем данные табличной части из оперативной памяти в Таблицу Значений
ТЗ_Ресурсы = Объект.РесурсыПредприятия.Выгрузить();
// 2. Создаем запрос и менеджер временных таблиц
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// 3. Помещаем ТЗ во временную таблицу внутри запроса
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаРесурсов.РесурсПредприятия КАК Сотрудник,
| ТаблицаРесурсов.Мощность КАК ПроцентПремии
|ПОМЕСТИТЬ ВТ_Ресурсы
|ИЗ
| &ТаблицаРесурсов КАК ТаблицаРесурсов";
Запрос.УстановитьПараметр("ТаблицаРесурсов", ТЗ_Ресурсы);
// Выполняем пакет запросов (помещение в ВТ)
Запрос.Выполнить();
// 4. Теперь мы можем работать с ВТ_Ресурсы, соединяя ее с любыми таблицами СУБД
// Например, выберем данные из временной таблицы
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Ресурсы.Сотрудник,
| ВТ_Ресурсы.ПроцентПремии
|ИЗ
| ВТ_Ресурсы КАК ВТ_Ресурсы";
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Объект.НачислениеПремии.Загрузить(РезультатЗапроса);
Этот способ гарантирует, что алгоритм будет одинаково успешно работать как для старых сохраненных документов, так и для новых, только что созданных пользователем на форме объектов.
Зачастую разработчики злоупотребляют запросами там, где они избыточны. Если перед вами стоит задача просто перенести данные из одной табличной части текущего документа в другую ТЧ того же документа (как в примере на форуме), обращаться к базе данных через запрос не имеет практического смысла. Это создает лишнюю нагрузку на сервер СУБД.
Гораздо проще и производительнее обработать данные в оперативной памяти. Рассмотрим два способа реализации.
Этот вариант идеален, если имена колонок в табличных частях отличаются, или требуется дополнительная логика обработки строк "на лету":
// Очищаем целевую ТЧ перед заполнением
Объект.НачислениеПремии.Очистить();
// Обходим строки исходной ТЧ в оперативной памяти
Для Каждого СтрокаТЧ Из Объект.РесурсыПредприятия Цикл
НоваяСтрока = Объект.НачислениеПремии.Добавить();
// Ручное сопоставление колонок с разными именами
НоваяСтрока.Сотрудник = СтрокаТЧ.РесурсПредприятия;
НоваяСтрока.ПроцентПремии = СтрокаТЧ.Мощность;
КонецЦикла;
Если имена колонок и их типы в обеих табличных частях полностью совпадают, перенос данных можно выполнить буквально в одну строку:
Объект.НачислениеПремии.Загрузить(Объект.РесурсыПредприятия.Выгрузить());
Этот метод работает мгновенно, так как преобразование данных происходит непосредственно в памяти, минуя фазы трансляции SQL-запроса, обращения к сетевому сокету СУБД и чтения дисковой подсистемы сервера.
Давайте проанализируем частые ошибки, которые совершают программисты при реализации подобных задач.
Ошибка 1. Попытка сравнения с Объект.Ссылка внутри текста запроса.
В Сообщении 9 упоминается неудачная попытка написать условие вида: ЗаказНаПроизводствоРесурсыПредприятия.Ссылка = Объект.Ссылка непосредственно в теле запроса. Почему это вызывает ошибку компиляции? Текст запроса — это строка, передаваемая СУБД. База данных ничего не знает о контексте исполнения кода в 1С и переменных вроде Объект.Ссылка. Для передачи значений из кода 1С в СУБД используются исключительно параметры, предваряемые символом &.
Ошибка 2. Опечатка в проверке на пустую ссылку.
В Сообщении 8 предлагается проверка: Если Не Объект.Ссылка.Пустая Тогда. Встроенный язык 1С жестко требует скобок при вызове методов объектов. Метод Пустая() — это функция ссылочного типа, поэтому правильный синтаксис выглядит так:
Если Не Объект.Ссылка.Пустая() Тогда // Обязательны круглые скобки
// Код выполнения запроса
КонецЕсли;
Если код выполняется в модуле самого объекта (а не в форме или внешней обработке), для проверки новизны документа правильнее и лаконичнее использовать метод:
Если Не ЭтоНовый() Тогда
// Код выполнения запроса
КонецЕсли;
Необходимо помнить, где именно выполняется ваш код. В современных управляемых формах 1С реквизит Объект имеет тип ДанныеФормыСтруктура, а его табличные части — тип ДанныеФормыКоллекция.
На клиенте (в процедурах с директивой &НаКлиенте) выполнять запросы категорически запрещено платформой. Любые манипуляции с классом Запрос должны происходить строго на сервере (в процедурах &НаСервере или &НаСервереБезКонтекста).
Если вы вызываете серверный метод без контекста (&НаСервереБезКонтекста), то напрямую обратиться к переменной Объект вы не сможете. В этом случае вам необходимо передать ссылку или саму выгруженную таблицу значений с клиента на сервер в качестве параметра процедуры.
В платформе 1С существует еще один интересный синтаксис запросов к табличным частям — обращение к ним как к вложенным таблицам через объект-владелец. Этот способ можно применять, когда документ гарантированно записан в базу данных:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказНаПроизводство.Ссылка КАК ДокументСсылка,
| ЗаказНаПроизводство.РесурсыПредприятия.(
| РесурсПредприятия КАК Сотрудник,
| Мощность КАК ПроцентПремии
| ) КАК ТаблицаРесурсов
|ИЗ
| Документ.ЗаказНаПроизводство КАК ЗаказНаПроизводство
|ГДЕ
| ЗаказНаПроизводство.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
ВыборкаШапки = Запрос.Выполнить().Выбрать();
Пока ВыборкаШапки.Следующий() Цикл
// Получаем вложенный результат запроса по ТЧ
ВыборкаТЧ = ВыборкаШапки.ТаблицаРесурсов.Выбрать();
Пока ВыборкаТЧ.Следующий() Цикл
// Обработка строк табличной части
ТекущийСотрудник = ВыборкаТЧ.Сотрудник;
ТекущаяМощность = ВыборкаТЧ.ПроцентПремии;
КонецЦикла;
КонецЦикла;
Этот подход возвращает одну строку документа (шапку), в одной из колонок которой находится полноценный вложенный результат выборки по строкам табличной части. Несмотря на элегантность синтаксиса, в повседневной практике для простых задач перегрузки данных чаще используют классический плоский запрос к ТЧ, рассмотренный в первом решении.