В своей первой статье по теме кэширования Использование программных перечислений, ч.1: строковые константы я рассказал о способе, который избавит от неприятностей, связанных с сравнением в коде на строку. Здесь на базе этого-же приема рассмотрена задача по оптимизации загрузки данных из внешнего источника. Расскажу о случае, который неоднократно наблюдал в разных его ипостасях. Стоит задача по загрузке данных в БД из внешнего файла, заказчик предоставил пару примеров, программист успешно все реализовал \ протестировал, сдал работу.. И тут через месяц заказчик грузит файл раз эдак в 50 превышающий пример.. Все не просто тупит, а зависает на ~2 часа. Заказчик недоволен. После работ по оптимизации время загрузки снизилось с 2 часов до 30 минут (из которых только 5 это синхронизация, а все остальное - запись данных в базу).. В чем же была ошибка? При синхронизации данных не использовалось кэширование, и поиск ссылок по одним и тем же ключевым полям выполнялся многократно. После того как был добавлен кэш все залетало.
... я добавил глобальную переменную типа "Структура", которая и выполняет роль кэша:
#Область ОписаниеПеременных
Перем мСтруктураКэшДанных;
#КонецОбласти
//...
Процедура ЗагрузитьДанные(ТаблицаДанныеФайла)
//...
КонецПроцедуры
//...
#Область Инициализация
мСтруктураКэшДанных = Новый Структура;
#КонецОбласти
Расскажу далее о нескольких универсальных методах, которые полезно применять в подобных случаях.
Синхронизация входящих данных с данными базы - один из самых болезненных этапов при написании любых обменов / загрузок. Особенно, если не кэшировать результаты поиска. Хочу погрузить читателя в один душераздирающий пример:
Процедура ЗагрузитьДанные(ТаблицаДанныеФайла)
Для каждого СтрокаТЗ Из ТаблицаДанныеФайла Цикл
//...
ФизЛицо = НайтиЭлементСправочникаПоРеквизиту("ФизическиеЛица", "ИНН", СтрокаТЗ.ИННФизЛица);
Организация = НайтиЭлементСправочникаПоРеквизиту("Организации", "Наименование", СтрокаТЗ.Организация);
СотрудникОрганизации = НайтиСотрудникаОрганизации(Организация, ФизЛицо);
//...
КонецЦикла;
КонецПроцедуры
Функция НайтиЭлементСправочникаПоРеквизиту(ИмяСправочника, ИмяРеквизита, ЗначениеРеквизита)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| "+ ИмяСправочника +".Ссылка
|ИЗ
| Справочник."+ ИмяСправочника +" КАК "+ ИмяСправочника +"
|ГДЕ
| НЕ "+ ИмяСправочника +".ПометкаУдаления
| И "+ ИмяСправочника +"."+ ИмяРеквизита +" = &ЗначениеРеквизита";
Запрос.УстановитьПараметр("ЗначениеРеквизита", ЗначениеРеквизита);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.Ссылка;
КонецЕсли;
Возврат Справочники[ИмяСправочника].ПустаяСсылка();
КонецФункции
Функция НайтиСотрудникаОрганизации(Организация, ФизЛицо)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СотрудникиОрганизаций.Ссылка
|ИЗ
| Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
|ГДЕ
| СотрудникиОрганизаций.Физлицо = &Физлицо
| И СотрудникиОрганизаций.Организация = &Организация
| И НЕ СотрудникиОрганизаций.ПометкаУдаления";
Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Физлицо", Физлицо);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.Ссылка;
КонецЕсли;
Возврат Справочники.СотрудникиОрганизаций.ПустаяСсылка();
КонецФункции
Теперь представьте, что в ТаблицаДанныеФайла 1000 строк, а физ. лиц- всего 10. Т.е. запрос в цикле по поиску физ. лица выполняется лишних 990 раз... Есть два решения:
- самый быстрый - это сделать пред-выборку по физ.лицам и организациям, передав туда массивы поисковых реквизитов. В цикле по основной таблице поиск ссылки выполнять уже по заранее полученным выборкам, где хранится соответствие <ПоисковыйРеквизит> - <Найденная или пустая ссылка> (этот способ рассмотрен ниже, в главе 2)
- чуть менее быстрый, но более читабельный вариант - который я выбираю из-за простоты и скорости реализации - это использовать кэширующий поиск. Все входящие данные загоняем в кэш, и перед тем как выполнить запрос проверяем - был ли он выполнен ранее с параметром конкретного поискового реквизита. Таким образом каждая ссылка ищется только один раз, а впоследствии получается из кэша.
Функция НайтиЭлементСправочникаПоРеквизиту(ИмяСправочника, ИмяРеквизита, ЗначениеРеквизита)
Перем КэшСвязи;
КлючСвязи = "СтруктураПоискаЭлемента_" + ИмяСправочника + "_" + ИмяРеквизита;
Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, КэшСвязи) Тогда
// В качестве кэша используется соответствие
КэшСвязи = Новый Соответствие;
мСтруктураКэшДанных.Вставить(КлючСвязи, КэшСвязи);
КонецЕсли;
Результат = КэшСвязи.Получить(ЗначениеРеквизита);
Если Результат = Неопределено Тогда
Запрос = Новый Запрос;
Запрос.Текст = // в запросе добавил предложение ВЫБРАТЬ 1
"ВЫБРАТЬ ПЕРВЫЕ 1
| "+ ИмяСправочника +".Ссылка
|ИЗ
| Справочник."+ ИмяСправочника +" КАК "+ ИмяСправочника +"
|ГДЕ
| НЕ "+ ИмяСправочника +".ПометкаУдаления
| И "+ ИмяСправочника +"."+ ИмяРеквизита +" = &ЗначениеРеквизита";
Запрос.УстановитьПараметр("ЗначениеРеквизита", ЗначениеРеквизита);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Результат = Справочники[ИмяСправочника].ПустаяСсылка();
Иначе
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Результат = Выборка.Ссылка;
КонецЕсли;
КэшСвязи.Вставить(ЗначениеРеквизита, Результат);
КонецЕсли;
Возврат Результат;
КонецФункции
Функция НайтиСотрудникаОрганизации(Организация, ФизЛицо)
Перем ТаблицаСотрудников;
КлючСвязи = "НайтиСотрудникаОрганизации"; // Ключ кэша следует всегда задавать по имени метода
// если бы здесь использовалось несколько кэшей
// это означало бы что их надо разнести по разным
// методам. Так необходимо делать, чтобы контролировать
// уникальность ключа кэша по телу модуля.
// Например инициализацию таблицы сотрудников можно было бы вынести
// в метод "ТаблицаСотрудниковПовтИсп". Но она нужна только
// в одном этом месте, с прикладной т.з. выносить нецелесообразно
// На входе два ссылочных реквизита - соответствие использовать неудобно, в качестве кэша применяется таблица значений:
Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, ТаблицаСотрудников) Тогда
ТаблицаСотрудников = Новый ТаблицаЗначений;
ТаблицаСотрудников.Колонки.Добавить("Организация");
ТаблицаСотрудников.Колонки.Добавить("ФизЛицо");
ТаблицаСотрудников.Колонки.Добавить("Сотрудник");
мСтруктураКэшДанных.Вставить(КлючСвязи, ТаблицаСотрудников);
КонецЕсли;
Поиск = Новый Структура("Организация, ФизЛицо", Организация, ФизЛицо);
СтрокиТаблицы = ТаблицаСотрудников.НайтиСтроки(Поиск);
Если ЗначениеЗаполнено(СтрокиТаблицы) Тогда
Результат = СтрокиТаблицы [0].Сотрудник;
Иначе
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| СотрудникиОрганизаций.Ссылка
|ИЗ
| Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
|ГДЕ
| СотрудникиОрганизаций.Физлицо = &Физлицо
| И СотрудникиОрганизаций.Организация = &Организация
| И НЕ СотрудникиОрганизаций.ПометкаУдаления";
Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Физлицо", Физлицо);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Результат = Справочники.Сотрудники.ПустаяСсылка();
Иначе
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Результат = Выборка.Ссылка;
КонецЕсли;
НоваяСтрока = ТаблицаСотрудников.Добавить();
НоваяСтрока.Организация = Организация;
НоваяСтрока.ФизЛицо = ФизЛицо;
НоваяСтрока.Сотрудник = НайденноеЗначение;
КонецЕсли;
Возврат Результат;
КонецФункции
Уже эта доработка дала существенные результаты, о которых я сказал выше. Такой подход экономит время поиска, и при том выглядит читабельно. В отличие от более "быстрого" способа с пред-выборками - в кавычках быстрого, поскольку временной выигрыш пропорционален разреженности значений поиска. Если у нас 1000 строк, в которых два контрагента - быстрее будет работать кэширующий поиск. Если на 1000 строк есть 200 уникальных контрагентов - то быстрее пред-выборка, но тут надо учитывать нагрузку на память. Окончательный вариант подбирается в процессе оптимизации, если есть достаточно примеров - оба показывают хорошее время, я обычно работаю именно с вторым способом.
Но это еще не все:) Представьте что в цикле далее идут такие строки:
Процедура ЗагрузитьДанные(ТаблицаДанныеФайла)
Для каждого СтрокаТЗ Из ТаблицаДанныеФайла Цикл
//...
ИННКППОрганизации = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Организация, "ИНН, КПП");
Если НЕ ДанныеОрганизацииКорректны(ИННКППОрганизации) Тогда
Продолжить;
КонецЕсли;
//...
КонецЦикла;
КонецПроцедуры
Запрос в цикле все заметили?... Не буду говорить что уж организаций-то на 1000 строк явно немного... Например их в принципе в базе две. Так зачем 998 раз повторять один и тот-же запрос? Оптимизировать этот момент можно используя такую функцию:
// Аналог функции для получения реквизитов, с поддержкой промежуточного кэша
//
// Параметры:
// СсылкаНаОбъект - ЛюбаяСсылка
// ИменаРеквизитовВходящие - Строка - имена реквизитов, разделяемые запятыми
// Кэш - Структура - внешний кэш. Однажды полученное значение реквизита сохраняется в этой переменной,
// и при повторном вызове берется из нее.
//
// Возвращаемое значение:
// Структура, Произвольный - если в параметре "ИменаРеквизитов" передано несколько значений, вернется структура
// с перечнем реквизитов. Иначе - конкретное значение полученого реквизита. Если в качестве параметра "СсылкаНаОбъект"
// передана пустая ссылка, то значения реквизитов берутся "через точку" от пустой ссылки, для типизации.
//
// Пример:
// Несколько реквизитов:
// ИННКПП = РеквизитыСсылки(Контрагент, "ИНН, КПП");
// Один реквизит:
// ИНН = РеквизитыСсылки(Контрагент, "ИНН"); - если перед эти методом был выполнен
// пред. пример, то запроса в базу не будет, значение ИНН возьмется из кэша. Если первый пример вызывать
// после этого, то запрос в базу выполнится - чтобы получить КПП.
//
Функция РеквизитыСсылки(СсылкаНаОбъект, ИменаРеквизитовВходящие) Экспорт
Если НЕ мСтруктураКэшДанных.Свойство("РеквизитыСсылки") Тогда
мСтруктураКэшДанных.Вставить("РеквизитыСсылки", Новый Соответствие);
КонецЕсли;
ИменаРеквизитов = ИменаРеквизитовВходящие;
Пока СтрНайти(ИменаРеквизитов, " ") Цикл
ИменаРеквизитов = СтрЗаменить(ИменаРеквизитов, " ", "");
КонецЦикла;
ДанныеРеквизитов = мСтруктураКэшДанных.РеквизитыОбъектов.Получить(СсылкаНаОбъект);
Если ДанныеРеквизитов = Неопределено ИЛИ ДанныеРеквизитов.ИменаРеквизитов <> ИменаРеквизитов Тогда
Если ДанныеРеквизитов = Неопределено Тогда
ДанныеРеквизитов = Новый Структура("ИменаРеквизитов, Реквизиты");
ДанныеРеквизитов.Реквизиты = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(СсылкаНаОбъект, ИменаРеквизитов);
КэшМодуля.РеквизитыОбъектов.Вставить(СсылкаНаОбъект, ДанныеРеквизитов);
КонецЕсли;
Если ДанныеРеквизитов.ИменаРеквизитов <> ИменаРеквизитов Тогда
ЗапрашиваемыеРеквизиты = Новый Структура(СтрЗаменить(ИменаРеквизитов, ".", ""));
ЗаполнитьЗначенияСвойств(ЗапрашиваемыеРеквизиты, ДанныеРеквизитов.Реквизиты);
ЭтоПустаяИлиБитаяСсылка = НЕ ОбщегоНазначения.СсылкаСуществует(СсылкаНаОбъект);
ЭтоБитаяСсылка = ЭтоПустаяИлиБитаяСсылка И ЗначениеЗаполнено(СсылкаНаОбъект);
Если ЭтоБитаяСсылка Тогда
ПустаяСсылка = ОбщегоНазначения.МенеджерОбъектаПоСсылке(СсылкаНаОбъект).ПустаяСсылка();
ИначеЕсли ЭтоПустаяИлиБитаяСсылка Тогда
ПустаяСсылка = СсылкаНаОбъект;
КонецЕсли;
ОбновитьКэшРеквизитов = Ложь;
СписокИменРеквизитов = СтрРазделить(ИменаРеквизитов, ",");
Для каждого ИмяРеквизита Из СписокИменРеквизитов Цикл
ИмяРеквизита = СокрЛП(ИмяРеквизита);
Алиас = СтрЗаменить(ИмяРеквизита, ".", "");
ЗначениеРеквизита = ЗапрашиваемыеРеквизиты[Алиас];
Если ЗначениеРеквизита = Неопределено Тогда
Если ЭтоПустаяИлиБитаяСсылка Тогда
ПутиРеквизита = СтрРазделить(ИмяРеквизита, ".");
ЗначениеРеквизита = ПустаяСсылка;
Для Каждого ЧастьПути Из ПутиРеквизита Цикл
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ЗначениеРеквизита, ЧастьПути) Тогда
ЗначениеРеквизита = ЗначениеРеквизита[ЧастьПути];
ИначеЕсли НРег(ЧастьПути) = "представление" Тогда
ЗначениеРеквизита = "";
Прервать;
Иначе
Прервать;
КонецЕсли;
КонецЦикла;
ДанныеРеквизитов.Реквизиты.Вставить(Алиас, ЗначениеРеквизита);
Иначе
ОбновитьКэшРеквизитов = Истина;
Прервать;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если ОбновитьКэшРеквизитов Тогда
ЗапрашиваемыеРеквизиты = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(СсылкаНаОбъект, ИменаРеквизитов);
ОбщегоНазначенияКлиентСервер.ДополнитьСтруктуру(ДанныеРеквизитов.Реквизиты, ЗапрашиваемыеРеквизиты, Истина);
КонецЕсли;
ДанныеРеквизитов.ИменаРеквизитов = ИменаРеквизитов;
КонецЕсли;
КонецЕсли;
Если ДанныеРеквизитов.Реквизиты.Количество() = 1 ИЛИ СтрНайти(ИменаРеквизитов, ",") = 0 Тогда
Результат = ДанныеРеквизитов.Реквизиты[СтрЗаменить(ИменаРеквизитов, ".", "")];
Иначе
Результат = ДанныеРеквизитов.Реквизиты;
КонецЕсли;
Возврат Результат;
КонецФункции
На выходе получим код:
ИННКППОрганизации = РеквизитыСсылки(Организация, "ИНН, КПП");
// Или, если нужен один реквизит:
ИННОрганизации = РеквизитыСсылки(Организация, "ИНН");
// Замечу, что если сначала получались значения нескольких реквизитов, а потом отдельно одного из них
// - то уже сработает кэш. То-же самое действует и наоборот:
ИННОрганизации = РеквизитыСсылки(Организация, "ИНН");
КППОрганизации = РеквизитыСсылки(Организация, "КПП");
ИННКППОрганизации = РеквизитыСсылки(Организация, "ИНН, КПП"); // Работает кэш по ранее найденным реквизитам
ДанныеОрганизации = РеквизитыСсылки(Организация, "ИНН, КПП, ОГРН"); // А вот тут уже понадобится кэш обновлять, будет новый запрос
В предыдущей главе я упоминал что поиск по заранее подготовленному массиву данных зачастую эффективнее, чем кэширующий поиск. Я намеренно пока не употребляю термин "Выборка", поскольку здесь есть такие варианты:
2.1 Загнать саму исходную таблицу в запрос (колонки должны быть заранее протипизированы), в запросе левым соединением по синхронизирующим полям получать ссылки из нужных таблиц. Выглядит это так:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаДанныеФайла.НаименованиеОрганизации,
| ТаблицаДанныеФайла.ИННФизЛица,
| ТаблицаДанныеФайла.ПрочиеПоля
|ПОМЕСТИТЬ ДаныеФайла
|ИЗ
| &ТаблицаДанныеФайла КАК ТаблицаДанныеФайла
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ДаныеФайла.НаименованиеОрганизации,
| ДаныеФайла.ИННФизЛица,
| ДаныеФайла.ПрочиеПоля,
| ЕСТЬNULL(Организации.Ссылка, ЗНАЧЕНИЕ(Справочник.Организации.ПустаяСсылка)) КАК Организация,
| ЕСТЬNULL(ФизическиеЛица.Ссылка, ЗНАЧЕНИЕ(Справочник.ФизическиеЛица.ПустаяСсылка)) КАК Контрагент,
| ЕСТЬNULL(СотрудникиОрганизаций.Ссылка, ЗНАЧЕНИЕ(Справочник.СотрудникиОрганизаций.ПустаяСсылка)) КАК Сотрудник
|ИЗ
| ДаныеФайла КАК ДаныеФайла
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Организации КАК Организации
| ПО ДаныеФайла.НаименованиеОрганизации = Организации.Наименование
| И (НЕ Организации.ПометкаУдаления)
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ФизическиеЛица КАК ФизическиеЛица
| ПО ДаныеФайла.ИННФизЛица = ФизическиеЛица.ИНН
| И (НЕ ФизическиеЛица.ПометкаУдаления)
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
| ПО (СотрудникиОрганизаций.Организация = Организации.Ссылка)
| И (СотрудникиОрганизаций.Физлицо = ФизическиеЛица.Ссылка)";
Запрос.УстановитьПараметр("ТаблицаДанныеФайла", ТаблицаДанныеФайла);
РезультатЗапроса = Запрос.Выполнить();
//...
Этот способ плох тем, что исходная таблица может быть очень объемной - кстати, при написании любых загрузок следует исходить именно из этого принципа - что исходный пример может быть увеличен в десятки раз. Если загружается некий excell на 100000 позиций, то время при таком подходе тратится на сбор данных в промежуточную таблицу значений ТаблицаДанныеФайла - которая сама по себе занимает порядком памяти, а также на ее помещение в временную таблицу запроса. Использование любого подхода с пред-выборкой связано с риском утечек памяти. Я однажды видел загрузку прайс-листа, реализованную так, что людям приходилось по ночам перезагружать сервер 1С. Вывод: лучше 10 быстрых подзапросов, чем один но объемный.
2.2 Более щадящий память метод связан с использованием метода выборки "НайтиСледующий", и в общем-то это глава как-раз о нем) Поскольку он не всегда применим, а при загрузке данных из объемного внешнего источника - по моему опыту не применим вовсе. Поясню что имею в виду, на примере:
Процедура ЗагрузитьДанные(ТаблицаДанныеФайла)
Запрос = Новый Запрос;
// Используем запрос с менеджером, чтобы искать сотрудников по ранее найденным физЛицам и организациям
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
РезультатЗапросаФизЛица = ДанныеФизЛиц(Запрос, ТаблицаДанныеФайла);
ВыборкаПоФизЛицам = РезультатЗапросаФизЛица.Выбрать();
РезультатЗапросаОрганизации = ДанныеОрганизаций(Запрос, ТаблицаДанныеФайла);
ВыборкаПоОрганизациям = РезультатЗапросаОрганизации.Выбрать();
РезультатЗапросаСотрудники = ДанныеСотрудников(Запрос);
ВыборкаПоСотрудникам = РезультатЗапросаСотрудники.Выбрать();
Для каждого СтрокаТЗ Из ТаблицаДанныеФайла Цикл
//..
ФизЛицо = Справочники.ФизическиеЛица.ПустаяСсылка();
Организация = Справочники.Организации.ПустаяСсылка();
СотрудникОрганизации = Справочники.СотрудникиОрганизаций.ПустаяСсылка();
ПоискФизЛица = Новый Структура("ИНН", СтрокаТЗ.ИННФизЛица);
Если ВыборкаПоФизЛицам.НайтиСледующий(ПоискФизЛица) Тогда
ФизЛицо = ВыборкаПоФизЛицам.Ссылка;
ВыборкаПоФизЛицам.Сбросить();
КонецЕсли;
ПоискОрганизации = Новый Структура("Наименование", СтрокаТЗ.НаименованиеОрганизации);
Если ВыборкаПоОрганизациям.НайтиСледующий(ПоискОрганизации) Тогда
Организация = ВыборкаПоОрганизациям.Ссылка;
ВыборкаПоОрганизациям.Сбросить();
КонецЕсли;
ПоискСотрудника = Новый Структура("ФизЛицо, Организация", ФизЛицо, Организация);
Если ВыборкаПоСотрудникам.НайтиСледующий(ПоискСотрудника) Тогда
Сотрудник = ВыборкаПоСотрудникам.Ссылка;
ВыборкаПоСотрудникам.Сбросить();
КонецЕсли;
//..
КонецЦикла;
КонецПроцедуры
Функция ДанныеФизЛиц(Запрос, ТаблицаДанныеФайла)
// Получение пула поисковых реквизитов
МассивИннФизЛиц = ТаблицаДанныеФайла.ВыгрузитьКолонку("ИННФизЛица");
МассивИннФизЛиц = ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивИннФизЛиц);
// Помещение найденных ссылок в временную таблицу, чтобы далее использовать ее для поиска сотрудника
Запрос.Текст =
"ВЫБРАТЬ
| ФизическиеЛица.Ссылка,
| ФизическиеЛица.ИНН
|ПОМЕСТИТЬ ФизЛица
|ИЗ
| Справочник.ФизическиеЛица КАК ФизическиеЛица
|ГДЕ
| ФизическиеЛица.ИНН В(&МассивИннФизЛиц)
| И НЕ ФизическиеЛица.ПометкаУдаления
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ФизЛица.Ссылка,
| ФизЛица.ИНН
|ИЗ
| ФизЛица КАК ФизЛица";
Запрос.УстановитьПараметр("МассивИннФизЛиц", МассивИннФизЛиц);
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса;
КонецФункции
Функция ДанныеОрганизаций(Запрос, ТаблицаДанныеФайла)
// Получение пула поисковых реквизитов
МассивНаименованийОрганизаций = ТаблицаДанныеФайла.ВыгрузитьКолонку("НаименованиеОрганизации");
МассивНаименованийОрганизаций = ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивНаименованийОрганизаций);
// Помещение найденных ссылок в временную таблицу, чтобы далее использовать ее для поиска сотрудника
Запрос.Текст =
"ВЫБРАТЬ
| Организации.Ссылка,
| Организации.Наименование
|ПОМЕСТИТЬ Организации
|ИЗ
| Справочник.Организации КАК Организации
|ГДЕ
| Организации.Наименование В(&МассивНаименованийОрганизаций)
| И НЕ Организации.ПометкаУдаления
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Организации.Ссылка,
| Организации.Наименование
|ИЗ
| Организации КАК Организации";
Запрос.УстановитьПараметр("МассивНаименованийОрганизаций", МассивНаименованийОрганизаций);
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса;
КонецФункции
Функция ДанныеСотрудников(Запрос)
Запрос.Текст =
"ВЫБРАТЬ
| СотрудникиОрганизаций.Ссылка,
| СотрудникиОрганизаций.Физлицо,
| СотрудникиОрганизаций.Организация
|ИЗ
| Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Организации КАК Организации
| ПО СотрудникиОрганизаций.Организация = Организации.Ссылка
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ФизЛица КАК ФизЛица
| ПО СотрудникиОрганизаций.Физлицо = ФизЛица.Ссылка";
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса;
КонецФункции
Этот способ сэкономит память, но не время поиска. По опыту - суммарное выполнение методов "НайтиСледующий" может занимать объем времени, в десятки раз превышающий затраты на сам запрос! Дело здесь в том, что каждый раз после выполнения удачного поиска используется сброс выборки, и каждый раз поиск начинается с начала - с самой первой строки. Отсюда следует что при больших объемах это использовать не стоит. Предупреждая вопрос "а если выгрузить результаты в таблицу значений, и искать по ней" - то же самое. ТЗ можно проиндексировать при больших объемах, но стабильное хорошее время это не обеспечит: во-первых таблица будет занимать память, во вторых - поиск без индекса будет затратен при большой выборке данных, а при малой выборке будут затраты на само построение индекса.
При всем при том - метод НайтиСледующий() очень хорош, когда необходимо связать данные двух запросов. Ведь если обеспечить одинаковый порядок поисковых реквизитов - то можно отказаться от сброса выборки, и каждый новый поиск будет выполнятся с текущего положения курсора - а это хороший выигрыш в скорости, по сравнению с составным поиском по таблице значений, например. Его я использую при написании сложных отчетов - когда нет возможности получить все данные за один заход. Тогда чтобы декомпозировать код, и обеспечить хорошую скорость отчета можно получить такой пример:
Функция ОтчетПоПартиям()
// Допустим у номенклатуры есть доп. свойство "АктивнаяПартия", и в отчете нужно отображать характеристики тех позиций,
// активная партия которых заполнена и есть на остатках. Это легко решается в один запрос, но здесь только
// ради демонстрации метода "НайтиСледующий()" открою две выборки.
// На практике - условие было более жестким, даже вспоминать его не буду) И решение в один запрос здоровы бы усложнило
// читабельность кода. Поэтому я пожертвовал небольшой толикой производительности - ради возможности быстро разобраться
// в ситуации, в случае сбоя.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ЗначенияСвойствОбъектов.Объект КАК Номенклатура,
| ЗначенияСвойствОбъектов.Значение КАК Партия
|ПОМЕСТИТЬ АктивныеПартии
|ИЗ
| РегистрСведений.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
|ГДЕ
| ЗначенияСвойствОбъектов.Свойство = &СвойствоАктивнаяПартия
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| АктивныеПартии.Номенклатура КАК Номенклатура,
| АктивныеПартии.Партия КАК Партия
|ИЗ
| АктивныеПартии КАК АктивныеПартии
|
|УПОРЯДОЧИТЬ ПО
| Номенклатура,
| Партия";
Запрос.УстановитьПараметр("СвойствоАктивнаяПартия", ПланыВидовХарактеристик.СвойстваОбъектов.АктивнаяПартия);
РезультатЗапросаПоСвойству = Запрос.Выполнить();
ВыборкаПоСвойству = РезультатЗапросаПоСвойству.Выбрать();
РезультатЗапросаПартий = ПартииНоменклатуры(Запрос);
ВыборкаПартий = РезультатЗапросаПартий.Выбрать();
Пока ВыборкаПоСвойству.Следующий() Цикл
ПоискПартии = Новый Структура("Номенклатура, Партия", ВыборкаПоСвойству.Номенклатура, ВыборкаПоСвойству.Партия);
Пока ВыборкаПартий.НайтиСледующий(ПоискПартии) Цикл
//... действия с результатом поиска
КонецЦикла;
// Т.к. обе выборки одинаково отсортированы по синхронизирующим полям - выборку не надо сбрасывать.
КонецЦикла;
КонецФункции
Функция ПартииНоменклатуры(Запрос)
Запрос.Текст =
"ВЫБРАТЬ
| ПартииТоваровНаСкладахОстатки.ДокументОприходования КАК ДокументОприходования КАК Партия,
| ПартииТоваровНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ПартииТоваровНаСкладахОстатки.ХарактеристикаНоменклатуры
|ИЗ
| РегистрНакопления.ПартииТоваровНаСкладах.Остатки(
| &ДатаОтчета,
| (Номенклатура, ДокументОприходования) В
| (ВЫБРАТЬ
| АктивныеПартии.Номенклатура,
| АктивныеПартии.Партия
| ИЗ
| АктивныеПартии КАК АктивныеПартии)) КАК ПартииТоваровНаСкладахОстатки
|
|УПОРЯДОЧИТЬ ПО
| Номенклатура,
| ДокументОприходования";
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса;
КонецФункции
И тут возникает такой вопрос. Получается - если в 2.2 отсортировать выходные поля таким образом, чтобы синхронизация была одинакова - то можно пользоваться поиском по выборке? Да, но выполняется несколько поисков, и использовать сортировку исходного набора можно только для одного из них:
// Можно выполнять поиск без сброса выборки по ИННФизЛица, но НаименованиеОрганизации отсортирована внутри него - и поиск по ней без сброса будет некорректный
ТаблицаДанныеФайла.Сортировать("ИННФизЛица, НаименованиеОрганизации");
// Можно выполнять поиск без сброса выборки по НаименованиеОрганизации, но ИННФизЛица отсортирована внутри него - и поиск по ней без сброса будет некорректный
ТаблицаДанныеФайла.Сортировать("НаименованиеОрганизации, ИННФизЛица");
А если выборка сбрасывается хоть раз - ее использование уже неэффективно.
Хочу добавить кое-что о сортировке таблицы значений по колонкам с ссылочными значениями. Фраза с ИТС "...рекомендуется... в остальных случаях – сортировать «по ссылке», а не по представлению. Для этого в методе Сортировать следует использовать объект СравнениеЗначений " ( ИТС: Сортировка строк таблиц значений). Но на произвольных наборах данных я заметил что такая сортировка по ссылочным полям не идет с сортировкой в запросе! Возможно я чего-то не допонял, но вот такой код некорректен:
Функция ОтчетПоПартиям()
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ЗначенияСвойствОбъектов.Объект КАК Номенклатура,
| ЗначенияСвойствОбъектов.Значение КАК Партия
|ПОМЕСТИТЬ АктивныеПартии
|ИЗ
| РегистрСведений.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
|ГДЕ
| ЗначенияСвойствОбъектов.Свойство = &СвойствоАктивнаяПартия
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| АктивныеПартии.Номенклатура КАК Номенклатура,
| АктивныеПартии.Партия КАК Партия
|ИЗ
| АктивныеПартии КАК АктивныеПартии";
Запрос.УстановитьПараметр("СвойствоАктивнаяПартия", ПланыВидовХарактеристик.СвойстваОбъектов.АктивнаяПартия);
РезультатЗапросаПоСвойству = Запрос.Выполнить();
// Здесь не буду открывать выборку, а выгружу результат в таблицу, и ее отсортирую
ТаблицаСвойств = РезультатЗапросаПоСвойству.Выгрузить();
ТаблицаСвойств.Сортировать("Номенклатура, Партия", Новый СравнениеЗначений);
РезультатЗапросаПартий = ПартииНоменклатуры(Запрос);
ВыборкаПартий = РезультатЗапросаПартий.Выбрать();
Для каждого СтрокаСвойства Из ТаблицаСвойств Цикл
ПоискПартии = Новый Структура("Номенклатура, Партия", СтрокаСвойства.Номенклатура, СтрокаСвойства.Партия);
Пока ВыборкаПартий.НайтиСледующий(ПоискПартии) Цикл
//... действия с результатом поиска
КонецЦикла;
// Несмотря на использование объекта "Сравнение значений" для сортировки ссылочных полей таблицы значений по УИД-у
// выходной порядок данных ТаблицаСвойств и ВыборкаПартий может отличаться, а значит здесь поиск отработает некорректно.
КонецЦикла;
КонецФункции
Функция ПартииНоменклатуры(Запрос)
Запрос.Текст =
"ВЫБРАТЬ
| ПартииТоваровНаСкладахОстатки.ДокументОприходования КАК ДокументОприходования КАК Партия,
| ПартииТоваровНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ПартииТоваровНаСкладахОстатки.ХарактеристикаНоменклатуры
|ИЗ
| РегистрНакопления.ПартииТоваровНаСкладах.Остатки(
| &ДатаОтчета,
| (Номенклатура, ДокументОприходования) В
| (ВЫБРАТЬ
| АктивныеПартии.Номенклатура,
| АктивныеПартии.Партия
| ИЗ
| АктивныеПартии КАК АктивныеПартии)) КАК ПартииТоваровНаСкладахОстатки
|
|УПОРЯДОЧИТЬ ПО
| Номенклатура,
| ДокументОприходования";
РезультатЗапроса = Запрос.Выполнить();
Возврат РезультатЗапроса;
КонецФункции
В заключение этой главы скажу что наиболее оптимально при загрузке данных из внешних источников использовать именно кэширующий поиск. Он может породить множество подзапросов, и в каких-то случаях быть менее эффективным чем поиск по пред-выборке - но чаще всего это оправдано за счет экономии памяти, а также более читабельного кода. Кроме того - для поиска по пред-выборке необходим некий пул данных, т.е. сначала обходим Excell, загоняем данные в таблицу значений (!память!) а далее используем эту таблицу для получения пулов поисковых реквизитов. Использование кэширующего поиска позволяет выполнить синхронизацию непосредственно из самой выборки по источнику (например при обходе результата ADO-запроса).
Кэшировать можно и нужно не только данные для синхронизации, но некоторые константные вспомогательные переменные. Благодаря функции "Счета<Префикс по плану счетов>()" новая переменная добавляется простым редактированием строки кодов счетов:
// Используемые управленческие счета
//
// Возвращаемое значение:
// ФиксированнаяСтруктура Из КлючИЗначение:
// * Ключ - Строка - идентификатор счета, образованный от его кода через замену точки на нижний слеш, и добавкой
// префикса "сч". Т.е. "60.2" = "сч60_2"
// * Значение - ПланСчетовСсылка.Управленческий
//
Функция СчетаУпр()
Перем Результат;
Ключ = "СчетаУпр";
мСтруктураКэшДанных.Свойство(Ключ, Результат);
Если Результат = Неопределено Тогда
СчетаУпр = Новый Структура;
// Чтобы добавить счет, добавьте сюда его код
СтрокаКодовСчетов = "10, 10.3, 19, 60.1, 60.2, 62, 51, 51, 70, 70.1, 76.АВ, 90, 90.01, 90.02";
МассивКодовСчетов = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаКодовСчетов, ", ");
Для каждого КодСчета Из МассивКодовСчетов Цикл
КлючСчета = "сч" + СтрЗаменить(КодСчета, ".", "_");
СчетаУпр.Вставить(КлючСчета, ПланыСчетов.Управленческий.ПустаяСсылка());
КонецЦикла;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Управленческий.Ссылка,
| Управленческий.Код
|ИЗ
| ПланСчетов.Управленческий КАК Управленческий
|ГДЕ
| Управленческий.Код В(&МассивКодовСчетов)";
Запрос.УстановитьПараметр("МассивКодовСчетов", МассивКодовСчетов);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
КлючСчета = "сч" + СтрЗаменить(Выборка.Код, ".", "_");
Если ЗначениеЗаполнено(Выборка.Ссылка) Тогда
СчетаУпр.Вставить(КлючСчета, Выборка.Ссылка);
КонецЕсли;
КонецЦикла;
Результат = Новый ФиксированнаяСтруктура(СчетаУпр);
мСтруктураКэшДанных.Вставить(Ключ, Результат);
КонецЕсли;
Возврат Результат;
КонецФункции
// Пример использования:
СчетаУпр = СчетаУпр();
Если ВыборкаПоОстаткам.Счет = СчетаУпр.сч90_01 Тогда
//...
ИначеЕсли ВыборкаПоОстаткам.Счет = СчетаУпр.сч90_02 Тогда
//...
КонецЕсли;
Какие плюсы:
- Это гораздо быстрее, чем писать ПланыСчетов.Управленческий.<ИмяПредопределенногоСчета>
- Имя предопределенного элемента еще надо узнать - я не встречал таких гуру, которые по коду счета помнят идентификатор предопределенного элемента. Обычно в голове сидит именно код счета, а идентификатор узнается через предопределенный список элементов плана счетов, т.е. туда надо провалиться, найти код счета, скопировать имя... - долго, добавить код в строку гораздо быстрее.
- Счет может и не быть предопределенным. В таких случаях часто используют конструкцию "НайтиПоКоду", причем вызывают ее десять раз подряд, при чем не гнушаются циклом.... Мрак:)
Довольно часто по счету группе необходимо получить список подчиненных ему субсчетов, чтобы обращаться к ним в коде.
// Субсчета указанной группы счетов
//
// Параметры:
// СчетГруппа - ПланСчетовСсылка.Управленческий
//
// Возвращаемое значение:
// ФиксированнаяСтруктура Из КлючИЗначение:
// * Ключ - Строка - идентификатор счета, образованный от его кода через замену точки на нижний слеш, и добавкой
// префикса "сч". Т.е. "60.2" = "сч60_2"
// * Значение - ПланСчетовСсылка.Управленческий
//
Функция СубсчетаСчета(СчетГруппа)
Перем КэшСвязи;
КлючСвязи = "СубсчетаСчета";
Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, КэшСвязи) Тогда
КэшСвязи = Новый Соответствие;
мСтруктураКэшДанных.Вставить(КлючСвязи, КэшСвязи);
КонецЕсли;
Результат = КэшСвязи.Получить(СчетГруппа);
Если Результат = Неопределено Тогда
СубсчетаСчета = Новый Структура;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Управленческий.Ссылка,
| Управленческий.Код
|ИЗ
| ПланСчетов.Управленческий КАК Управленческий
|ГДЕ
| Управленческий.Ссылка В ИЕРАРХИИ(&СчетГруппа)
| И НЕ Управленческий.ЗапретитьИспользоватьВПроводках";
Запрос.УстановитьПараметр("СчетГруппа", СчетГруппа);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
КлючСчета = "сч" + СтрЗаменить(Выборка.Код, ".", "_");
СубсчетаСчета.Вставить(КлючСчета, Выборка.Ссылка);
КонецЦикла;
Результат = Новый ФиксированнаяСтруктура(СубсчетаСчета);
КэшСвязи.Вставить(СчетГруппа, Результат);
КонецЕсли;
Возврат Результат;
КонецФункции
// Пример использования
СчетаУпр = СчетаУпр();
Субсчета76 = СубсчетаСчета(СчетаУпр.сч76);
//...
Запрос.УстановитьПараметр("сч76_11", Субсчета76.сч76_11);
//...
Также бывает необходимо получить массив субсчетов, чтобы передать их в параметр запроса, или проверить - является ли счет субсчетом
// Список субсчетов счета
//
// Параметры:
// СчетГруппа - ПланСчетовСсылка.Управленческий
//
// Возвращаемое значение:
// ФиксированныйМассив Из ПланСчетовСсылка.Управленческий
//
Функция СписокСубсчетовСчета(СчетГруппа)
Перем КэшСвязи;
КлючСвязи = "СписокСубсчетовСчета";
Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, КэшСвязи) Тогда
КэшСвязи = Новый Соответствие;
мСтруктураКэшДанных.Вставить(КлючСвязи, КэшСвязи);
КонецЕсли;
Результат = КэшСвязи.Получить(СчетГруппа);
Если Результат = Неопределено Тогда
Субсчета = Новый Массив;
СтруктураСубсчета = СубсчетаСчета(СчетГруппа);
Для каждого КлючИзначение Из СтруктураСубсчета Цикл
Субсчета.Добавить(КлючИзначение.Значение);
КонецЦикла;
Результат = Новый ФиксированныйМассив(Субсчета);
КэшСвязи.Вставить(СчетГруппа, Результат)
КонецЕсли;
Возврат Результат;
КонецФункции
/// Пример использования: допустим понадобилось выбрать остатки по счетам в иерархии 76 и 58, причем необходимо исключить
// из выборки счет 76.11. Тормознутость конструкции "В ИЕРАРХИИ" давно известна, как и способ ее лечения - все субсчета
// необходимо получить заранее, и передать параметром в запрос.
СчетаУпр = СчетаУпр();
СписокСубсчетов = Новый Массив;
Субсчета76 = СписокСубсчетовСчета(СчетаУпр.сч76);
ОбщегоНазначенияКлиентСервер.ДополнитьМассив(СписокСубсчетов , Субсчета76);
ОбщегоНазначенияКлиентСервер.УдалитьЗначениеИзМассива(СписокСубсчетов, СчетаУпр.сч76_11);
Субсчета58 = СписокСубсчетовСчета(СчетаУпр.сч58);
ОбщегоНазначенияКлиентСервер.ДополнитьМассив(СписокСубсчетов, Субсчета58);
Запрос.УстановитьПараметр("СписокСубсчетов", СписокСубсчетов);
// После осуществления выборки необходимо выполнить какие-то действия, в зависимости от того, подчинен ли текущий счет остатков счету 76:
ЭтоСубсчетСчета76 = Субсчета76.Найти(Выборка.СчетОстатков) <> Неопределено;
Если ЭтоСубсчетСчета76 Тогда
//...
КонецЕсли;
//...
В той самой ситуации, о которой я рассказал в начале статьи, в некоторой части кода требовалось выполнять действия в зависимости от того использовался ли на счете некий вид субконто. Что сделал программист? Получил объект счета, и проверял его табличную часть "ВидыСубконто". Что сделал я - да то же самое, только без получения объекта, и с использованием кэшируемого поиска:
// Определяет наличие вида субконто на управленческом счете
//
// Параметры:
// Счет - ПланСчетовСсылка.Управленческий
// ВидСубконто - ПланВидовХарактеристикСсылка.ВидыСубконтоУпр
//
// Возвращаемое значение:
// Булево - Истина, если на указанном счете есть указанный вид субконто
//
Функция ЕстьВидСубконтоНаСчете(Счет, ВидСубконто)
Перем КэшТаблица;
КлючСвязи = "ЕстьВидСубконтоНаСчете";
Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, КэшТаблица) Тогда
КэшТаблица = Новый ТаблицаЗначений;
КэшТаблица.Колонки.Добавить("Счет");
КэшТаблица.Колонки.Добавить("ВидСубконто");
КэшТаблица.Колонки.Добавить("ЕстьВидСубконтоНаСчете");
мСтруктураКэшДанных.Вставить(КлючСвязи, КэшТаблица);
КонецЕсли;
Поиск = Новый Структура("Счет, ВидСубконто", Счет, ВидСубконто);
СтрокиКэша = КэшТаблица.НайтиСтроки(Поиск);
Если ЗначениеЗаполнено(СтрокиКэша) Тогда
Результат = СтрокиКэша[0].ЕстьВидСубконтоНаСчете;
Иначе
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ИСТИНА КАК ЕстьВидСубконтоНаСчете
|ИЗ
| ПланСчетов.Управленческий.ВидыСубконто КАК УправленческийВидыСубконто
|ГДЕ
| УправленческийВидыСубконто.Ссылка = &Счет
| И УправленческийВидыСубконто.ВидСубконто = &ВидСубконто";
Запрос.УстановитьПараметр("ВидСубконто", ВидСубконто);
Запрос.УстановитьПараметр("Счет", Счет);
РезультатЗапроса = Запрос.Выполнить();
Результат = НЕ РезультатЗапроса.Пустой();
НоваяСтрока = КэшТаблица.Добавить();
НоваяСтрока.Счет = Счет;
НоваяСтрока.ВидСубконто = ВидСубконто;
НоваяСтрока.ЕстьВидСубконтоНаСчете = Результат;
КонецЕсли;
Возврат Результат;
КонецФункции
При использовании этого подхода нередко возникает ситуация, когда надо организовать кэш из соответствия. см. методы работы с планом счетов СубсчетаСчета, СписокСубсчетовСчета.
Для упрощения можно добавить следующий метод в клиент-серверный модуль:
// Организация кэша соответствий
//
// Параметры:
// КэшЛокальный - Структура
// КлючСвязи - Строка
// Измерение - Произвольный
//
// Возвращаемое значение:
// Соответствие
//
// Пример использования:
// При необходимости организовать кэш в виде соответствия можно заменить конструкцию:
//
// Перем КэшСвязи;
// КлючСвязи = "ИмяМетода";
// Если НЕ мСтруктураКэшДанных.Свойство(КлючСвязи, КэшСвязи) Тогда
// КэшСвязи = Новый Соответствие;
// мСтруктураКэшДанных.Вставить(КлючСвязи, КэшСвязи);
// КонецЕсли;
//
// На:
// КэшСвязи = МодульКлиентСервер.КэшСвязи(мСтруктураКэшДанных, "ИмяМетода");
//
// Параметр "Измерение" используется для организации более глубой вложенности кэша соответствий, что иногда бывает
// нужно.
//
Функция КэшСвязи(КэшЛокальный, КлючСвязи, Измерение = Неопределено) Экспорт
Перем Связь;
Если НЕ КэшЛокальный.Свойство(КлючСвязи, Связь) Тогда
Связь = Новый Соответствие();
КэшЛокальный.Вставить(КлючСвязи, Связь);
КонецЕсли;
Если Измерение = Неопределено Тогда
Результат = Связь;
Иначе
Результат = Связь.Получить(Измерение);
Если Результат = Неопределено Тогда
Результат = Новый Соответствие();
Связь.Вставить(Измерение, Результат);
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции
Порой возникает ситуация, когда надо кэшировать некоторые методы в общих модулях. Это легко сделать, добавив в метод параметр "Кэш", и передавать туда локальную мСтруктураКэшДанных. Однако тогда возникает проблема вероятности пересечения ключей.
Например. При работе с банковской выпиской надо получить описание контрагента из объекта описания выписки. У него есть некий УИД, скажем ИНН+КПП. В выписке много документов, где этот контрагент встречается. А описание надо получать повсеместно по конфигурации, т.е. метод просится в общий модуль. Но если просто разместить туда этот метод с передачей локального кэша, то может возникнуть ситуация, что есть локальный метод "ОписаниеКонтрагента", использующий кэш. А есть такой же метод общего модуля. И ключ в разрезе которого хранится значение в локальном кэше - одинаковый. Поэтому, чтобы обратиться к методу общего модуля в кэше надо дополнительно добавить разрез. Я это делаю через вспомогательную структуру, которая создается из метода ниже:
// Используется при работе с методами, поддерживающими промежуточное кэширование
// Создает и возвращает подраздел, идентифицирующй общий модуль в переданном кэше.
//
// Параметры:
// Идентификатор - Строка - имя общего модуля, как он задан в конфигураторе
// Кэш - Структура - внешний кэш
//
// Возвращаемое значение:
// Структура
//
// Пример использования:
//
// Функция МетодЛокальный()
// Результат = ОбщийМодуль.ОписаниеКонтрагента(ИсточникОписания, мСтруктураКэшДанных);
// Возврат Результат;
// КонецФункции
//
// Метод модуля ОбщийМодуль:
// Функция ОписаниеКонтрагента(ИсточникОписания, Кэш)
// КэшМодуля = МодульКлиентСервер.КэшМодуля(Кэш);
// КэшСвязи = МодульКлиентСервер.КэшСвязи(КэшМодуля, "ОписаниеКонтрагента");
//
// Результат = КэшСвязи.Получить(ИсточникОписания.ИдКонтрагента);
// Если Результат = Неопределено Тогда
// Результат = получаем результат метода...
// КэшСвязи.Вставить(ИсточникОписания.ИдКонтрагента, Результат);
// КонецЕсли;
//
// Возврат Результат;
//
// КонецФункции
//
Функция КэшМодуля(Идентификатор, Кэш) Экспорт
Перем Результат;
Ключ = "ОбщийМодуль_" + Идентификатор;
Если НЕ Кэш.Свойство(Ключ, Результат) Тогда
Результат = Новый Структура;
Кэш.Вставить(Ключ, Результат);
КонецЕсли;
Возврат Результат;
КонецФункции
По кэшированию методов модуля также замечу - что на самом деле эта ситуация специфична, и ее стоит избегать, чтобы не плодить лишние параметры в библиотечном API. Этот подход использую только для служебного программного интерфейса, и весьма ограниченно.
И напоследок упомяну опасные нюансы. При работе с этим подходом могут возникнуть некоторые соблазны, которые ведут по кривой дорожке, и которых нельзя допускать.
1) Желание использовать в качестве локального кэша модуля объекта его "ДополнительныеСвойства". Ни в коем случае так не делайте. Локальный кэш должен оставаться *локальным*. иначе потом кто-то долго будет курить код какой-нибудь подписки на событие, и соображать что же там происходит, и зачем в доп. свойствах столько ключей.
2) Желание разорвать кэширование. Пример описан ниже, здесь дополню: необходимо помнить что локальный кэш режется ключом, и если этот ключ где-то задублируется, то начнется трэш. С этой точки зрения представленный концепт кэширования - это граната для неопытного разработчика. Единственный верный способ такой ситуации избежать - помнить про два правила:
- Ключ всегда объявляется однократно.
- Ключ всегда именуется по имени метода
Тогда никаких проблем не будет.
Перем мСтруктураКэшДанных;
// Неправильно: разрывать кэширующий метод нельзя
Процедура СложныйАлгоритм1()
// рассчитываем какое-то сложное условие...
РезультатСложногоРасчета = ....
// И помещаем в глобальный кэш:
мСтруктураКэшДанных.Вставить("РезультатСложногоРасчета", РезультатСложногоРасчета);
КонецПроцедуры
Процедура СложныйАлгоритм2()
// А здесь проверяем:
РезультатСложногоРасчета = Неопределено;
Если НЕ мСтруктураКэшДанных.Свойство("РезультатСложногоРасчета", РезультатСложногоРасчета) Тогда
СложныйАлгоритм1();
КонецЕсли;
РезультатСложногоРасчета = мСтруктураКэшДанных.РезультатСложногоРасчета;
КонецПроцедуры
// Правильно: вынести сложный расчет в свой метод
Процедура СложныйАлгоритм1()
РезультатСложногоРасчета = РезультатСложногоРасчета();
КонецПроцедуры
Процедура СложныйАлгоритм2()
РезультатСложногоРасчета = РезультатСложногоРасчета();
КонецПроцедуры
Функция РезультатСложногоРасчета();
Перем Результат;
Ключ = "РезультатСложногоРасчета";
мСтруктураКэшДанных.Свойство(Ключ, Результат);
Если Результат = Неопределено Тогда
// рассчитываем какое-то сложное условие...
Результат =
мСтруктураКэшДанных.Вставить(Ключ, Результат);
КонецЕсли;
Возврат Результат;
КонецФункции