Клиент или сервер: что где выполняется

Один из первых вопросов на собеседовании: покажите код с директивами компиляции и спросите, где что выполнится.

Клиент-серверная архитектура 1С — модель, в которой интерфейс (формы, кнопки, поля) живёт на клиенте, а работа с базой данных идёт только на сервере; между ними код «переезжает» по директивам компиляции.

Что вообще спрашивают

Работа пользователя в 1С устроена так: тонкий клиент (или веб-клиент) показывает формы на компьютере сотрудника, а кластер серверов держит соединение с СУБД и выполняет тяжёлую логику. Клиент и сервер — это два разных процесса, часто на разных машинах. Поэтому один и тот же модуль формы физически компилируется в двух местах, и разработчик директивами указывает, где именно должна оказаться каждая процедура.

На собеседовании обычно дают маленький фрагмент и спрашивают: «Где выполнится эта строка? Почему тут ошибка компиляции?». Проверяют не зубрёжку, а понимаете ли вы, что клиент и сервер разделены не условно, а по-настоящему.

Ключевые директивы компиляции в модуле управляемой формы:

  • &НаКлиенте — процедура выполняется на компьютере пользователя. Здесь доступны элементы формы, диалоги, ОткрытьФорму, но нет прямого доступа к базе.
  • &НаСервере — выполняется на сервере, есть доступ к базе и к данным формы (реквизитам). На сервер целиком передаётся контекст формы.
  • &НаСервереБезКонтекста — тоже сервер, но контекст формы не передаётся: быстрее и дешевле, но внутри не видно реквизитов формы — только переданные параметры.

Живой пример

Пользователь нажал кнопку. Обработчик всегда клиентский — интерфейс живёт на клиенте. Но чтобы прочитать данные из базы, нужно «сходить» на сервер:

&НаКлиенте
Процедура ЗаполнитьЦенуПоТовару(Команда)
    // Мы на клиенте: базы тут нет, поэтому вызываем серверный метод
    Если НЕ ЗначениеЗаполнено(Объект.Товар) Тогда
        Сообщить("Сначала выберите товар");
        Возврат;
    КонецЕсли;
    Объект.Цена = ПолучитьЦенуСервер(Объект.Товар);
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьЦенуСервер(СсылкаНаТовар)
    // Мы на сервере: доступ к базе есть
    Запрос = Новый Запрос;
    Запрос.Текст =
        "ВЫБРАТЬ Цена ИЗ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, Номенклатура = &Товар)";
    Запрос.УстановитьПараметр("Дата", ТекущаяДата());
    Запрос.УстановитьПараметр("Товар", СсылкаНаТовар);
    Выборка = Запрос.Выполнить().Выбрать();
    Возврат ?(Выборка.Следующий(), Выборка.Цена, 0);
КонецФункции

Обратите внимание: параметр в серверную функцию мы передали явно (СсылкаНаТовар), потому что выбрали &НаСервереБезКонтекста — внутри неё реквизит Объект.Товар недоступен.

Почему нельзя обращаться к базе с клиента

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

  • Безопасность. Если бы клиент напрямую видел СУБД, любой пользователь мог бы обойти права и бизнес-логику. Сервер — единственный «привратник» к данным.
  • Архитектура. Клиент вообще не держит соединение с базой данных — его держит рабочий процесс кластера. Строка запроса на клиенте просто не скомпилируется.
  • Производительность и масштаб. Тяжёлые выборки должны идти рядом с базой, а не гонять терабайты по сети на слабый ноутбук кассира.
&НаКлиенте
Процедура НеправильноЧитаемБазу()
    // ОШИБКА КОМПИЛЯЦИИ: объект "Запрос" недоступен на клиенте
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Контрагенты";
    Результат = Запрос.Выполнить();
КонецПроцедуры

Результат:

Ошибка компиляции модуля: обращение к процедуре (функции) как к переменной (Запрос)
— тип "Запрос" недоступен в контексте "НаКлиенте".

Как это работает: цена вызова сервера

Каждый переход с клиента на сервер — это серверный вызов: сериализация данных, передача по сети, разбор на другой стороне. Один вызов — это доли секунды, но если вызвать сервер в цикле по строкам таблицы, набегают секунды и минуты «тормозов».

Отсюда правило: минимизируйте число серверных вызовов. Плохо — дёргать сервер на каждую строку; хорошо — собрать данные и сходить один раз. Здесь же всплывает разница контекстов: вызов &НаСервере тащит на сервер и обратно весь контекст формы (все реквизиты, таблицы) — это дороже, чем &НаСервереБезКонтекста, куда едут только нужные параметры.

// ПЛОХО: сервер дёргается на каждую строку — N вызовов
&НаКлиенте
Процедура ЗаполнитьЦеныПлохо()
    Для Каждого Строка Из Объект.Товары Цикл
        Строка.Цена = ПолучитьЦенуСервер(Строка.Номенклатура); // вызов сервера в цикле!
    КонецЦикла;
КонецПроцедуры

// ХОРОШО: один серверный вызов на всю таблицу
&НаКлиенте
Процедура ЗаполнитьЦеныХорошо()
    ЗаполнитьВсеЦеныНаСервере(); // внутри — один проход по таблице формы
КонецПроцедуры

Как отвечать

Скелет сильного ответа: «Клиент отвечает за интерфейс, сервер — за данные. Директивой я задаю, где компилируется процедура. С клиента к базе обратиться нельзя: там просто нет объектов вроде Запрос, и это правильно с точки зрения безопасности. Каждый переход на сервер — дорогой серверный вызов, поэтому я стараюсь ходить на сервер редко и, где можно, использую &НаСервереБезКонтекста, чтобы не гонять контекст формы».

Если попросят пример из практики — расскажите про заполнение табличной части: наивный код вызывает сервер в цикле и тормозит, а правильный делает один вызов и обрабатывает все строки на сервере.

Частые ошибки на собеседовании

  • Говорить «код выполняется там, где написан». Нет — выполняется там, где указывает директива, а не где физически лежит строка в модуле.
  • Путать &НаСервере и &НаСервереБезКонтекста или считать, что разницы нет. Разница именно в передаче контекста формы, и она влияет на производительность.
  • Забывать про цену серверного вызова и спокойно ставить обращение к серверу внутрь цикла.
  • Утверждать, что «с клиента к базе можно, просто медленно». Нельзя вообще — не скомпилируется.

Итоги

  • Клиент — интерфейс, сервер — данные; директива компиляции решает, где выполнится процедура.
  • С клиента нет доступа к базе (нет объекта Запрос и т.п.) — по соображениям безопасности и архитектуры.
  • Серверный вызов — дорогая операция; вызовы в цикле — классический источник тормозов.
  • &НаСервереБезКонтекста дешевле &НаСервере, потому что не передаёт контекст формы.
Проверьте себя
1. В модуле управляемой формы процедура помечена &НаКлиенте. Что произойдёт, если внутри написать: Запрос = Новый Запрос;?
AКод выполнится, но медленно, потому что запрос уйдёт на сервер по сети
BОшибка компиляции: объект Запрос недоступен в клиентском контексте
CЗапрос выполнится прямо на клиенте, соединившись с СУБД напрямую
DНичего, строка просто проигнорируется во время выполнения
2. Чем &НаСервереБезКонтекста отличается от &НаСервере?
AНичем, это синонимы
BБезКонтекста выполняется на клиенте, а НаСервере — на сервере
CБезКонтекста не передаёт на сервер контекст формы (реквизиты, таблицы), поэтому вызов дешевле
DНаСервере не имеет доступа к базе, а БезКонтекста имеет