Оглавление
Имена таблиц и полей хранения в БД
Результат запроса ADODB. Таблица в формате JSON
Значения предопределенных данных и использование имен 1С
Значения пустой ссылки, Неопределено
Временная таблица из объекта с данными
Запросы на удаление и изменение
Работа в клиент/серверном режиме
Конструктор модели запроса SQL
Демо-пример сценария использования модели запроса SQL
Однажды мне поручили работу по восстановлению данных. Данные хранились в различных системах в сервисно-ориентированной архитектуре. Эти системы разрабатывались в разных технологических стеках, но всех их объединяло использование реляционных БД. В такой архитектуре избыточность хранения считается нормой. Все системы обмениваются со всеми и каждая хранит у себя копию нужных ей данных.
В результате атаки на сервера часть данных была либо заблокирована (навсегда), либо уничтожена. И вот в такой ситуации разработчики разных систем пытались восстановить свои данные за счет данных других. Я, как представитель технологического стека 1С, решал задачу восстановления данных в базе 1С.
Специфика работы с данными в 1С - использование объектного доступа к БД. Однако в сложившихся обстоятельствах этот способ работы с данными был очень неэффективным.
Альтернативный способ работы с данными - прямые запросы на SQL. Этот способ оказался очень эффективным в плане производительности и очень трудоемким в плане разработки и сопровождения. Основная трудность для меня была в том, что имена таблиц и полей запроса к базе 1С приходилось искать по сопоставлению имени в конфигураторе и в структуре хранения.
Вот тогда то и родилась у меня идея создания некоторого прокси-объекта, с помощью которого можно было бы получать имена хранения таблиц и их полей по их имени в конфигураторе.
Однако быстро оказалось так, что толку от простого получения имен хранения немного, если потом приходится описывать запрос. Поначалу я собирал запрос с помощью шаблонов с подстановкой имен хранения по имени из конфигуратора. И в конечном счете оказалось, что я делаю модель запроса.
Структура запроса в общем виде состоит из секций: описание запроса, объединение, упорядочивание. Секция описания состоит из: источников, связей, отборов и выбранных полей. Если сделать модель запроса, которая будет определять источники 1С, то можно построить и сам запрос в именах хранения БД. Для этого можно использовать функцию платформы ПолучитьСтруктуруХраненияБазыДанных для получения описания таблиц в именах хранения БД.
На входе модель принимает имя таблицы источника и ее псевдоним. Таблица источника может быть распознана как таблица 1С и тогда для нее определяется имя хранения таблицы и её полей в БД. Обращение к полям таблицы 1С можно сделать через рекурсивный вызов функции самой модели. Т.к. модель распознает таблицы 1С, то имя поля хранения можно получить из самой модели и вернуть его в условие отбора или в выражение выбранного поля модели. Иными словами, если в модели уже была использована таблица 1С, то модель уже "знает" имя хранения таблицы и ее полей в БД.
В SQL можно использовать запросы разного типа: выбрать, удалить, изменить. В новой модели запроса SQL также нет ограничений и все эти типы запросов доступны.
Конечно, использование запросов, минуя объектную модель 1С может привести к последствиям. Поэтому нужно иметь очень веские основания для использования запросов на изменение к данным, хранящимся в базе 1С. Это могут быть задачи восстановления, тестирования, сверки, конвертации или еще какой-либо разовой обработки данных.
С другой стороны, вполне можно представить задачу на обновление данных во внешних источниках. Тогда использование запросов на изменение может быть вполне допустимым, ведь мы в этом случае предполагаем, что у внешнего источника нет своей объектной модели.
Выполнение запроса SQL из 1С можно сделать например через объект ADODB. Для это достаточно создать объект и настроить соединение с базой. Использовать объект можно как на клиенте, так и на сервере. Для меня особый интерес представлял клиентский режим работы, т.к. я использовал win-аутентификацию на клиенте вместо служебного пользователя на сервере.
Для работы с ADODB был создан общий модуль РаботаС_ADODB. Основное назначение модуля: интерпретировать входящие параметры подключения в формат соединения ADODB и выдавать результат запроса в обработанном виде.
Для настройки соединения необходимо указать Сервер БД и Имя базы данных, а также параметры аутентификации. ADODB работает не с отдельными параметрами соединения, а со строкой соединения. Строка соединения имеет свой формат, справочник по формату подключения к MS SQL можно посмотреть здесь https://www.connectionstrings.com/sql-server/. Есть еще интерфейсный способ конструирования строки подключения, который можно вызвать кодом на 1С (см. Пример в //infostart.ru/1c/articles/80730/). В модели РаботаС_ADODB соединение к БД получается из переданных параметров подключения.
Для работы с результатом запроса через ADODB необходимо его загрузить в какой нибудь тип данных. Таблица значений не подходит, т.к. нужно также обеспечить работу и на клиенте. Устоявшегося формата для хранения типизированных табличных данных, кроме таблицы значений - в платформе нет. Можно смоделировать таблицу в виде структуры со свойствами: Колонки, Строки. Колонки - массив структур со свойствами колонки, а строки - массив строк, где каждая строка - массив значений.
Также необходимо решить вопрос с преобразованием типов данных ADODB в соответствующие типы 1С. Простые типы: Строка, Число, Дата - необходимо интерпретировать по размеру, точностью, формату времени и даты. Тип Булево и ссылки 1С хранит в бинарном типе. При чтении полей с бинарным типом ADODB возвращает массив байтов COMSafeArray. Этот массив необходимо преобразовать в соответствующий тип: Булево, Ссылка.
При выполнении пакета запросов ADODB также возвратит пакет результатов. В модуле есть две функции выполнения запроса по аналогии с запросом 1С: ВыполнитьЗапрос(ТекстЗапросаSQL), ВыполнитьПакетЗапросов(ТекстЗапросаSQL) - в первом будет возвращен результат последнего запроса, а во втором - массив результатов.
В одной БД может существовать неограниченное количество таблиц с одинаковыми именами. Для обеспечения уникальности таблиц в БД используются пространства имен. Пространство задается указанием схемы. Обычно схема имеет имя своего владельца. Например для таблиц 1С обычно указывается владелец dbo и для указания таблицы 1С требуется указывать префикс dbo (если имя таблицы в БД однозначно, то можно и не указывать).
Еще в БД можно обращаться к таблицам из других БД и даже к находящимся на других серверах. В таких случаях в понятие схемы включается путь к другой базе данных или к другому серверу (сервер должен быть связан).
В модели также можно указать используемую схему. Поскольку я предполагаю, что в запросе могут участвовать как таблицы из БД 1С, так и из внешнего источника, то в модели можно указать две разные схемы: Схема и Схема1С. Схемы можно задать как на уровне модели, так и для каждого запроса пакета отдельно.
Если вы еще не знакомы с моделью запроса, то рекомендую познакомиться прежде, чем читать дальше :)
Для тех, кто знаком с моей разработкой Модель запроса, представленный способ работы с запросами SQL будет также понятен. Здесь и далее я буду пользоваться обработкой "Конструктор модели запроса SQL".
Для начала сделайте простой эксперимент: создайте запрос SQL из запроса 1С. Для этого откройте конструктор модели запроса, создайте простой запрос без условий и отборов (можно использовать соединения), создайте текст модели по команде "Получить текст модели" и скопируйте его в конструктор модели SQL. Добавьте к названию модели окончание "SQL" (впрочем последнее необязательно). Вызовите команду "Получить текст запроса SQL" в конструкторе модели запроса SQL.
Использование конструктора запроса 1С для получения модели запроса с последующим преобразованием в модель запроса SQL
Исследуйте полученный текст запроса на SQL. При желании его можно тут же и выполнить по команде "Выполнить запрос". Результат выполнения можно увидеть в формате JSON. По команде "Печать" результат можно вывести в табличный отчет.
В текущей версии модель может быть использована только для получения текста запроса SQL и работает в контексте Сервера. Выполнение запроса и обработка результата доступна в разных контекстах через использование функций модуля РаботаС_ADODB.
Для работы с результатом запроса я разработал следующий формат таблицы результата: {Колонки: [{Имя, ТипЗначения}, ...], Строки: [[,,,,], ...]}. Соответствующий тип в 1С - это Структура с полями Колонки, Строки. Колонки - массив структур с полями Имя, ТипЗначения. Строки - массив строк, где строка - массив значений колонок. Такая структура хорошо сериализуется в формат JSON и с ней можно работать в разных контекстах клиент/сервер.
Для описания типа я принял следующий формат: СТРОКА(n), ЧИСЛО(n, m), ДАТА, БУЛЕВО, где описание размера и точности можно опустить.
Например для следующей модели запроса:
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета()
.Выбрать().Первые(3)
.Источник("Документ._ДемоЗаказПокупателя")
.Поле("_ДемоЗаказПокупателя.Ссылка")
.Поле("_ДемоЗаказПокупателя.Дата")
.Поле("_ДемоЗаказПокупателя.Номер")
.Поле("_ДемоЗаказПокупателя.ПометкаУдаления")
;
получим следующий текст запроса SQL:
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
SELECT TOP 3
_ДемоЗаказПокупателя._IDRRef AS Ссылка
, _ДемоЗаказПокупателя._Date_Time AS Дата
, _ДемоЗаказПокупателя._Number AS Номер
, _ДемоЗаказПокупателя._Marked AS ПометкаУдаления
FROM
_Document39 AS _ДемоЗаказПокупателя
и результат выполнения запроса:
{
"Колонки": [
{
"Имя": "Ссылка",
"ТипЗначения": "СТРОКА(34)"
},
{
"Имя": "Дата",
"ТипЗначения": "СТРОКА(19)"
},
{
"Имя": "Номер",
"ТипЗначения": "СТРОКА(11)"
},
{
"Имя": "ПометкаУдаления",
"ТипЗначения": "СТРОКА(4)"
}
],
"Строки": [
[
"0x896CE0CB4ED5F65511E4A2D5A2EB6C9C",
"4015-01-23 11:03:58",
"00-00000018",
"0x00"
],
[
"0x8C3608002700700111E1CA6CC5F6A3F7",
"4011-12-12 10:45:05",
"00-00000002",
"0x00"
],
[
"0x8C3608002700700111E1CA6CC5F6A3F8",
"4012-03-23 11:57:31",
"00-00000003",
"0x00"
]
]
}
По команде Печать получим такой отчет:
В таблице результата можно заметить, что типы полей строковые. Такая особенность работы с типами данных из БД 1С: тип Булево хранится в бинарном виде, тип Дата возвращается из ADO как строковый, а ссылка хранится в бинарном виде 16 слов. Подробнее про работу с типами я описал ниже.
Для различных преобразований результата есть функции из общих модулей:
JSON = ОбщийКлиентСервер.ОбъектВJSON(ТаблицаJSON);
ТаблицаJSON = ОбщийКлиентСервер.JSONВОбъект(JSON);
Взаимные преобразования ТаблицаJSON->JSON и JSON->ТаблицаJSON
ТаблицаJSON = Общий.ТаблицаЗначенийВСтруктуру(ТаблицаЗначения);
ТаблицаЗначения = Общий.СтруктураВТаблицуЗначений(ТаблицаJSON);
Взаимные преобразования ТаблицаЗначения->ТаблицаJSON и ТаблицаJSON ->ТаблицаЗначения
Значения предопределенных данных и использование имен 1С
В структуре данных базы 1С типы Булево и Ссылка хранятся в бинарных полях.
В текущей версии формата таблицы результата бинарные поля не поддерживаются прямо. При обработке значений таких полей интерпретируются в виде бинарного числа в шетнадцатиричном формате. Хранится такое значение в поле типа Строка соответствующего размера + 2, где два символа отводятся на префикс "0x". При интерпретации запроса из источника, значения с таким префиксом интерпретируются как бинарные числа.
Например напишем слудующий текст модели выборки результата:
;// ЗАПРОС ПАКЕТА.
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать().Первые(3)
.Источник(Результат, "Результат")
.Поле("Ссылка")
.Поле("Дата")
.Поле("Номер")
.Поле("ПометкаУдаления")
;
по команде Получить текст запроса получится следующий SQL запрос из результата выполнения предыдущего запроса:
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
SELECT TOP 3
Ссылка AS Ссылка
, Дата AS Дата
, Номер AS Номер
, ПометкаУдаления AS ПометкаУдаления
FROM
(VALUES
(0x896CE0CB4ED5F65511E4A2D5A2EB6C9C, '4015-01-23 11:03:58', '00-00000018', 0x00)
, (0x8C3608002700700111E1CA6CC5F6A3F7, '4011-12-12 10:45:05', '00-00000002', 0x00)
, (0x8C3608002700700111E1CA6CC5F6A3F8, '4012-03-23 11:57:31', '00-00000003', 0x00)
) AS Результат (Ссылка, Дата, Номер, ПометкаУдаления)
В полученном тексте запроса значения ссылок уже не строковые, а бинарные и эти значения можно использовать в соединениях с таблицами 1С (нужно выбрать данные во временную таблицу, как это сделать см. ниже).
Значения ссылки
Ссылки или ИД объектов 1С хранятся в бинарном поле размером 16 двухбайтовых слов. Особенность работы с таким типом в том, что интерпретация значения ссылки в БД и в коде 1С различаются. В БД ссылка хранится в бинарном виде и представляется как шестнадцатиричное число. В коде 1С ссылка имеет особое представление в шестнадцатиричном виде. Т.е. это тоже самое число, но порядок байтов другой, не от старшего к младшему, а особый.
В модуле есть две функции преобразования: HexToBin - порядок представления ссылки в 1С -> бинарное представление в БД, BinToHex - бинарное представление в БД -> шестнадцатиричное представление ссылки в 1С.
Если получить результат запроса, то в нем будет представление в БД. Такое представление можно использовать без преобразования в других запросах к БД. Однако чтобы по такому значению найти ссылку средствами 1С необходимо его преобразовать в шестнадцатиричное представление 1С. И наоборот, чтобы по представлению ссылки из 1С найти значение ссылки в запросе SQL ее нужно преобразовать в бинарный формат шестнадцатиричного представления в БД.
Значение даты 1С хранит в полях с типом данных datetime2. Это предпочтительный тип хранения даты, но при чтении через ADO значения таких полей интерпретируются как строковые.
Проблем с чтением типа datetime нет. Значения этого типа читаются как дата и время.
Для решения проблемы чтения типа datetime2 можно использовать следующее преобразование к типу datetime: CAST(DATEADD(YYYY, -2000, ИмяПоляХранения) AS DATETIME) AS Псевдоним.
Исправим поле в уже приведенном примере модели (не забываем указывать псевдоним поля, т.к. псевдоним по умолчанию от выражения может содержать ошибки):
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать().Первые(3)
.Источник("Документ._ДемоЗаказПокупателя")
.Поле("_ДемоЗаказПокупателя.Ссылка")
.Поле(СтрШаблон("CAST(DATEADD(YYYY, -2000, %1) AS DATETIME)", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Дата")), "Дата")
.Поле("_ДемоЗаказПокупателя.Номер")
.Поле("_ДемоЗаказПокупателя.ПометкаУдаления")
;
полученный текст SQL:
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
SELECT TOP 3
_ДемоЗаказПокупателя._IDRRef AS Ссылка
, CAST(DATEADD(YYYY, -2000, _ДемоЗаказПокупателя._Date_Time) AS DATETIME) AS Дата
, _ДемоЗаказПокупателя._Number AS Номер
, _ДемоЗаказПокупателя._Marked AS ПометкаУдаления
FROM
_Document39 AS _ДемоЗаказПокупателя
и полученный результат:
{
"Колонки": [
{
"Имя": "Ссылка",
"ТипЗначения": "СТРОКА(34)"
},
{
"Имя": "Дата",
"ТипЗначения": "ДАТА"
},
{
"Имя": "Номер",
"ТипЗначения": "СТРОКА(11)"
},
{
"Имя": "ПометкаУдаления",
"ТипЗначения": "СТРОКА(4)"
}
],
"Строки": [
[
"0x896CE0CB4ED5F65511E4A2D5A2EB6C9C",
"2015-01-23T11:03:58",
"00-00000018",
"0x00"
],
[
"0x8C3608002700700111E1CA6CC5F6A3F7",
"2011-12-12T10:45:05",
"00-00000002",
"0x00"
],
[
"0x8C3608002700700111E1CA6CC5F6A3F8",
"2012-03-23T11:57:31",
"00-00000003",
"0x00"
]
]
}
, где тип поля - Дата, формат даты в ISO и учтено смещение -2000.
Отмечу также, что для обратного преобразования достаточно добавить к значению поля datetime смещение 2000 вот так: DATEADD(YYYY, 2000, ИмяПоля). Такие значения можно использовать в выражениях сравнения или для обновления данных в базе 1С.
Имя поля хранения
Простые преобразования полей 1С в поля хранения производятся сразу. Если в модели явно задан источник - таблица 1С и через точку поле из этого источника, то модель запроса SQL преобразует такое поле в имя поля хранения с псевдонимом имени поля. Например Поле("_ДемоЗаказПокупателя.Ссылка") будет преобразовано в _IDRRef AS Ссылка. Однако выражение поля Поле("SUM(Товары.Количество)") уже преобразовано не будет. Для преобразования такого поля нужно использовать функцию модели ИмяПоляХранения(ПутьКПолю). Приведенный код будет выглядеть так:
Поле(СтрШаблон("SUM(%1)", МодельЗапросаSQL.ИмяПоляХранения("Товары.Количество")))
Аналогичный подход следует использовать в выражениях соединения, отбора, порядка.
Использование в запросе значений перечислений или предопределенных значений реализовано через использование функции модели ИД(ПредопределенноеЗначение). Предопределенное значение можно использовать в выражении поля, отбора, условий связи или в выражении порядка.
По аналогии с подстановкой полей хранения здесь также можно использовать шаблон строки. С помощью шаблона можно сформировать строку с подстановкой значений ссылок в бинарном формате шестнадцатиричного представления. Для получения ссылки в бинарной формате достаточно передать в функцию ИД ссылку предопределенного значения или значение перечисления (нужно передать именно ссылку, а не путь к метаданным!).
;// ЗАПРОС ПАКЕТА.
МодельЗапроса.ЗапросПакета()
.Выбрать()
.Источник("Справочник._ДемоФизическиеЛица.КонтактнаяИнформация", "КонтактнаяИнформация")
.Источник("Справочник._ДемоФизическиеЛица")
.ЛевоеСоединение("КонтактнаяИнформация", "_ДемоФизическиеЛица")
.Связь("Ссылка")
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("КонтактнаяИнформация.Тип"), МодельЗапроса.ИД(Перечисления.ТипыКонтактнойИнформации.АдресЭлектроннойПочты)))
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("КонтактнаяИнформация.Вид"), МодельЗапроса.ИД(Справочники.ВидыКонтактнойИнформации.EmailПользователя)))
.Поле("_ДемоФизическиеЛица.Ссылка")
.Поле("КонтактнаяИнформация.НомерСтроки")
.Поле("КонтактнаяИнформация.Тип")
.Поле("КонтактнаяИнформация.Вид")
.Поле("КонтактнаяИнформация.Значение")
;
Использование полей хранения и предопределенных значений в условиях отбора
-- ЗАПРОС ПАКЕТА 1.
SELECT
_ДемоФизическиеЛица._IDRRef AS Ссылка
, КонтактнаяИнформация._LineNo4843 AS НомерСтроки
, КонтактнаяИнформация._Fld4844RRef AS Тип
, КонтактнаяИнформация._Fld4845RRef AS Вид
, КонтактнаяИнформация._Fld6754 AS Значение
FROM
_Reference38_VT4842 AS КонтактнаяИнформация
LEFT JOIN _Reference38 AS _ДемоФизическиеЛица
ON КонтактнаяИнформация._Reference38_IDRRef = _ДемоФизическиеЛица._IDRRef
WHERE
КонтактнаяИнформация._Fld4844RRef = 0x82E6D573EE35D0904BF4D326A84A91D2
AND КонтактнаяИнформация._Fld4845RRef = 0xA6974B87E83C8DD04F784C2BB930F2D1
Текст запроса SQL, полученные из модели
Значения ПустаяСсылка, Неопределено
Значения пустой ссылки или значения Неопределено в SQL одно - это бинарный ноль: 0x00000000000000000000000000000000. Для подстановки такого значения в модель можно воспользоваться функцией ИД() без аргумента или с пустым значением. Эта функция для любого пустого значения или Неопределено вернет бинарное представление ноля.
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Поле("_ДемоЗаказПокупателя.Номер")
.Поле("_ДемоЗаказПокупателя.Дата")
.Поле("_ДемоЗаказПокупателя.Организация")
.Поле("_ДемоЗаказПокупателя.Контрагент")
.Поле("_ДемоЗаказПокупателя.Договор")
.Поле("_ДемоЗаказПокупателя.Проведен")
.Поле("_ДемоЗаказПокупателя.ПометкаУдаления")
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапроса.ИД()))
;
Отбор заказов по пустому значению договора
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
SELECT
_ДемоЗаказПокупателя._Number AS Номер
, _ДемоЗаказПокупателя._Date_Time AS Дата
, _ДемоЗаказПокупателя._Fld968RRef AS Организация
, _ДемоЗаказПокупателя._Fld2908RRef AS Контрагент
, _ДемоЗаказПокупателя._Fld2909RRef AS Договор
, _ДемоЗаказПокупателя._Posted AS Проведен
, _ДемоЗаказПокупателя._Marked AS ПометкаУдаления
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Fld2909RRef = 0x00000000000000000000000000000000
SQL запрос с отбором по пустой ссылке
{
"Колонки": [
{
"Имя": "Номер",
"ТипЗначения": "СТРОКА(11)"
},
{
"Имя": "Дата",
"ТипЗначения": "СТРОКА(19)"
},
{
"Имя": "Организация",
"ТипЗначения": "СТРОКА(34)"
},
{
"Имя": "Контрагент",
"ТипЗначения": "СТРОКА(34)"
},
{
"Имя": "Договор",
"ТипЗначения": "СТРОКА(34)"
},
{
"Имя": "Проведен",
"ТипЗначения": "СТРОКА(4)"
},
{
"Имя": "ПометкаУдаления",
"ТипЗначения": "СТРОКА(4)"
}
],
"Строки": [
[
"00-00000019",
"4015-07-24 10:46:36",
"0x00000000000000000000000000000000",
"0x00000000000000000000000000000000",
"0x00000000000000000000000000000000",
"0x00",
"0x00"
]
]
}
Результат запроса: найден один заказ, у которого не заполнены Организация, Контрагент, Договор
Работа с перечислениями несколько отличается от работы с предопределенными значениями других видов метаданных. Дело в том, что перечисления - это именованные константы, ссылки на которые хранятся в конфигурации, а не в БД.
Для использования значения перечисления в запросах БД необходимо как то получить его идентификатор хранения в БД. Обычно идентификатор можно получить из ссылки, но к перечислениям это не относится. Несмотря на то, что перечисление не является ссылочным типом, в БД его значение хранится в виде идентификатора ссылки именованной константы.
В модели запроса SQL можно использовать перечисления наравне с другими предопределенными значениями. Модель использует сохраненное соответствие именованной константы перечисления и ее идентификатора в макете "Перечисления". Текст макета можно получить по команде "Конструктора модели запроса SQL" - "Макет "Перечисления"".
В большинстве случаев у вас не должно возникнуть проблем в получении макета. Обработка позволяет извлечь данные перечислений как с использованием конфигуратора (режим Конфигуратор), так и из файлов выгрузки (формат EDT/XML). При работе через конфигуратор нужно иметь в виду, что среди действующих расширений не должно быть расширений для перечислений. Это единственное ограничение, которое я обнаружил. Обойти его можно либо выключив предварительно такие расширения, либо через выгрузку файлов.
Обработка получения макета "Перечисления"
Процесс описания создания временной таблицы аналогичен как в модели запроса. Делается это оператором Поместить(ИмяВременнойТаблицы). Имя временной таблицы следует начинать с символа #. Если же использовать обычное имя таблицы, то тогда такая таблица будет создана в базе и уже не будет временной.
Есть еще один вариант создания временной таблицы, так называемая таблица локального использования. Это что-то типа вложенного запроса с именем. Использование такого варианта временной таблицы имеет свои ограничения, но иногда более оптимально. Оператор для такой таблицы будет: Использовать(ИмяВременногоЗапроса).
;// ЗАПРОС ПАКЕТА. #ВТ_ЗАКАЗЫ
МодельЗапроса.ЗапросПакета().Поместить("#ВТ_ЗАКАЗЫ")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Отбор(СтрШаблон("%1 = 0", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Проведен")))
.Поле("_ДемоЗаказПокупателя.Ссылка")
.Поле("_ДемоЗаказПокупателя.Проведен")
.Поле("_ДемоЗаказПокупателя.ПометкаУдаления")
;// ЗАПРОС ПАКЕТА. ВТ_ЗАКАЗЫ
МодельЗапроса.ЗапросПакета("ВТ_ЗАКАЗЫ")
.Выбрать()
.Источник("#ВТ_ЗАКАЗЫ", "ВТ_ЗАКАЗЫ")
.Поле("ВТ_ЗАКАЗЫ.Ссылка")
.Поле("ВТ_ЗАКАЗЫ.Проведен")
.Поле("ВТ_ЗАКАЗЫ.ПометкаУдаления")
;
Модель создания временной таблицы
-- ЗАПРОС ПАКЕТА 1. #ВТ_ЗАКАЗЫ
SELECT
_ДемоЗаказПокупателя._IDRRef AS Ссылка
, _ДемоЗаказПокупателя._Posted AS Проведен
, _ДемоЗаказПокупателя._Marked AS ПометкаУдаления
INTO #ВТ_ЗАКАЗЫ
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Posted = 0
-- ЗАПРОС ПАКЕТА 2. ВТ_ЗАКАЗЫ
SELECT
ВТ_ЗАКАЗЫ.Ссылка AS Ссылка
, ВТ_ЗАКАЗЫ.Проведен AS Проведен
, ВТ_ЗАКАЗЫ.ПометкаУдаления AS ПометкаУдаления
FROM
#ВТ_ЗАКАЗЫ AS ВТ_ЗАКАЗЫ
SQL запрос на создание временной таблицы
-- ЗАПРОС ПАКЕТА 1. #ВТ_ЗАКАЗЫ
WITH #ВТ_ЗАКАЗЫ AS (SELECT
_ДемоЗаказПокупателя._IDRRef AS Ссылка
, _ДемоЗаказПокупателя._Posted AS Проведен
, _ДемоЗаказПокупателя._Marked AS ПометкаУдаления
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Posted = 0)
-- ЗАПРОС ПАКЕТА 2. ВТ_ЗАКАЗЫ
SELECT
ВТ_ЗАКАЗЫ.Ссылка AS Ссылка
, ВТ_ЗАКАЗЫ.Проведен AS Проведен
, ВТ_ЗАКАЗЫ.ПометкаУдаления AS ПометкаУдаления
FROM
#ВТ_ЗАКАЗЫ AS ВТ_ЗАКАЗЫ
SQL запрос использование временного запроса
В качестве источника можно указать объект с данными. Это делается по аналогии с работой модели запроса. В модели запроса допускается вместо имени таблицы передавать таблицу-объект типа Таблица значений или Данные формы коллекция. Выглядит это так: .Источник(Таблица-объект, Псевдоним). В этой модели в качестве источника можно также указать таблицу JSON.
Конструирование текста запроса из таблицы-объекта может пригодится для задач переноса данных между серверами, когда прямым запросом это сделать невозможно. Так можно получить результат запроса на одном сервере, прочитать его в текст запроса SQL, а затем уже выполнить на другом сервере. На 2-м сервере данные будут загружены из самого текста запроса.
Например сделаем запрос к результату из примера с пустым договором:
;// ЗАПРОС ПАКЕТА. #ВТ_РЕЗУЛЬТАТ
МодельЗапроса.ЗапросПакета().Поместить("#ВТ_РЕЗУЛЬТАТ")
.Выбрать()
.Источник(Результат, "Результат")
.Поле("Номер")
.Поле("Дата")
.Поле("Организация")
.Поле("Контрагент")
.Поле("Договор")
.Поле("Проведен")
.Поле("ПометкаУдаления")
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("#ВТ_РЕЗУЛЬТАТ")
.Источник("Документ._ДемоЗаказПокупателя")
.ВнутреннееСоединение("#ВТ_РЕЗУЛЬТАТ", "_ДемоЗаказПокупателя")
.Связь("Номер, Дата")
.Поле("_ДемоЗаказПокупателя.Ссылка")
;
Создание временной таблицы из результата выполнения предыдущего запроса SQL (в Конструкторе модели запроса SQL поддерживается использование предопределенной переменной Результат)
-- ЗАПРОС ПАКЕТА 1. #ВТ_РЕЗУЛЬТАТ
SELECT
Номер AS Номер
, Дата AS Дата
, Организация AS Организация
, Контрагент AS Контрагент
, Договор AS Договор
, Проведен AS Проведен
, ПометкаУдаления AS ПометкаУдаления
INTO #ВТ_РЕЗУЛЬТАТ
FROM
(VALUES
('00-00000019', '4015-07-24 10:46:36', 0x00000000000000000000000000000000, 0x00000000000000000000000000000000, 0x00000000000000000000000000000000, 0x00, 0x00)
) AS Результат (Номер, Дата, Организация, Контрагент, Договор, Проведен, ПометкаУдаления)
-- ЗАПРОС ПАКЕТА 2. _ДемоЗаказПокупателя
SELECT
_ДемоЗаказПокупателя._IDRRef AS Ссылка
FROM
#ВТ_РЕЗУЛЬТАТ AS #ВТ_РЕЗУЛЬТАТ
INNER JOIN _Document39 AS _ДемоЗаказПокупателя
ON #ВТ_РЕЗУЛЬТАТ.Номер = _ДемоЗаказПокупателя._Number AND #ВТ_РЕЗУЛЬТАТ.Дата = _ДемоЗаказПокупателя._Date_Time
Запрос SQL, где значения таблицы результата моделируются в строках VALUES
{
"Колонки": [
{
"Имя": "Ссылка",
"ТипЗначения": "СТРОКА(34)"
}
],
"Строки": [
[
"0xB1ACE0CB4ED5F65511E531D81C1C93F5"
]
]
}
Результат выполнения запроса из временной таблицы, сконструированной из строк VALUES в самом запросе. Запрос можно выполнить на другом сервере, отличным от того, в котором был получен первоначальный результат
Уничтожение таблицы
Обычно уничтожение таблиц в запросах 1С подразумевает уничтожение временной таблицы. Однако в модели SQL такого ограничения нет. Следует быть очень осторожным с использованием этой команды! Для большей безопасности используйте ее только для временных таблиц, а чтобы не ошибиться ориентируйтесь на префикс # в имени временных таблиц (иначе это будет таблица, созданная в БД!).
;// ЗАПРОС ПАКЕТА. #ВТ_РЕЗУЛЬТАТ
МодельЗапроса.ЗапросПакета().Уничтожить("#ВТ_РЕЗУЛЬТАТ")
;
Уничтожение временной таблицы
-- ЗАПРОС ПАКЕТА 1. #ВТ_РЕЗУЛЬТАТ
DROP TABLE #ВТ_РЕЗУЛЬТАТ
SQL запрос уничтожение временной таблицы
Агрегатные функции
В модели поддерживаются агрегатные функции. Отдельные операторы агрегатных функций, по аналогии с моделью запроса 1С, здесь пока не реализованы.
В полях и условиях модели можно использовать следующие агрегатные функции: SUM, COUNT, MIN, MAX, AVG. Использование таких функций автоматически добавляет в запрос группирующие поля. В список полей группировки входят все поля, которые не содержат агрегатную функцию.
;// ЗАПРОС ПАКЕТА.
МодельЗапросаSQL.ЗапросПакета()
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Источник("Справочник._ДемоКонтрагенты")
.ВнутреннееСоединение("_ДемоЗаказПокупателя", "_ДемоКонтрагенты")
.Связь("Контрагент = Ссылка")
.Отбор(СтрШаблон("%1 = 0x01", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.Проведен")))
.Отбор(СтрШаблон("COUNT(DISTINCT %1) > 1", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.Ссылка")))
.Поле("_ДемоКонтрагенты.Наименование", "КонтрагентНаименование")
.Поле(СтрШаблон("COUNT(DISTINCT %1)", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.Ссылка")), "Колво")
;
Использование агрегатных функций
-- ЗАПРОС ПАКЕТА 1.
SELECT
_ДемоКонтрагенты._Description AS КонтрагентНаименование
, COUNT(DISTINCT _ДемоЗаказПокупателя._IDRRef) AS Колво
FROM
_Document39 AS _ДемоЗаказПокупателя
INNER JOIN _Reference15 AS _ДемоКонтрагенты
ON _ДемоЗаказПокупателя._Fld2908RRef = _ДемоКонтрагенты._IDRRef
WHERE
_ДемоЗаказПокупателя._Posted = 0x01
GROUP BY
_ДемоКонтрагенты._Description
HAVING
COUNT(DISTINCT _ДемоЗаказПокупателя._IDRRef) > 1
SQL запрос с агрегатными функциями в поле и в выражении отбора
{
"Колонки": [
{
"Имя": "КонтрагентНаименование",
"ТипЗначения": "СТРОКА(50)"
},
{
"Имя": "Колво",
"ТипЗначения": "ЧИСЛО(10)"
}
],
"Строки": [
[
"ООО \"Альфа\"",
3
]
]
}
Результат выполнения запроса SQL с агрегатными функциями
Запросы на удаление и изменение
Для удаления строк таблицы необходимо описать запрос с источниками и указать источник для изменения. При выполнении такого запроса будут удалены строки выбранного источника, вошедшие в запрос по условиям отбора или по ограничениям в соединениях.
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапроса.ИД()))
.Удалить("_ДемоЗаказПокупателя")
;
Модель запроса SQL на удаление строк таблицы _ДемоЗаказПокупателя с пустым договором
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
DELETE _ДемоЗаказПокупателя
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Fld2909RRef = 0x00000000000000000000000000000000
SQL запрос на удаление строк
Запрос на изменение данных похож на предыдущий запрос, только в этом случае будет не удаление строк, а изменение полей строк указанного источника.
Будьте внимательны! Если не указать поля для изменения, то запрос будет отработан как запрос на удаление!
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапроса.ИД()))
.Изменить("_ДемоЗаказПокупателя")
.Установить("ПометкаУдаления", "1")
;
Установка признака пометки удаления по заказам с пустым договором. Обратите внимание, что имя поля таблицы для изменения указывается без полного пути!
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
UPDATE _ДемоЗаказПокупателя
SET
_Marked = 1
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Fld2909RRef = 0x00000000000000000000000000000000
SQL запрос на обновление таблицы _ДемоЗаказПокупателя
В следующем примере устанавливаемое значение получается из логического выражения. Используемые поля в выражении требуется разименовать в поля хранения, поэтому используется шаблон с подстановкой:
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапроса.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Отбор(СтрШаблон("%1 = %2", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапроса.ИД()))
.Изменить("_ДемоЗаказПокупателя")
.Установить("ПометкаУдаления", СтрШаблон("CASE WHEN %1 = 0 THEN 1 ELSE 0 END", МодельЗапроса.ИмяПоляХранения("_ДемоЗаказПокупателя.Проведен")))
;
Модель запроса на изменение. Выражение значения получается путем разименования полей запроса в поля хранения
-- ЗАПРОС ПАКЕТА 1. _ДемоЗаказПокупателя
UPDATE _ДемоЗаказПокупателя
SET
_Marked = CASE WHEN _ДемоЗаказПокупателя._Posted = 0 THEN 1 ELSE 0 END
FROM
_Document39 AS _ДемоЗаказПокупателя
WHERE
_ДемоЗаказПокупателя._Fld2909RRef = 0x00000000000000000000000000000000
SQL Запрос на установку значения с использованием логического выражения
Модель запроса SQL доступна только на сервере, однако полученный запрос SQL из модели можно выполнять в разных контекстах.
Модель может возвращать текст запроса в разных вариантах:
- ПолучитьТекстЗапроса() - последний запрос пакета,
- ПолучитьТекстЗапросаПакета() - полный текст запроса пакета,
- ПолучитьПакетЗапросов() - массив запросов пакета, где элемент массива - структура вида {Имя, ТекстЗапроса}.
Полученный из модели текст запроса SQL уже можно вернуть в контекст клиента или продолжить использовать на сервере. Пример работы в режиме выполнения запроса на клиенте приложен в демо обработке "Демо Модель запроса SQL".
Основное назначение конструктора - получить текст запроса из готовой модели запроса и проанализировать его выполнение. Если запрос выполняется, значит нет синтаксических ошибок. Также это удобно для исследования работы запроса по модели. Это удобно для отладки запроса SQL.
Полученный текст запроса SQL можно скопировать в консоль выполнения и продолжить работу уже в ней.
Текст модели удобно получать из конструктора модели запроса 1С. В этом конструкторе можно использовать конструктор запроса 1С, затем получить модель запроса. Полученную таким образом модель можно уже использовать в модели запроса SQL. Для того, чтобы модель SQL работала необходимо выполнение следующих условий: нужно заменить все обращения к предопределенным значениям на вызов функции МодельЗапросаSQL.ИД(ПредопределенноеЗначение), заменить все обращения к полям таблиц 1С в выраженях на шаблон с подстановкой имени поля хранения МодельЗапросаSQL.ИмяПоляХранения("ПутьКПолю").
Внешний вид обработки "Конструктор модели запроса SQL"
Демо пример сценария использования модели запроса SQL
Обработка с демо примером
В представленном демо-примере демонстрируется следующий сценарий использования модели. Вначеле выполняется запрос на чтение данных, затем результат переиспользуется во 2-м запросе для изменения.
В обработке есть две команды: первая выполняет обработку по сценарию, а вторая возвращает данные к исходному состоянию.
Обработка заключается в получении списка заказов, где незаполнен договор и нет пометки удаления, через соединение 1. Результат выполнения (список заказов) передается во второе соединение 2 и по сопоставленным заказам выполняется запись пометки удаления. Отмена изменений заключается в снятии пометки удаления по заказам с незаполненным договором. В обработке используется три запроса: получение непомеченных заказов с пустым договором, изменяющий запрос установки пометки на удаление и изменяющий запрос снятия пометки на удаление.
&НаСервере
Функция ТекстЗапросаНеПомеченныеЗаказыСПустымДоговором(ПараметрыСоединения)
МодельЗапросаSQL = Общий.МодельЗапросаSQL();
МодельЗапросаSQL
.Схема1С(ПараметрыСоединения.Схема1С)
.Схема(ПараметрыСоединения.Схема)
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапросаSQL.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Поле("_ДемоЗаказПокупателя.Номер")
.Поле("_ДемоЗаказПокупателя.Дата")
.Поле("_ДемоЗаказПокупателя.Организация")
.Поле("_ДемоЗаказПокупателя.Контрагент")
.Поле("_ДемоЗаказПокупателя.Договор")
.Поле("_ДемоЗаказПокупателя.Проведен")
.Поле("_ДемоЗаказПокупателя.ПометкаУдаления")
.Отбор(СтрШаблон("%1 = %2", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапросаSQL.ИД()))
.Отбор(СтрШаблон("%1 = 0x00", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.ПометкаУдаления")))
;
ТекстЗапроса = МодельЗапросаSQL.ПолучитьТекстЗапроса();
Возврат ТекстЗапроса;
КонецФункции
Получение запроса из модели. Непомеченные заказы с пустым договором
&НаСервере
Функция ТекстЗапросаПометитьНаУдалениеЗаказыИзРезультата(ПараметрыСоединения, Таблица)
МодельЗапросаSQL = Общий.МодельЗапросаSQL();
МодельЗапросаSQL
.Схема1С(ПараметрыСоединения.Схема1С)
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапросаSQL.ЗапросПакета("_ДемоЗаказПокупателя").Поместить("#ТАБЛИЦА")
.Выбрать()
.Источник(Таблица, "Результат")
.Поле("Номер")
.Поле("Дата")
.Поле("Организация")
.Поле("Контрагент")
.Поле("Договор")
.Поле("Проведен")
.Поле("ПометкаУдаления")
;
МодельЗапросаSQL.ЗапросПакета()
.Выбрать().Различные()
.Источник("#ТАБЛИЦА")
.Источник("Документ._ДемоЗаказПокупателя")
.ВнутреннееСоединение("#ТАБЛИЦА", "_ДемоЗаказПокупателя")
.Связь("Номер = Номер")
.УсловиеСвязи(СтрШаблон("%1 = 0x00", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.ПометкаУдаления")))
.Изменить("_ДемоЗаказПокупателя")
.Установить("ПометкаУдаления", "0x01")
;
ТекстЗапроса = МодельЗапросаSQL.ПолучитьТекстЗапросаПакета();
Возврат ТекстЗапроса;
КонецФункции
Получение изменяющего запроса из модели. Пометка на удаление заказов, полученных из результата предыдущего запроса
&НаСервере
Функция ТекстЗапросаУбратьПометкуУдаленияСЗаказовСПустымДоговором(ПараметрыСоединения)
МодельЗапросаSQL = Общий.МодельЗапросаSQL();
МодельЗапросаSQL
.Схема1С(ПараметрыСоединения.Схема1С)
.Схема(ПараметрыСоединения.Схема)
;// ЗАПРОС ПАКЕТА. _ДемоЗаказПокупателя
МодельЗапросаSQL.ЗапросПакета("_ДемоЗаказПокупателя")
.Выбрать()
.Источник("Документ._ДемоЗаказПокупателя")
.Отбор(СтрШаблон("%1 = %2", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.Договор"), МодельЗапросаSQL.ИД()))
.Отбор(СтрШаблон("%1 = 0x01", МодельЗапросаSQL.ИмяПоляХранения("_ДемоЗаказПокупателя.ПометкаУдаления")))
.Изменить("_ДемоЗаказПокупателя")
.Установить("ПометкаУдаления", "0x00")
;
ТекстЗапроса = МодельЗапросаSQL.ПолучитьТекстЗапроса();
Возврат ТекстЗапроса;
КонецФункции
Получение изменяющего запроса из модели. Сброс пометки удаления по заказам с пустым договором
Использование соединений 1, 2 демонстрирует возможность переноса данных между различными серверами через промежуточный результат запроса.
Поставка
Разработка проекта ведется в EDT с использованием платформы 8.3.23. При объединении с конфигурацией сбросьте признак "Объединять свойства конфигурации". Для варианта с расширением установите режим совместимости согласно версии вашей платформы.
Состав:
Конфигурация или расширение |
Общие модули: Общий, ОбщийКлиент, ОбщийКлиентСервер, РаботаС_ADODB Обработка: МодельЗапросаSQL, КонструкторМоделиЗапросаSQL |
Подсистемы | МПодсистемы.МодельЗапросаSQL |
Зависимости |
|
Текущие ограничения | Не поддерживаются вложенные запросы |
Проект выложен на github.
Обновление от 29.06.2023
Добавлена поддержка объединений и параметров запроса по аналогии как в модели запроса 1С.
Теперь в запросе можно использовать несколько операторов Выбрать, Объединить/ОбъединитьВсе.
Можно использовать параметры в тексте запроса. В самом тексте параметр указывается с префиксом @, установка параметров через оператор Параметр(ИмяПараметра, Значение).
Модель DSL:
МодельЗапросаSQL
.Параметр("ActID", "d5ae0dea-7bc8-4e9a-833a-3f45e8b7d45f")
;
Текст запроса:
-- ПАРАМЕТРЫ
DECLARE @ActID nvarchar(36) = N'd5ae0dea-7bc8-4e9a-833a-3f45e8b7d45f'
Обновление от 23.08.2023
Добавлена поддержка виртуальной таблицы остатков. Поля ресурсов нужно использовать без окончания Остаток.
Модель DSL:
;// ЗАПРОС ПАКЕТА. ОстаткиТоваров
МодельЗапроса.ЗапросПакета("ОстаткиТоваров")
.Выбрать()
.Источник("РегистрНакопления._ДемоОстаткиТоваровВМестахХранения.Остатки", "ОстаткиТоваров")
.Поле("ОстаткиТоваров.КоличествоОстаток")
.Поле("ОстаткиТоваров.Номенклатура")
.Поле("ОстаткиТоваров.МестоХранения")
.Поле("ОстаткиТоваров.Организация")
.Поле("ОстаткиТоваров.Количество", "КоличествоОстаток")
;// Обработка результата
//МодельЗапроса.ВыполнитьЗапрос();
//Результат = МодельЗапроса.Результат("ОстаткиТоваров");
Запрос SQL:
-- ЗАПРОС ПАКЕТА 1. ОстаткиТоваров
SELECT
ОстаткиТоваров.КоличествоОстаток AS КоличествоОстаток
, ОстаткиТоваров._Fld508RRef AS Номенклатура
, ОстаткиТоваров._Fld507RRef AS МестоХранения
, ОстаткиТоваров._Fld506RRef AS Организация
, ОстаткиТоваров._Fld509 AS КоличествоОстаток
FROM
_AccumRgT510 AS ОстаткиТоваров