Отказ от работы с временными файлами при работе с двоичными данными или Потоки как простая замена ADODB.Stream и временным файлам

Программирование - Практика программирования

В платформе начиная с версии 3.8.9 (как я понял по документации) появился расширенный функционал средств работы с двоичными данными. Если раньше простой и очевидный способ преобразования данных строился на использовании временных файлов, то теперь благодаря новым средствам можно уйти от их использования.

Вместо дисклеймера: Это первая моя статья, поэтому прошу отнестись с пониманием. Цель - попытка показать возможности нового функционала на примере двух конкретных задач.

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

Итак задачи:

  1. Собственная десериализация объектов из механизма версионирования объектов УПП.  Задача - обойтись без временного файла.
  2. Получение данных из SQL базы с varbinary типом данных. Задача - уйти от использования COM объекта ADODB.Stream и временного файла.

Начнем по порядку.

1. Собственная десериализация объектов из механизма версионирования объектов.

Здесь нет абсолютно никаких проблем. Объекты сериализуются стандартными механизмами и помещаются в регистр с ресурсом типа ХранилищеЗначений. И все хорошо за исключением последующей десериализации объектов. Для основы был взят механизм из отчета "История изменения объектов". Опускаем ненужное и идем к самой функции, которая из ресурса с типом ХранилищеЗначений получает строку для десерализации. Всю функцию приводить не буду, нам нужны строки преобразования двоичных данных в строковый тип:

ИмяВременногоФайла = ПолучитьИмяВременногоФайла();
ВерсияОбъекта.Записать(ИмяВременногоФайла);
ТекстовыйДокумент = Новый ТекстовыйДокумент;
ТекстовыйДокумент.Прочитать(ИмяВременногоФайла, КодировкаТекста.UTF8);
СтрокаXML = ТекстовыйДокумент.ПолучитьТекст();
УдалитьФайлы(ИмяВременногоФайла);

И как мы видим - используется механизм временных файлов. ВерсияОбъекта в данном случае имеет тип ДвоичныеДанные, получаемые из ресурса регистра с типом ХранилищеЗначения.

Сразу вопрос - есть ли возможность обойтись без его использования? Проблема заключается в том, что получив двоичные данные мы не можем преобразовать их в строковый тип для десереализации. Однако это не так. Немного покопавшись в великом СП, было найдено решение.

Собственно нас интересует объект "ЧтениеДанных", а точнее конструктор на основе двоичных данных. У этого объекта широйкий набор свойств и методов. И по СП видно, что нам пригодятся чтение данных в строковые типы - а именно два метода ЧтениеСтроки и ЧтениеСимволов. Первый дает возможность читать построчно, но это цикл и нам сейчас ни к чему. Второй читает количество символов, по умолчанию равное всему потоку. Его мы и будем использовать.

В качестве ремарки - оба метода имеют возможность указания вида кодировки в качестве параметра чтения, однако мы укажем кодировку в конструкторе объекта вторым параметром.

Почитав внимательно мы преобразуем код и получаем всего три строки:

Читатор = новый ЧтениеДанных(ВерсияОбъекта,КодировкаТекста.UTF8);
СтрокаXML = Читатор.ПрочитатьСимволы();
Читатор.Закрыть(); //не забываем чистить за собой

И в общем то все. Никаких временных файлов, на выходе строка для разбора десериализатором.

В голову приходит логичная мысль - для собственных целей мы получаем объект десереализацией по нашему новому алгоритму без временных файлов, но это не частая операция. А как выглядят дела с механизмом версионирования в принципе? Как происходит запись в регистр? Точнее нас интересует как происходит помещение данных в ХранилищеЗначения. Идем в общий модуль УПП реализующий версионирование и ищем то что нам нужно:

ИмяВременногоФайла = ПолучитьИмяВременногоФайла();

ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ИмяВременногоФайла); 
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписатьXML(ЗаписьXML, Источник, НазначениеТипаXML.Явное);
ЗаписьXML.Закрыть();
		
ДвоичныеДанные = Новый ДвоичныеДанные(ИмяВременногоФайла);
ХранилищеДанных = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));

Бинго! В регистр уходит ХранилищеДанных, но используются временные файлы для формирования ДовичныхДанных. И это понятно конфигурация УПП должна иметь и обратную совместимость. Но у нас уже в руках новые технологии и мы хотим их использовать. В базе с 3000 активными пользователями каждую минуту формируются огромное количество объектов подлежащих версионированию, а значит здесь уже мы имеем возможность практического применения потоков для ускорения и удаления "слабого звена".

Абсолютно справедливо механизм работает и в обратную сторону при помощи объекта ЗаписьДанных. Однако если при чтении  мы уже имеем двоичные данные, которые по сути являются потоком данных и нам нет необходимости его формировать, то при записи нам потребуется поток который мы затем и выгрузим как двоичные данные, а запись в этот поток осуществляется объектом ЗаписьДанных:

ЗаписьXML = новый ЗаписьXML();
ЗаписьXML.УстановитьСтроку();
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписатьXML(ЗаписьXML, Источник, НазначениеТипаXML.Явное);
СтрокаXML = ЗаписьXML.Закрыть();

Поток = новый ПотокВПамяти();
Запись = новый ЗаписьДанных(Поток,КодировкаТекста.UTF8);
Запись.ЗаписатьСимволы(СтрокаXML);
Запись.Закрыть(); //не забываем чистить за собой	
ДвоичныеДанные = Поток.ЗакрытьИПолучитьДвоичныеДанные(); 

ХранилищеДанных = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));

И опять никаких временных файлов. На что стоит обратить внимание - кодировка как и в первом случае может указываться в конструкторе объекта ЗаписьДанных, так и в его методах при непосредственной записи. Кроме того кодировка может указываться и в сериализации, но это уже другая история. Мы снова используем метод записи символов, но можем так же использовать и построчную "порционную" запись.

Код вышел чуть длиннее, но у нас нет временных файлов, все происходит в памяти. Вот так бесхитростно мы оптимизировали версионирование объектов в УПП.

Все выше перечисленное - очевидное использование Потоков. Теперь же сладкое - неочевидное использование потоков, а именно конкретная задача по чтению данных из SQL.

2. Получение данных из SQL базы с varbinary типом данных.

Возникла задача - создать обработку, которая будет обращаться к внешней базе данных на MS SQL и хранить в ней макеты текущих версий разрабатываемых внешних обработок. Такой некий совсем простой аналог GIT. Это обработки начального заполнения данных из разных источников, а необходимость хранения во внешнем источнике так как в процессе разработки параллельно тестируется около 20 баз и потребовался единый источник обработок заполнения. Возможно подход и неправильный, но речь сейчас не об этом.

В ходе работы с ADODB возникла проблема получения двоичных данных из varbinary типа SQL. Все кто работал и сталкивался понимают о чем идет речь - при получении значения из базы через ADODB.Recordset нам вываливается значение типа COMSafeArray с целочисленными элементами.

Не надо быть гением чтобы понять, что нам вываливают байты в десятиричном исчислении. Просто попытавшись перевести их в 16-тиричный формат это становится очевидным, а сам массив и есть наша последовательность байтов - по сути поток двоичных данных. Все решения найденные в "этих ваших интернетах" сводятся к одному - использовать ADODB.Stream, в котором как раз таки есть функция преобразования такого потока данных в файл, который нам и предлагают затем читать. Полное описание и способы работы не буду приводить, так как не в этом цель. Главное что следует понять ADODB.Stream есть ничто иное как обертка работы с потоками, а если у нас теперь есть возможность работать с потоками в 1С, значит все что делает эта обертка, мы можем сделать штатными средствами.

Начинаем копать и думать. Первая мысль - преобразовать каждый элемент в 16-тиричный формат и попытаться сформировать двоичные данные по символьным значениям. Но через 30 секунд отметаем идею - она тупиковая так как налицо избыточность используемых объемов памяти, и "детскость" в подходе. Вывод - смотрим в сторону потоков в 1С. Мы понимаем что наш COMSafeArray  это и есть поток данных, но в "особом" виде. Курим СП выискивая возможность формирования потока в 1С на основе нашего. Основной объект который мы смотрим ПотокВПамяти. Понятно что нам надо формировать его, однако не очевиден способ.

Вертим объект Поток со всех сторон и не находим методов нас удовлетворяющих. Однако находим зацепку - поток данных можно сформировать конструктором на основе буфера. Курим объект БуферДвоичныхДан

ных в СП и выясняем что БуферДвоичныхДанных согласно СП это:

Описание:
Коллекция байтов фиксированного размера с возможностью произвольного доступа и изменения по месту.

Дак это то что надо! Это и есть словесное описание того результата, что мы получаем из SQL. Смотрим методы и находим то, что нужно:

БуферДвоичныхДанных (BinaryDataBuffer)
Установить (Set)

Синтаксис:

Установить(<Позиция>, <Значение>)

Параметры:

<Позиция> (обязательный)

Тип: Число.
Позиция, на которую требуется поместить новое значение.

<Значение> (обязательный)

Тип: Число.
Значение, которое требуется установить в заданную позицию буфера.
Если значение больше 255 или меньше 0, будет выдана ошибка о неверном значении параметра.

Описание:

Устанавливает значение элемента на заданной позиции (нумерация начинается с 0).

Почему именно тут мы танцуем джигу? Потому что мы изначально работаем с набором байтов  в виде массива чисел от 0 до 255, и тут без каких либо преобразований нам предлагают установку этих байтов в виде этих же самых чисел от 0 до 255. На что обращаем внимание - конструктор буфера подразумевает заранее известную длину, а методом придется писать однозначно в цикле. Остальное - дело техники, поток сформировать по буферу и выгрузить в двоичные данные мы уже пробовали.

Вот код с использованием ADODB.Stream:

Функция ПолучитьФайлАДО_Stream(Value)
	Файл = Неопределено;
	Stream = Новый COMОбъект("ADODB.Stream");
	Stream.Type = 1;
	Stream.Open();
	Stream.Write(Value);
	ИмяФайла = ПолучитьИмяВременногоФайла();
	Stream.SaveToFile(ИмяФайла);
	Stream.Close();
	Файл = Новый ДвоичныеДанные(ИмяФайла);
	УдалитьФайлы(ИмяФайла);
	Возврат Файл;
КонецФункции

Пример вызова:

Файл = ПолучитьФайлАДО_Stream(АДОНаборДанных.Fields(2).Value);

Итак, код функции получения файла без использования ADODB.Stream и временных файлов.

Функция ПолучитьФайлАДО(Массив)
	Длинна = Массив.Количество();
	Буфер = новый БуферДвоичныхДанных(Длинна);
	Для индекс = 0 по Длинна - 1 Цикл
		Буфер.Установить(индекс,Массив[индекс]);	
	КонецЦикла;
	
	Поток = новый ПотокВПамяти(Буфер);
	ДвоичныеДанные = Поток.ЗакрытьИПолучитьДвоичныеДанные();
	
	Возврат ДвоичныеДанные;	

КонецФункции // ПолучитьФайлАДО()

И вызов этой функции при чтении данных (выдержка из кода моей функции):

	Пока не АДОНаборДанных.eof() Цикл
		Строка = ТЧОбработки.Добавить();
		Строка.ИмяОбработки = АДОНаборДанных.Fields(1).Value;
				
		Если ТипЗнч(АДОНаборДанных.Fields(2).Value) = Тип("COMSafeArray") Тогда
			Строка.Макет = ПолучитьФайлАДО(АДОНаборДанных.Fields(2).Value.Выгрузить());
			Строка.Запустить = "Запустить";
		КонецЕсли;
		
		АДОНаборДанных.MoveNext();
	КонецЦикла;

В качестве пояснения - проверка на тип COMSafeArray нужна для исключения NULL. Ну и с помощью Выгрузить() делаем сразу преобразование в обычный массив. Зачем? Просто так захотелось. Можно работать и без выгрузки прямиком с COMSafeArray.

Итого - мы получили двоичные данные без временных файлов и дополнительной внешней компоненты.

Все выше описанное - лишь верхушка айсберга в виде мощного инструмента работы с двоичными данным. Не затронуты темы порядка чтения и записи, частичного чтения и записи и многих других аспектов.

В качестве вывода - об использовании временных файлов в 1С можно забыть.

Upd: Исправил орфографические ошибки. Спасибо всем, кто обратил внимание.

См. также

Комментарии
1. Валерий М (VmvLer) 12.09.17 14:55 Сейчас в теме
При таком способе ухода от временных файлов мы минимизируем операции с диском, но увеличиваем нагрузку на память, так нет? Если под временными файлами быстрый диск, то может память поберечь для других задач.

Я хочу спросить хрен редки не слаще ли, т.е. тесты на общую производительность есть?
2. Александр Сергеев (vardeg) 65 12.09.17 15:09 Сейчас в теме
Полноценные тесты не стояла задача делать.
Вот умозаключения по этому поводу не подкрепленные практическими тестами:
Выигрыш в производительности вполне может быть и малым, но насчет слаще редька или нет - готов поспорить. Каждый объект и так у нас существует в памяти. Если для преобразования строки в ДД и обратно мы в добавок к ним задействуем еще и временные файлы - то выделенной памяти явно не станет меньше. В любом случае для записи в файл и чтения из него платформа должна будет сформировать временные структуры в памяти - мы это делаем сами потоками. По логике - то же самое.
А вот при работе с ADODB.Stream - выигрыш очевиден. У нас нет необходимости при прочих равных (я имею ввиду и так наличие временных структур в памяти) иметь еще один COM объект и обращаться к его методам. Все делается штатными средствами.
Чего искренне не хватает и что хотелось бы - формирование БуфераДвоичныхДанных на основе массива. Если это будет реализовано на уровне платформы - то итоговый выигрыш в производительности так же будет очевиден - пока же я не уверен, что цикл формирования буфера не съест весь профит.
GreenDragon; mevgenym; +2 Ответить 1
3. vitkhv vitkhv (vitkhv) 12.09.17 15:30 Сейчас в теме
Исправьте плиз:
Получение данных из SQL базы с varbinaty varbinary типом данных.
4. PerlAmutor IC (PerlAmutor) 7 12.09.17 17:13 Сейчас в теме
Исправьте плиз:
Итак, код функции получения фала файла без использования ADODB.Stream и временных файлов.

работаем с наобором набором байтов

десереализация десериализация

мощного инустремнты инструмента работы
5. PerlAmutor IC (PerlAmutor) 7 13.09.17 08:37 Сейчас в теме
Upd: Исправил орфографические ошибки. Спасибо всем, кто обратил внимание.

Тогда вот еще =)
десерализации десериализации

десереализации десериализации

десереализацией десериализацией

интерисует интересует

десятиричном десятеричном

инструмента работы с двоичными данным данными


P.S.: я не Grammar Nazi =)
6. Иван Пантелеев (RailMen) 680 13.09.17 08:37 Сейчас в теме
Отличная статья, задрали временные файлы.
7. Алексей Соловьев (Silenser) 414 13.09.17 10:45 Сейчас в теме
8. Максим Горбачев (Tangram) 126 13.09.17 10:58 Сейчас в теме
Интересно, буду использовать. Ну и в заголовке поправь версию платформы пжлста (3.8.9).
GreenDragon; +1 Ответить
9. Владимир Крючков (ivanov660) 406 13.09.17 11:12 Сейчас в теме
"+" за акцент внимания на новых фитчах от 1С.
10. kiruha Дронов (kiruha) 360 13.09.17 11:25 Сейчас в теме
Иногда файлы xml имеют размеры > 1 Гб
Этот Поток будет скидывать данные во временный файл в случае нехватки памяти
или рухнет 1С ?
11. Александр Сергеев (vardeg) 65 13.09.17 12:11 Сейчас в теме
(10) Если честно - Вы меня немного озадачили и заставили задуматься.
Хотелось бы посмотреть на работу с такими xml файлами через механизм временных файлов, ну или вообще на работу с ними.
Хотелось бы посмотреть на попытку создать конструктором ДовчиныеДанные на основе такого файла. Ну или простую десериализацию такого файла.
Однако мысли на этот счет следующие - мы имеем набор инструментов, и сами определяем в какой ситуации каким набором инструментов.
Поэтому в качестве ответа:
В отличии от коснтруктора ДвоичныхДанных на основе файла мы с потоками можем работать порционно - делать последовательные записи и чтения Потока, для этого существует полный набор методов объекта ЧтениеДанных, включая приятный метод ПрочитатьСтроку, который в потоке ДД определяет по указанному нами разделителю строку. Аналогично в обратную сторону.Если мы знаем, что XML будет таких размеров, а у нас возникает необходимость получить его из двоичных данных, то я бы создал механизм поточного чтения и обработки порций информации. Тем более что десериализация XML так же поддерживает механизм последовательного чтения - у объекта ЧтениеXML есть соответствующие методы.
Теоретически используя подход чтения и обработки таких файлов "по кускам", а правильнее поточного (масло масленое конечно, но все же) мы можем обойти ограничения по памяти, которое вывалит нам ошибку при попытке работы в объектной модели.
Если честно - Ваш вопрос очень интересный и в нем я вижу потенциал для нового исследования и статьии с практическими примерами работы с большими массивами данных. Если Вас не затруднит сформулировать задачу из абстрактной в более или менее предметную - было бы интересно попытаться ее решить.
12. PerlAmutor IC (PerlAmutor) 7 13.09.17 12:32 Сейчас в теме
Жалко что 1С умеет работать со своими коллекциями (Массив, Структура, ТаблицаЗначений и т.д.) только на уровне оперативной памяти.
Вы можете сериализовать ТаблицуЗначений в файл и потом загрузить обратно. А представьте что будет, если сериализовать ТаблицуЗначений в файл на сервере где >100Гб ОЗУ, а потом попробовать загрузить этот файл уже на клиенте (толстый клиент, обычное приложение) ?
Такие операции как построение индекса, сортировка или свертывание ТЗ хотелось бы иметь возможность проводить на диске.
Чтобы можно было делать отборы по ТЗ находящейся на диске, подгружать порционно в окно пользователя (pagination), а не держать сотни мегабайт в ОЗУ, где все данные и не нужны.
Иногда хочется иметь выбор между скоростью (забить всю оперативку) и стабильностью (медленно но с гарантией, что всё будет обработано и в критичный момент не свалится)
13. Константин Гейнрих (CyberCerber) 158 13.09.17 12:34 Сейчас в теме
По поводу оптимизации могу сказать, что операцию, описанную во второй части статьи, пришлось проделать пару месяцев назад в одном проекте, т.к. объект ADODB.Stream время от времени отваливался на сервере. Код, в принципе, почти полностью совпадает с вашим.
Только в итоге остановился на варианте, что сначала в попытке выполняется по ADODB.Stream, а только в исключении встроенный ЗакрытьИПолучитьДвоичныеДанные, т.к. на данных объемом в несколько мегабайт тормоза у встроенного заметные. Наверное, дело в побайтовой записи в цикле.
14. Альтаир (Altair777) 639 13.09.17 12:43 Сейчас в теме
(5) Ну, и это поправьте
Точнее нас интерисует как происходит

А еще не хватает несколько десятков запятых. Попробуйте воспользоваться MS Word
15. Александр Сергеев (vardeg) 65 13.09.17 12:47 Сейчас в теме
(13) Собственно подтверждение домыслов из моего ответа (2) . Как уже говорил - вполне логично разработчикам платформы предусмотреть метод формирования буфера двоичных данных по массиву. Если это будет реализовано, то это даст ощутимый прирост скорости.
16. kiruha Дронов (kiruha) 360 13.09.17 13:14 Сейчас в теме
(11)
Если Вас не затруднит сформулировать задачу из абстрактной в более или менее предметную - было бы интересно попытаться ее решить.

В более предметной - читаются или пишутся файлы обмена со сторонними системами xml - очень большого размера .
Обсуждений много и на инфостарте и на мисте , например http://www.forum.mista.ru/topic.php?id=489798 https://infostart.ru/public/15464/
17. Александр Сергеев (vardeg) 65 13.09.17 13:23 Сейчас в теме
(16) я в принципе Вас понял. Я смоделирую ситуацию и попробую создать инструментарий для таких загрузок из сторонних систем.
18. Алекс zhu4 (Arxxximed) 13.09.17 13:50 Сейчас в теме
По первому пункту вроде добавлен
 	ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки(ПараметрСтрока,КодировкаТекста.ANSI);
 	ПараметрСтрока = ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные,КодировкаТекста.ANSI);

...Показать Скрыть


Как то все намного изящнее... Но это в 8.3.10 я вижу. Не знаю как в 8.3.9
binex; vardeg; +2 Ответить 1
19. Александр Сергеев (vardeg) 65 13.09.17 14:34 Сейчас в теме
(18) да, в 8.3.10 функционал еще более расширен. так как с самой платформой еще не работал - упустил из виду эти функцию. спасибо - изучим.
20. Павел Жихарев (palsergeich) 13.09.17 14:34 Сейчас в теме
Вместо
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ИмяВременногоФайла); 
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписатьXML(ЗаписьXML, Источник, НазначениеТипаXML.Явное);
ЗаписьXML.Закрыть(); 
...Показать Скрыть

и обратного есть же шикарные методы - XMLСтрока и XMLЗначение ...
Функция ПолучитьФайлАДО_Stream(Value)
	Файл = Неопределено;
	Stream = Новый COMОбъект("ADODB.Stream");
	Stream.Type = 1;
	Stream.Open();
	Stream.Write(Value);
	ИмяФайла = ПолучитьИмяВременногоФайла();
	Stream.SaveToFile(ИмяФайла);
	Stream.Close();
	Файл = Новый ДвоичныеДанные(ИмяФайла);
	УдалитьФайлы(ИмяФайла);
	Возврат Файл;
КонецФункции
...Показать Скрыть

Нет никаких гарантий, что завтра сервер переедет на тот же linux и придется все участки с COM лопатить. Что мешает сделать сразу универсально с помошью хранимых процедур....
21. Денис Новосёлов (binex) 213 13.09.17 15:00 Сейчас в теме
Спасибо за разбор! Сегодня уже применил. Недавно ковырял в этом направлении, да забросил.
22. Александр Сергеев (vardeg) 65 13.09.17 15:41 Сейчас в теме
(20)
1 - речь в статье не возможностях сериализатора. Уверен там есть изящные и удобные методы работы. Речь идет о преобразовании данных при работе с ДД.
2 - как раз таки от COM и предлагается в статье уйти на примере получения двоичных данных. Касательно хранимых процедур - не всегда есть возможность создать хранимые процедуры на целевом источнике данных. Понятно, что есть варианты различные решения и этой проблемы - но повторюсь - речь в статье о том как новый механизм на основе новых объектов Поток позволяет штатными средствами работать с двоичными данными и заниматься их преобразованием. А именно применять новые возможности в задачах решаемых ранее шаблонным типом на основе временных файлов.
23. Сергей Маслов (LexSeIch) 185 14.09.17 06:23 Сейчас в теме
Спасибо за интересную статью. Добавил себе в копилку.
24. Владимир Ленгин (vlengin) 18.09.17 12:27 Сейчас в теме
За информацию по новым объектам - спасибо!
Ну а от ADODB.Stream (и "промежуточного" файла) легко избавиться другим способом: конвертировать Binary-Base64 (Base64-Binary) средствами MS SQL, ну а на стороне 1С использовать Base64Значение(Base64Строка), т.е. "обмен данными" с MS SQL через строку Base64
25. Алексей Соловьев (Silenser) 414 20.09.17 11:34 Сейчас в теме
Попробовал прямой метод считывания побайтово данных из потока и запихивания в COMSafeArray. С производительностью, как и ожидалось, беда. На файлах 2-10 Кб - все проходит незаметно, а вот на файлах в несколько мегабайт все печально.
Прикрепленные файлы:
26. Алексей Соловьев (Silenser) 414 20.09.17 13:23 Сейчас в теме
Какой-то глюк, вот вариант с временными файлами
Прикрепленные файлы:
Оставьте свое сообщение