Мне лично очень нравится статья - //infostart.ru/1c/articles/307045/
Также я использовал принципы из библиотеки тестирования YAXUNIT
В комментариях к оригиналу - возникли обоснованные сомнения в рациональности использования объектной схемы запроса, я надеюсь не создавать повод для очередных споров, а попытаться создать более "Юзабельный" пример.
Также прикладываю "упрощенный" граф "Схемы запроса" - сомневаюсь, что им действительно можно, или нужно пользоваться (кажется, намного проще представлять в голове знакомый всем запрос 1С, разбитый на "блоки" соответствующие той или иной процедуре СхемыЗапроса), так что, он тут скорее для красивой картинки.
Не претендую на то, что мое решение лучшее из возможных, руководствуюсь общей идеей о том, что если мы хотим, чтобы будущее было светлым - следует попробовать улучшить что-то. Возможно, вам понравится мое решение, возможно, даст вам пищу для размышления или вдохновение для собственных решений.
Мне понадобятся 2 общих модуля, создающих более удобную "обертку" вокруг типовых функций построения схемы запроса.
1. Общий модуль - ТекучийКод - Серверный, ПовторноеИспользование - на время сеанса.
(Совсем простенький, чтобы сохранять текущий контекст)
Функция Контекст(Строка_УИД) Экспорт
Возврат Новый Структура();
КонецФункции
2. Общий модуль - ОбъектЗапрос - Серверный.
(Обертка, реализующая текучий интерфейс. Он посложнее, однако вникать имеет смысл, только если вы хотите разобраться в принципах самого механизма объектных запросов, можно просто скопировать.)
Общий модуль ОбъектЗапрос
#Область ОбъектнаяМодельЗапроса
Функция СхемаЗапроса(Запрос) Экспорт
СхемаЗапроса = Новый СхемаЗапроса;
СхемаЗапроса.УстановитьТекстЗапроса(Запрос.Текст);
// Хранение контекста, повторным использованием.
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Очистить();
Контекст.Вставить("Запрос", Запрос);
Контекст.Вставить("СхемаЗапроса", СхемаЗапроса);
Контекст.Вставить("Пакет", Контекст.СхемаЗапроса.ПакетЗапросов[0]);
Контекст.Вставить("Выборка", Контекст.Пакет.Операторы[0]);
Контекст.Вставить("ИсточникСхемы", Контекст.Выборка.Источники[0]); //Не уверен, что этот указатель необходим
Возврат ОбъектЗапрос;
КонецФункции
// Устанавливает указатели на пакет по индексу
// + на первую выборку из текущего пакета
// + на первый источник из первой выборки
Функция Пакет(Индекс) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Вставить("Пакет", Контекст.СхемаЗапроса.ПакетЗапросов[Индекс]);
Контекст.Вставить("Выборка", Контекст.Пакет.Операторы[0]);
Контекст.Вставить("ИсточникСхемы", Контекст.Выборка.Источники[0]);
Возврат ОбъектЗапрос;
КонецФункции
Функция ДобавитьПакет(Источник, ПсевдонимИсточника) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
НовыйИндекс = Контекст.СхемаЗапроса.ПакетЗапросов.Количество();
НовыйПакет = Контекст.СхемаЗапроса.ПакетЗапросов.Добавить();
НоваяВыборка = НовыйПакет.Операторы[0]; //Выборка создается автоматически
НовыйИсточник = НоваяВыборка.Источники.Добавить(Источник, ПсевдонимИсточника);
Возврат Пакет(НовыйИндекс); // Переустанавливаем селекторы
КонецФункции
Функция Уничтожить(ИмяТаблицы) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
ЗапросУничтожениеВТ = Контекст.СхемаЗапроса.ПакетЗапросов.Добавить(Тип("ЗапросУничтоженияТаблицыСхемыЗапроса"));
ЗапросУничтожениеВТ.ИмяТаблицы = ИмяТаблицы;
Возврат ОбъектЗапрос;
КонецФункции
// Устанавливает указатель на выборку из текущего пакета по индексу
// + на первый источник из текущей выборки
Функция Выборка(Индекс) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Вставить("Выборка", Контекст.Пакет.Операторы[Индекс]);
Контекст.Вставить("ИсточникСхемы", Контекст.Выборка.Источники[0]);
Возврат ОбъектЗапрос;
КонецФункции
// Устанавливает указатель на источник по индексу
Функция Источник(Индекс) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Вставить("ИсточникСхемы", Контекст.Выборка.Источники[Индекс]);
Возврат ОбъектЗапрос;
КонецФункции
Функция ДобавитьПоле(Поле, Псевдоним) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
// ++ 001 Добавление поля - в объединение:
Если Контекст.Пакет.Операторы.Количество() > 1 Тогда
// Ищем уже сущестующий индекс по псевдониму, в основной выборке.
Индекс = 0;
Найден = Ложь;
Для Каждого Колонка Из Контекст.Пакет.Колонки Цикл
Если Колонка.Псевдоним = Псевдоним Тогда
Найден = Истина;
Прервать;
КонецЕсли;
Индекс = Индекс+1;
КонецЦикла;
Выражение = Контекст.Выборка.ВыбираемыеПоля.Добавить(Поле);
// Сопоставляем
Контекст.Пакет.Колонки[Индекс].Поля.Установить(1, Выражение);
// Если не нашли сопоставление, значит это новое поле
Если Не Найден Тогда
Контекст.Пакет.Колонки[Индекс].Псевдоним = Псевдоним;
КонецЕсли;
Возврат ОбъектЗапрос;
КонецЕсли; // -- 001
ИндексНового = Контекст.Выборка.ВыбираемыеПоля.Количество();
Выражение = Контекст.Выборка.ВыбираемыеПоля.Добавить(Поле);
// Псевдонимы для колонок - указываются на уровне запроса
Контекст.Пакет.Колонки[ИндексНового].Псевдоним = Псевдоним;
Возврат ОбъектЗапрос;
КонецФункции
Функция УдалитьПоле(Псевдоним) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Индекс = 0;
Для Каждого Колонка Из Контекст.Пакет.Колонки Цикл
Если Колонка.Псевдоним = Псевдоним Тогда
Контекст.Выборка.ВыбираемыеПоля.Удалить(Индекс);
Прервать;
КонецЕсли;
Индекс = Индекс+1;
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
// Можно было бы назвать это "ДобавитьОтбор" но Где писать короче, и, кажется, очевиднее
Функция ДобавитьГде(Условие) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Выборка.Отбор.Добавить(Условие);
Возврат ОбъектЗапрос;
КонецФункции
Функция УдалитьГде(Условие) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Индекс = 0;
Для Каждого Элемент Из Контекст.Выборка.Отбор Цикл
Если Строка(Элемент) = СокрЛП(Условие) Тогда
Контекст.Выборка.Отбор.Удалить(Индекс);
Прервать;
КонецЕсли;
Индекс = Индекс+1;
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
Функция Разрешенные(Да=Истина) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Пакет.ВыбиратьРазрешенные = Да;
Возврат ОбъектЗапрос;
КонецФункции
Функция Различные(Да=Истина) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Выборка.ВыбиратьРазличные = Да;
Возврат ОбъектЗапрос;
КонецФункции
Функция Первые(Количество=1000) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Выборка.КоличествоПолучаемыхЗаписей = Количество;
Возврат ОбъектЗапрос;
КонецФункции
Функция Индекс(ПоляИндекса) Экспорт
Поля = СтрРазделить(ПоляИндекса, ",", Ложь);
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Для Каждого Поле из Поля Цикл
Контекст.Пакет.Индекс.Добавить( Контекст.Пакет.Колонки.Найти(СокрЛП(Поле)) );
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
Функция Группировка(ПоляГруппировки) Экспорт
Поля = СтрРазделить(ПоляГруппировки, ",", Ложь);
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Для Каждого Поле из Поля Цикл
Контекст.Выборка.Группировка.Добавить(СокрЛП(Поле));
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
Функция Порядок(ПоляПорядка) Экспорт
Поля = СтрРазделить(ПоляПорядка, ",", Ложь);
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Для Каждого Поле из Поля Цикл
Контекст.Пакет.Порядок.Добавить( Контекст.Пакет.Колонки.Найти(СокрЛП(Поле)) );
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
Функция ИтогиОбщие(Да = Истина) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Пакет.ОбщиеИтоги = Да;
Возврат ОбъектЗапрос;
КонецФункции
// Возможно тут тоже можно искать по псевдонимам, но в группе итогов используются не псевдонимы - это может запутать пользователя?
Функция ИтогиПо(ИндексыПолей) Экспорт
Индексы = СтрРазделить(ИндексыПолей, ",", Ложь);
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Для Каждого Инд из Индексы Цикл
Контекст.Пакет.ВыраженияИтогов.Добавить( Контекст.Пакет.Колонки[Число(Инд)] );
КонецЦикла;
Возврат ОбъектЗапрос;
КонецФункции
Функция ИтогТолькоИерархия(ИмяПоля) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Итог = Контекст.Пакет.КонтрольныеТочкиИтогов.Добавить( Контекст.Пакет.Колонки.Найти(ИмяПоля) );
Итог.ТипКонтрольнойТочки = ТипКонтрольнойТочкиСхемыЗапроса.ТолькоИерархия;
Возврат ОбъектЗапрос;
КонецФункции
Функция Поместить(ИмяВременнойТаблицы) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Пакет.ТаблицаДляПомещения = ИмяВременнойТаблицы;
Возврат ОбъектЗапрос;
КонецФункции
// Переключает селектор выборки!
Функция ДобавитьОбъединение(Источник, ПсевдонимИсточника) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Выборка = Контекст.Пакет.Операторы.Добавить();
Контекст.Вставить("Выборка", Выборка);
НовыйИсточник = Выборка.Источники.Добавить(Источник, ПсевдонимИсточника);
Контекст.Вставить("ИсточникСхемы", НовыйИсточник);
Возврат ОбъектЗапрос;
КонецФункции
Функция ДобавитьЛевоеСоединение(К, НовыйИсточник, Как, ОбъединитьПо) Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Источник = Контекст.Выборка.Источники.НайтиПоПсевдониму(К);
НовыйИсточник = Контекст.Выборка.Источники.Добавить(НовыйИсточник, Как);
НовыйИсточник.Соединения.Очистить(); //Удаляем автогенерацию
Источник.Соединения.Добавить(НовыйИсточник, ОбъединитьПо);
Контекст.Вставить("ИсточникСхемы", НовыйИсточник);
Возврат ОбъектЗапрос;
КонецФункции
Функция УстановитьПараметрыОбороты(НачалоПериода="", КонецПериода="", Периодичность="", Условие="") Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.ИсточникСхемы.Источник.Параметры[0].Выражение = Новый ВыражениеСхемыЗапроса(НачалоПериода);
Контекст.ИсточникСхемы.Источник.Параметры[1].Выражение = Новый ВыражениеСхемыЗапроса(КонецПериода);
Контекст.ИсточникСхемы.Источник.Параметры[2].Выражение = Новый ВыражениеСхемыЗапроса(Периодичность);
Контекст.ИсточникСхемы.Источник.Параметры[3].Выражение = Новый ВыражениеСхемыЗапроса(Условие);
Возврат ОбъектЗапрос;
КонецФункции
Функция Завершить() Экспорт
Контекст = ТекучийКод.Контекст("ОбъектныеЗапросы");
Контекст.Запрос.Текст = Контекст.СхемаЗапроса.ПолучитьТекстЗапроса();
КонецФункции
#КонецОбласти
Попробуем воссоздать запрос из оригинальной статьи, используя меньше кода. Поскольку использование механизма я вижу скорее в модификации существующих запросов, точка входа - минимальный запрос, который мы "дособерем" в процессе:
ТестЗапрос = Новый Запрос;
ТестЗапрос.Текст =
"ВЫБРАТЬ
| Товары.Ссылка КАК Номенклатура
|ИЗ
| Справочник.Номенклатура КАК Товары";
ОбъектЗапрос.СхемаЗапроса(ТестЗапрос)
.Пакет(0).Выборка(0)
.Разрешенные()
.ДобавитьЛевоеСоединение("Товары", "РегистрНакопления.Закупки.Обороты", "Закупки", "Закупки.Номенклатура = Товары.Ссылка")
.УстановитьПараметрыОбороты("&Начало", "&Окончание", "Месяц")
.ДобавитьПоле("Закупки.Период", "Период")
.ДобавитьПоле("ЕСТЬNULL(Закупки.СуммаОборот, 0)", "СуммаЗакупок")
.ДобавитьПоле("0", "СуммаПродаж")
.Поместить("ТаблицаОбороты")
.ДобавитьГде("НЕ Товары.ЭтоГруппа")
.ДобавитьОбъединение("Справочник.Номенклатура", "Товары")
.Различные().Первые(100)
.ДобавитьЛевоеСоединение("Товары", "РегистрНакопления.Продажи.Обороты", "Продажи", "Продажи.Номенклатура = Товары.Ссылка")
.УстановитьПараметрыОбороты("&Начало", "&Окончание", "Месяц")
.ДобавитьПоле("Товары.Ссылка", "Номенклатура") // Объединяется по псевдониму
.ДобавитьПоле("Продажи.Период", "Период")
.ДобавитьПоле("0", "СуммаЗакупок")
.ДобавитьПоле("ЕСТЬNULL(Продажи.СуммаОборот, 0)", "СуммаПродаж")
.ДобавитьГде("НЕ Товары.ЭтоГруппа")
.Индекс("Номенклатура, Период")
.ДобавитьПакет("ТаблицаОбороты", "ТаблицаОбороты")
.ДобавитьПоле("ТаблицаОбороты.Номенклатура", "Номенклатура")
.ДобавитьПоле("ТаблицаОбороты.Период", "Период")
.ДобавитьПоле("СУММА(ТаблицаОбороты.СуммаЗакупок)", "СуммаЗакупок")
.ДобавитьПоле("СУММА(ТаблицаОбороты.СуммаПродаж)", "СуммаПродаж")
.Группировка("ТаблицаОбороты.Номенклатура, ТаблицаОбороты.Период")
.ДобавитьГде("СУММА(ТаблицаОбороты.СуммаЗакупок) > 0") // Говорят схема запроса сама определит раздел "ИМЕЮЩИЕ"?
.Порядок("Номенклатура, Период")
.ИтогиОбщие()
.ИтогиПо("2, 3")
.ИтогТолькоИерархия("Номенклатура")
.Уничтожить("ТаблицаОбороты")
.Завершить();
Обратите внимание, что форматирование отступов - произвольное, я оформил так, как удобнее читать лично мне, надеюсь и вам тоже.
Идеи для улучшения:
В данный момент селекторы реализованы через индексы, в случае с большими запросами - это может быть не слишком удобно. Кажется, лучше было бы реализовать поиск по псевдонимам, это должно быть более читаемо, когда мы будем анализировать код изменяющий запрос. Нам ведь важно, что именно изменилось? Сравните варианты:
Выборка(0).ДобавитьПоле("Закупки.Период", "Период")
Выборка("Товары").ДобавитьПоле("Закупки.Период", "Период")
Да и в целом код, наверное, можно еще причесать, у меня, к сожалению, нет много времени на это, надо работать :)
Надеюсь, что это станет хорошей "точкой отсчета", если вы захотите использовать этот функционал.