Чтение наборов записей запросом

02 ноября 2015

О чем речь

При работе с наборами записей регистров часто необходимо выполнять чтение их текущего состояния с учетом установленных отборов. Эти операции выполняются при проведении документов, выгрузке наборов регистров в XML при обмене, а также многие другие задачи, требующие манипуляций с записями регистров.Движения документа

Сегодня рассмотрим стандартный и альтернативный ему способ чтения наборов записей регистров, а также пример использования нестандартного подхода работы с наборами записей. Для примеров будем использовать типовую конфигурацию "Управление торговлей" ред. 11.1.

Стандартный подход

Стандартный подход чтения записей набора регистра сводится к использованию в объектной технике метода "Прочитать()". Например, чтобы прочитать набор записей регистра "ДвиженияНоменклатураНоменклатура" для документа "Реализация товаров и услуг" воспользуемся следующим кодом:

	НаборДвиженияНоменклатураНоменклатура = РегистрыНакопления.ДвиженияНоменклатураНоменклатура.СоздатьНаборЗаписей();
	// "Документ" - ссылка на документ регистратор
	НаборДвиженияНоменклатураНоменклатура.Отбор.Регистратор.Установить(Документ);
	НаборДвиженияНоменклатураНоменклатура.Прочитать();

Переменная "Документ" содержит ссылку на документ "Реализация товаров и услуг". После вызова метода "Прочитать()" набор будет заполнен записями регистра, которые сформировал документ реализации.

Заполненный набор движений регистра

Все довольно просто. После заполнения набора можно изменять его данные, добавлять или удалять записи. Для сохранения изменений достаточно вызвать метод "Записать()", который инициирует сохранение в базу данных.

Чтение набора запросом

Предлагаю также альтернативный метод заполнения наборов записей регистров, который основывается на использовании собственного запроса для получения данных и последующее заполнение набора при обходе выборки результата этого запроса. Если изменить предыдущий пример, то код будет следующий:

	НаборДвиженияНоменклатураНоменклатура = РегистрыНакопления.ДвиженияНоменклатураНоменклатура.СоздатьНаборЗаписей();
	// "Документ" - ссылка на документ регистратор
	НаборДвиженияНоменклатураНоменклатура.Отбор.Регистратор.Установить(Документ);
	// Вместо вызова метода "Прочитать()" используем собственную процедуру
	ПрочитатьНаборРегистраЗапросом(НаборДвиженияНоменклатураНоменклатура);

Результат будет абсолютно таким же, как и при использовании объектной техники. Процедура "ПрочитатьНаборРегистраЗапросом" имеет следующий алгоритм:

Процедура ПрочитатьНаборРегистраЗапросом(НаборРегистра) Экспорт
     
    // Получаем имя типа регистра и имя его метаданных
    МетаданныеРегистра = НаборРегистра.Метаданные();   
    ВидОбъекта = Неопределено;
	ЭтоРегистрБухгалтерии = Ложь;
    Если ОбщегоНазначения.ЭтоРегистрНакопления(МетаданныеРегистра) Тогда
        ВидОбъекта = "РегистрНакопления";
    ИначеЕсли ОбщегоНазначения.ЭтоРегистрСведений(МетаданныеРегистра) Тогда
        ВидОбъекта = "РегистрСведений";
    ИначеЕсли ОбщегоНазначения.ЭтоРегистрБухгалтерии(МетаданныеРегистра) Тогда
        ВидОбъекта = "РегистрБухгалтерии";
		ЭтоРегистрБухгалтерии = Истина;
    ИначеЕсли ОбщегоНазначения.ЭтоРегистрРасчета(МетаданныеРегистра) Тогда
        ВидОбъекта = "РегистрРасчета";
    КонецЕсли;
     
    // Если переданный объект не является регистром, то чтение не выполняем
    Если ВидОбъекта = Неопределено Тогда
        Возврат;
    КонецЕсли;
     
    // Формируем текст запроса для получения записей регистра
    ЗапросДанныхРегистра = Новый Запрос;
	Если ЭтоРегистрБухгалтерии Тогда
	    ЗапросДанныхРегистра.Текст =
	        "Выбрать * 
	        |   Из " + ВидОбъекта + "." + МетаданныеРегистра.Имя + ".ДвиженияССубконто(, , {УсловияОтбора}, , )
	        |ГДЕ
	        | {УсловияОтбора}";
	Иначе
	    ЗапросДанныхРегистра.Текст =
	        "Выбрать * 
	        |   Из " + ВидОбъекта + "." + МетаданныеРегистра.Имя + "
	        |ГДЕ
	        | {УсловияОтбора}";
	КонецЕсли;
    // Условия отбора регистра формируем отдельно по коллекции элементов
    // отбора переданного набора записей
    ТекстОтбора = "";
    Для Каждого ЭлОтбора Из НаборРегистра.Отбор Цикл
        ОбработатьУсловиеОтбора(ЭлОтбора, ЗапросДанныхРегистра, ТекстОтбора);          
    КонецЦикла;
    Если ЗапросДанныхРегистра.Параметры.Количество() > 0 Тогда
        ЗапросДанныхРегистра.Текст = СтрЗаменить(ЗапросДанныхРегистра.Текст, "{УсловияОтбора}", ТекстОтбора);
    Иначе
        ЗапросДанныхРегистра.Текст = СтрЗаменить(ЗапросДанныхРегистра.Текст, "{УсловияОтбора}", " ИСТИНА ");       
    КонецЕсли;
     
    // Получаем результат запроса в виде выборки и заполняем
    // набор записей
    ВыборкаДанныхРегистра = ЗапросДанныхРегистра.Выполнить().Выбрать();
    НаборРегистра.Очистить();
	Если ЭтоРегистрБухгалтерии Тогда
		Пока ВыборкаДанныхРегистра.Следующий() Цикл
			// Заполняем таблицу движений
			НоваяЗапись = НаборРегистра.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяЗапись, ВыборкаДанныхРегистра);
			// Заполняем таблицу субконто
			ЕстьПоляСубконто = Истина;
			НомерСубконто = 1;
			Пока ЕстьПоляСубконто Цикл
				ИмяПоляСубконтоКт = "СубконтоКт"+Формат(НомерСубконто,"ЧГ=0");
				ИмяПоляСубконтоДт = "СубконтоДт"+Формат(НомерСубконто,"ЧГ=0");
				ЕстьСубконтоКт = СодержитСвойство(ВыборкаДанныхРегистра, ИмяПоляСубконтоКт);
				ЕстьСубконтоДт = СодержитСвойство(ВыборкаДанныхРегистра, ИмяПоляСубконтоДт);
				Если ЕстьСубконтоДт ИЛИ ЕстьСубконтоКт Тогда
					Если ЕстьСубконтоДт Тогда
						ЗначениеСубконто = ВыборкаДанныхРегистра[ИмяПоляСубконтоДт];
						ВидСубконто = ВыборкаДанныхРегистра["Вид"+ИмяПоляСубконтоДт];
						НоваяЗапись.СубконтоДт[ВидСубконто] = ЗначениеСубконто;	
					КонецЕсли;
					Если ЕстьСубконтоКт Тогда
						ЗначениеСубконто = ВыборкаДанныхРегистра[ИмяПоляСубконтоКт];
						ВидСубконто = ВыборкаДанныхРегистра["Вид"+ИмяПоляСубконтоКт];
						НоваяЗапись.СубконтоКт[ВидСубконто] = ЗначениеСубконто;	
					КонецЕсли;
				Иначе
					ЕстьПоляСубконто = Ложь;	
				КонецЕсли;
				НомерСубконто = НомерСубконто + 1;
			КонецЦикла;			
		КонецЦикла;
	Иначе
   		Пока ВыборкаДанныхРегистра.Следующий() Цикл
	        НовСтр = НаборРегистра.Добавить();
	        ЗаполнитьЗначенияСвойств(НовСтр, ВыборкаДанныхРегистра);
		КонецЦикла;
	КонецЕсли;
     
КонецПроцедуры
 
// Формируем текст условия запроса с учетом элементов отбора регистра, а также
// устанавливаем все необходимые параметры запроса
//  Параметры:
//      ЭлОтбора - элемент обора из коллекции "НаборЗаписейРегистра.Отбор"
//      ЗапросДанныхРегистра - объект типа "Запрос", с помощью которого будет 
//          выполнено получение данных записей регистра
//      ТекстОтбора - переменная, в которую будут записаны сформированные
//          условия запроса строкой
//
Функция ОбработатьУсловиеОтбора(ЭлОтбора, ЗапросДанныхРегистра, ТекстОтбора)
     
    ТекстОтбора = ?(ЗапросДанныхРегистра.Параметры.Количество() > 0, " И ", "") +
        ТекстОтбора +
        ЭлОтбора.ПутьКДанным;
         
    УсловиеОтбора = " = ";
    Если ЭлОтбора.ВидСравнения = ВидСравнения.Больше Тогда
        УсловиеОтбора = " > (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.БольшеИлиРавно Тогда
        УсловиеОтбора = " >= (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ВИерархии Тогда
        УсловиеОтбора = " В ИЕРАРХИИ (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ВСписке Тогда
        УсловиеОтбора = " В (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ВСпискеПоИерархии Тогда
        УсловиеОтбора = " В ИЕРАРХИИ (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.Интервал Тогда
        УсловиеОтбора = " МЕЖДУ &" + ЭлОтбора.Имя+"ЗначениеС И &" + ЭлОтбора.Имя+"ЗначениеПо";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеС", ЭлОтбора.ЗначениеС);
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеПо", ЭлОтбора.ЗначениеПо);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ИнтервалВключаяГраницы Тогда
        УсловиеОтбора = " " + ЭлОтбора.Имя + " >= &"+ЭлОтбора.Имя+"ЗначениеС И " + ЭлОтбора.Имя + " <= &"+ЭлОтбора.Имя+"ЗначениеПо ";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеС", ЭлОтбора.ЗначениеС);
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеПо", ЭлОтбора.ЗначениеПо);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ИнтервалВключаяНачало Тогда
        УсловиеОтбора = " " + ЭлОтбора.Имя + " >= &"+ЭлОтбора.Имя+"ЗначениеС И " + ЭлОтбора.Имя + " < &"+ЭлОтбора.Имя+"ЗначениеПо ";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеС", ЭлОтбора.ЗначениеС);
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеПо", ЭлОтбора.ЗначениеПо);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.ИнтервалВключаяОкончание Тогда
        УсловиеОтбора = " " + ЭлОтбора.Имя + " > &"+ЭлОтбора.Имя+"ЗначениеС И " + ЭлОтбора.Имя + " <= &"+ЭлОтбора.Имя+"ЗначениеПо ";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеС", ЭлОтбора.ЗначениеС);
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя+"ЗначениеПо", ЭлОтбора.ЗначениеПо);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.Меньше Тогда
        УсловиеОтбора = " < (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.МеньшеИлиРавно Тогда
        УсловиеОтбора = " <= (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.НеВИерархии Тогда
        УсловиеОтбора = " НЕ В ИЕРАРХИИ (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.НеВСписке Тогда
        УсловиеОтбора = " НЕ В (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.НеВСпискеПоИерархии Тогда
        УсловиеОтбора = " НЕ В ИЕРАРХИИ (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.НеРавно Тогда
        УсловиеОтбора = " <> (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.НеСодержит Тогда
        УсловиеОтбора = " НЕ ПОДОБНО (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.Равно Тогда
        УсловиеОтбора = " = (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    ИначеЕсли ЭлОтбора.ВидСравнения = ВидСравнения.Содержит Тогда
        УсловиеОтбора = " ПОДОБНО (&" + ЭлОтбора.Имя + ")";
        ЗапросДанныхРегистра.УстановитьПараметр(ЭлОтбора.Имя, ЭлОтбора.Значение);
    КонецЕсли;
     
    ТекстОтбора = ТекстОтбора +
        УсловиеОтбора;
     
КонецФункции

// Универсальная функция для проверки наличия свойств у значения любого типа данных
// Переменные:
// 1. Переменная - переменная любого типа, для которой необходимо проверить наличие свойства
// 2. ИмяСвойства - переменная типа "Строка", содержащая искомое свойства
// 
Функция СодержитСвойство(Переменная, ИмяСвойства) Экспорт // 14.10.2014 - Пермитин Ю.А.
	
	// Исключения. Эти типы не имеют свойств при проверке
	ТипПеременной = ТипЗнч(Переменная);
	Если ТипПеременной = Тип("Неопределено") ИЛИ ТипПеременной = Тип("Массив") Тогда
		Возврат Ложь;
	КонецЕсли;
	
	// Инициализируем структуру для теста с ключом (значение переменной "ИмяСвойства") и значением произвольного GUID'а
	GUIDПроверка = Новый УникальныйИдентификатор;
	СтруктураПроверка = Новый Структура;
	СтруктураПроверка.Вставить(ИмяСвойства, GUIDПроверка);
	// Заполняем созданную структуру из переданного значения переменной
	ЗаполнитьЗначенияСвойств(СтруктураПроверка, Переменная);
	// Если значение для свойства структуры осталось NULL, то искомое свойство не найдено, и наоборот.
	Если СтруктураПроверка[ИмяСвойства] = GUIDПроверка Тогда
		Возврат Ложь;
	Иначе
		Возврат Истина;
	КонецЕсли;
КонецФункции
Может возникнуть вопрос: "Зачем это нужно, если объектная техника делает то же самое и таким простым способом?". Приведу следующий пример.

Пример использования

При использовании объектной техники чтения наборов записей регистра есть одна интересная особенность - это наложение разделяемой управляемой блокировки, если вызов метода "Прочитать()" выполняется в транзакции. То есть если в транзакции был вызван метод "Прочитать()", то на выбранные записи платформа наложит разделяемую управляемую блокировку, что не позволит другим операциям выполнять запись аналогичного набора регистра, пока транзакция не будет завершена.

Приведу наглядный пример. В транзакции было выполнено чтение набора записей объектной техникой:

	НачатьТранзакцию();
	НаборДвиженияНоменклатураНоменклатура = РегистрыНакопления.ДвиженияНоменклатураНоменклатура.СоздатьНаборЗаписей();
	НаборДвиженияНоменклатураНоменклатура.Отбор.Регистратор.Установить(Документ);
	НаборДвиженияНоменклатураНоменклатура.Прочитать();
	// ... какая-то длительная операция ...
	ЗафиксироватьТранзакцию();

Пока выполняется некоторая длительная операция, транзакция остается активной. В это время другая транзакция пытается прочитать набор и после выполнить его запись:

	НачатьТранзакцию();
	НаборДвиженияНоменклатураНоменклатура = РегистрыНакопления.ДвиженияНоменклатураНоменклатура.СоздатьНаборЗаписей();
	НаборДвиженияНоменклатураНоменклатура.Отбор.Регистратор.Установить(Документ);
	// Чтение записей набора
	НаборДвиженияНоменклатураНоменклатура.Прочитать();
	// ... операции модификации записей набора ...
	НаборДвиженияНоменклатураНоменклатура.Записать();
	ЗафиксироватьТранзакцию();

При чтении набора никаких проблем не возникнет, т.к. первая транзакция установила разделяемую (данные блокируются от записи) блокировку. Но при попытке записать набор возникнет ожидание на блокировке данных, а если первая транзакция будет выполняться достаточно долго, то возникнет таймаут.

Ошибка таймаута на управляемой блокировке

При использовании же предложенного способа чтения наборов регистров с помощью запроса блокировки использоваться не будут, что позволит избежать подобных проблем.

Самому пришлось использовать предложенный способ при оптимизации выполнения обменов данными между базами УПП 1.3. При формировании XML-файла сообщения обмена используются транзакции, поэтому при выгрузке движений документов на них накладывается разделяемая управляемая блокировка, которая может мешать активным пользователям проводить документы.

Без фанатизма

Не смотря на то, что чтение наборов записей запросом может выглядеть более оптимальным решением по сравнению с объектной техникой, не стоит бросаться менять конфигурацию и повсеместно отказываться от использования метода "Прочитать()". Предложенная оптимизация необходима только в случаях, когда она действительно возымеет существенный эффект, в остальных же случаях стоит придерживаться стандартным методам работы.


comments powered by Disqus