При переходе на управляемое приложение разработчики часто сталкиваются с трудностями понимания клиент-серверного взаимодействия. Типичная задача — вывод актуальных остатков товара в табличную часть документа при выборе или изменении номенклатуры. Для её решения необходимо четко понимать, где должен выполняться код, какие данные передавать между клиентом и сервером и как оптимизировать этот процесс для обеспечения максимального быстродействия системы.
В этой статье мы подробно рассмотрим, как правильно организовать клиент-серверное взаимодействие, какую директиву выполнения выбрать, как спроектировать оптимальный алгоритм работы программы и как написать быстрый запрос к базе данных без потери производительности.
Прежде чем перейти к написанию кода, давайте разберем фундаментальный принцип работы платформы 1С:Предприятие 8. Клиентское приложение (тонкий клиент, веб-клиент) отвечает исключительно за отображение интерфейса и непосредственное взаимодействие с пользователем. У него нет прямого доступа к базе данных. Проводить сложные расчеты, делать запросы к таблицам остатков и обращаться к регистрам накопления на клиенте невозможно.
Для выполнения любых операций с базой данных нам требуется обращаться к серверу. Для этого в модуле управляемой формы используются специальные директивы компиляции:
Для решения нашей задачи — получения остатков при изменении номенклатуры — мы обязаны использовать связку: на клиенте перехватывать событие изменения строки, а на сервере выполнять запрос к регистру остатков.
Выбор правильного типа серверного вызова критически важен для производительности всей системы. Платформа предоставляет нам два варианта серверных процедур в модуле формы:
1. Контекстный вызов (директива &НаСервере)
При использовании этой директивы серверной процедуре доступен весь контекст формы, включая реквизит Объект (содержимое шапки документа, все строки табличной части) и параметры формы. Вы можете напрямую написать в коде Объект.Склад или Объект.Товары. Однако за это удобство приходится платить производительностью. При каждом таком вызове платформа автоматически выполняет следующие действия:
Если в табличной части вашего документа содержатся сотни строк, контекстный вызов приведет к передаче огромных объемов избыточных данных по сети, что вызовет заметные задержки («зависания» интерфейса) у конечного пользователя.
2. Внеконтекстный вызов (директива &НаСервереБезКонтекста)
При использовании этой директивы серверная процедура или функция полностью изолирована от контекста формы. Ей недоступен реквизит Объект. Все необходимые для расчета данные вы должны явно передать в качестве параметров при вызове функции. Платформа отправляет на сервер только эти параметры и возвращает обратно только результат вычислений. Это работает в разы быстрее, экономит сетевой трафик и серверные ресурсы.
Вывод: для получения остатка конкретного товара по складу и дате идеальным решением является использование внеконтекстного вызова &НаСервереБезКонтекста.
Поскольку при использовании внеконтекстного вызова у сервера нет доступа к элементам формы, мы должны построить алгоритм взаимодействия следующим образом:
ПриИзменении для поля номенклатуры в строке табличной части.Номенклатуру, а из шапки документа — Склад и Дату.Рассмотрим детальный пример реализации этого алгоритма:
&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
// Получаем строку табличной части, в которой произошло изменение
СтрокаТЧ = Элементы.Товары.ТекущиеДанные;
Если СтрокаТЧ <> Неопределено Тогда
// Проверяем заполненность ключевых реквизитов
Если ЗначениеЗаполнено(СтрокаТЧ.Номенклатура) Тогда
// Вызываем внеконтекстную функцию сервера, передавая параметры
ОстатокТовара = ПолучитьОстатокТовараНаСервере(
СтрокаТЧ.Номенклатура,
Объект.Склад,
Объект.Дата
);
// Заполняем полученное значение в строку на клиенте
СтрокаТЧ.Остаток = ОстатокТовара;
Иначе
СтрокаТЧ.Остаток = 0;
КонецЕсли;
КонецЕсли;
EndProcedure
&НаСервереБезКонтекста
Функция ПолучитьОстатокТовараНаСервере(Знач Номенклатура, Знач Склад, Знач Дата)
// Вызываем логику получения остатка из базы данных
Возврат ПолучитьОстатокПоБазе(Номенклатура, Склад, Дата);
КонецФункции
Проанализируем архитектурную сторону вопроса. В процессе разработки у вас может возникнуть вопрос: где именно должна располагаться функция получения остатка — непосредственно в модуле формы документа или в общем модуле системы?
Стандарты разработки фирмы «1С» рекомендуют руководствоваться правилом повторного использования кода:
&НаСервереБезКонтекста непосредственно в модуле формы.Правильным решением будет вынесение всей логики расчета остатков в серверный Общий модуль (например, с именем ЗапасыСервер или РаботаСТоварамиСервер). У этого модуля в свойствах должен быть установлен флаг «Сервер» и снят флаг «Клиент». Сама функция внутри общего модуля должна быть объявлена с ключевым словом Экспорт. В этом случае вы сможете легко обращаться к ней из любой формы вашей конфигурации.
Вызов сервера без контекста — это лишь половина успеха. Если сам запрос к базе данных написан неоптимально, система будет работать медленно. Разберем основные правила построения запросов для расчета остатков:
Остатки (например, РегистрНакопления.ТоварыНаСкладах.Остатки).ГДЕ (WHERE) после обращения к таблице, СУБД сначала прочитает остатки по абсолютно всем товарам на всех складах компании, и только потом отсечет лишнее. Это приводит к колоссальной нагрузке на сервер. Условия нужно передавать строго внутрь параметров виртуальной таблицы.Проанализируем правильный пример реализации серверной функции с запросом:
Функция ПолучитьОстатокПоБазе(Номенклатура, Склад, Дата) Экспорт
// Если склад или номенклатура не заполнены, нет смысла делать запрос
Если Не ЗначениеЗаполнено(Номенклатура) Или Не ЗначениеЗаполнено(Склад) Тогда
Возврат 0;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.В НаличииОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Номенклатура = &Номенклатура И Склад = &Склад) КАК ТоварыНаСкладахОстатки";
// Заполняем параметры запроса
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Запрос.УстановитьПараметр("Склад", Склад);
// Для получения оперативных (текущих) остатков передаем Неопределено в качестве периода,
// либо передаем конкретную дату документа
Запрос.УстановитьПараметр("Период", ?(ЗначениеЗаполнено(Дата), Дата, Неопределено));
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.Остаток;
КонецЕсли;
Возврат 0;
КонецФункции
Выясним причину еще одной неочевидной проблемы. Если пользователь редактирует уже существующий и ранее проведенный документ, его движения уже записаны в регистр накопления. Если мы просто передадим в параметры виртуальной таблицы остатков дату этого документа (Объект.Дата), то запрос вернет остаток товара, в который уже включены движения текущего документа.
Чтобы получить «чистый» остаток товара, который был на складе непосредственно до проведения текущего документа, необходимо использовать специальный объект платформы — Граница, указав вид границы «Исключая».
Посмотрим, как модифицировать код установки параметра периода в запросе:
// Корректная обработка даты для проведенного документа
Если ЗначениеЗаполнено(Дата) Тогда
// Использование Границы исключает влияние движений самого документа на этой секунде
ГраницаОстатков = Новый Граница(Дата, ВидГраницы.Исключая);
Запрос.УстановитьПараметр("Период", ГраницаОстатков);
Иначе
Запрос.УстановитьПараметр("Период", Неопределено);
КонецЕсли;
Использование объекта Граница гарантирует получение актуальных и достоверных данных об остатках при интерактивной корректировке табличных частей документов пользователями.