Что происходит с кодом 1С от модуля до записи в базу
Содержание
Язык 1С не компилируется в машинный код. Платформа 1С:Предприятие 8.3 переводит текст модуля в промежуточный байт-код и исполняет его своей виртуальной машиной — на клиенте или на сервере, в зависимости от директив компиляции. Объекты, с которыми работает код (справочники, документы, регистры), платформа создаёт сама по метаданным конфигурации, а запросы переводит в SQL конкретной СУБД.
Частое заблуждение — считать встроенный язык «скриптом поверх таблиц», где строка кода сразу меняет данные. На деле между Объект.Записать() и строкой в таблице СУБД стоят транзакция, обработчики событий, блокировки и проверка прав. Пока разработчик этого не видит, он пишет код, который работает на одном пользователе и падает на двадцати.
Как платформа 1С превращает текст модуля в исполняемый код
Каждый модуль — общий, объекта, менеджера, формы — хранится в конфигурации как текст. При первом обращении к модулю платформа компилирует его во внутреннее представление и дальше исполняет уже его. Повторной компиляции при каждом вызове нет: скомпилированный модуль живёт в памяти процесса.
Что из этого следует на практике:
- 🧩 Синтаксис проверяется до запуска. Ошибку вида
Переменная не определенав тексте модуля конфигуратор покажет при синтаксическом контроле, но ошибку обращения к несуществующему реквизиту объекта — только в момент выполнения. Язык динамически типизирован. - 🔀 Тип переменной определяется значением. Одна и та же переменная может хранить сначала число, потом строку. Объявлять тип нельзя, поэтому проверку делают явно:
ТипЗнч(Значение) = Тип("Число"). - 📦 Можно поставлять без исходников. В режиме поставки конфигурации модули разрешено сохранить без исходных текстов — в базу попадёт только скомпилированный код. Читать и править такой модуль нельзя.
- ⚙️ Код компилируется отдельно для клиента и сервера. Один и тот же модуль формы даёт две части: клиентскую и серверную. Об этом подробно ниже.
Перед компиляцией работает препроцессор. Его инструкции начинаются с # и вырезают куски кода под нужное окружение:
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
// код, который нужен только там, где есть доступ к базе
#КонецЕсли
#Область СлужебныеПроцедурыИФункции
// области не влияют на исполнение, только на навигацию по модулю
#КонецОбласти
Инструкция #Если не выполняется в рантайме. Отрезанный ею код просто не попадает в скомпилированный модуль нужного окружения — это дешевле, чем проверка условия в каждом вызове.
Объектная модель встроенного языка: метаданные, типы и контекст
Разработчик не создаёт классы. Он добавляет в дерево конфигурации объект метаданных, например справочник «Контрагенты», а платформа сама порождает для него набор типов. Для справочника это:
| Тип | Что это | Пример обращения |
|---|---|---|
| Менеджер | точка входа: поиск, создание, выборка | Справочники.Контрагенты |
| Ссылка | указатель на запись в базе, лёгкий | СправочникСсылка.Контрагенты |
| Объект | вся запись в памяти, можно менять и записывать | СправочникОбъект.Контрагенты |
| Выборка | курсор для обхода записей | СправочникВыборка.Контрагенты |
Аналогичная четвёрка есть у документов, планов видов характеристик, планов счетов. У регистров вместо объекта — набор записей, а вместо ссылки — ключ записи.
Ссылка и объект — главная развилка производительности
Ссылка хранит только идентификатор. Когда вы пишете Контрагент.ИНН, платформа читает данные из базы и держит их в кэше. Это удобно, но каждое такое обращение через точку — потенциальный запрос.
Ссылка.ПолучитьОбъект() читает запись целиком, со всеми табличными частями. Объект нужен, только если запись будут менять. Для чтения одного поля это расточительно — для отчёта по тысяче документов тем более.
Где живёт код объекта
- 📄 Модуль объекта — обработчики записи и проведения:
ПередЗаписью,ПриЗаписи,ОбработкаПроведения,ОбработкаПроверкиЗаполнения. - 🗂 Модуль менеджера — процедуры, которым не нужен конкретный экземпляр: печать, заполнение по умолчанию, служебные выборки.
- 🖥 Модуль формы — поведение формы и её реквизитов.
- 🌐 Общие модули — библиотека процедур, доступная из любого места с учётом флагов
Клиент,Сервер,Вызов сервера,Привилегированный,Глобальный. - 🚪 Модуль сеанса, модуль управляемого приложения, модуль внешнего соединения — события запуска и завершения в соответствующем окружении.
Всё, что объявлено в глобальном контексте (свойства вроде Справочники, Документы, РегистрыСведений, методы Сообщить, ТекущаяДата, глобальные общие модули), доступно без префикса. Набор доступного зависит от того, где выполняется строка: на клиенте свойства Справочники с методами поиска нет.
Память и циклические ссылки
Платформа освобождает объекты по счётчику ссылок. Если структура хранит ссылку на саму себя или два объекта ссылаются друг на друга, память не освободится до конца сеанса. На долгих фоновых заданиях это даёт рост памяти рабочего процесса, который выглядит как утечка платформы.
Где выполняется код: клиент, сервер и директивы компиляции
В управляемом приложении клиент не имеет прямого доступа к базе. Объекты, запросы, регистры живут только на сервере. Поэтому у каждой процедуры модуля формы стоит директива, которая говорит компилятору, куда её отнести.
| Директива | Где выполняется | Что передаётся на сервер |
|---|---|---|
&НаКлиенте | на клиенте | ничего, вызова сервера нет |
&НаСервере | на сервере | данные формы целиком (контекст) |
&НаСервереБезКонтекста | на сервере | только параметры процедуры |
&НаКлиентеНаСервереБезКонтекста | там, откуда вызвали | — |
Каждый переход с клиента на сервер — это сетевой вызов. Платформа упаковывает данные формы, отправляет их, на сервере восстанавливает контекст, выполняет код и везёт изменённые данные обратно. На тонком клиенте через интернет один такой вызов заметен глазу.
Правила, которые знает каждый, кто оптимизировал формы:
- Собирайте серверную работу в один вызов. Три последовательных
&НаСерверев обработчике кнопки — это три круга по сети. - Если процедуре не нужны реквизиты формы, ставьте
&НаСервереБезКонтекста. Тогда форма не сериализуется. - Через границу клиент — сервер ходят только сериализуемые значения: примитивы, ссылки,
Структура,Массив,ДанныеФормы....СправочникОбъектилиЗапросна клиент передать нельзя. - Для общих модулей с повторяющимися неизменными данными используйте свойство «Повторное использование возвращаемых значений» (
На время вызоваилиНа время сеанса). Функция выполнится один раз, дальше результат берётся из кэша.
Флаг Привилегированный у общего модуля отключает проверку прав для его кода. Помещайте туда только то, что действительно должно работать в обход ролей, и не принимайте в такие процедуры произвольный текст запроса от клиента.
Фоновые задания — единственный способ параллельности
Код одного сеанса выполняется в одном потоке. Если нужно параллельно пересчитать данные по десяти организациям, запускают фоновые задания: ФоновыеЗадания.Выполнить("ИмяМодуля.ИмяПроцедуры", Параметры). Каждое задание — отдельный сеанс на сервере со своей транзакцией и своими блокировками. Регламентные задания работают так же, только запускаются по расписанию.
На клиенте долгие операции делают асинхронными. Начиная с 8.3.18 для этого есть методы с суффиксом Асинх и оператор Ждать — вместо цепочек ОписаниеОповещения.
Работа с данными: как язык запросов 1С превращается в SQL
Язык запросов похож на SQL, но обращается к метаданным, а не к таблицам. Вы пишете ИЗ Документ.РеализацияТоваровУслуг, а платформа подставляет реальную таблицу СУБД с техническим именем вроде _Document123. В файловом варианте запрос исполняет собственный движок платформы, в клиент-серверном — СУБД: MS SQL Server, PostgreSQL, IBM Db2 или Oracle.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, Склад = &Склад) КАК Остатки";
Запрос.УстановитьПараметр("НаДату", ТекущаяДатаСеанса());
Запрос.УстановитьПараметр("Склад", Склад);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// обработка строки
КонецЦикла;
Что платформа делает за вас и о чём надо помнить:
- 📊 Виртуальные таблицы.
Остатки,Обороты,ОстаткиИОбороты,СрезПоследнихсобираются из итогов регистра. Отбор нужно писать в параметрах виртуальной таблицы, какСклад = &Складвыше, а не вГДЕ— иначе СУБД сначала посчитает остатки по всем складам. - 🔗 Разыменование полей.
Документ.Контрагент.ОсновнойДоговор.Валютав запросе — это неявные левые соединения. Для составного типа платформа соединяет со всеми возможными таблицами, и запрос раздувается. - 🔐 Права. Если в ролях настроены ограничения на уровне записей (RLS), платформа дописывает условия в каждый запрос. Отключить их в запросе можно только в привилегированном режиме.
- 🧮 Временные таблицы.
МенеджерВременныхТаблици пакетные запросы позволяют подготовить данные один раз и использовать в нескольких запросах.
Запрос внутри цикла — главный убийца производительности 1С. Тысяча документов в цикле с запросом в каждой итерации — тысяча обращений к СУБД. Тот же результат даёт один запрос с параметром-массивом: ГДЕ Ссылка В (&МассивДокументов).
Транзакции и блокировки данных в 1С
Каждый вызов Записать() у объекта платформа сама заворачивает в транзакцию. Внутрь неё попадают ПередЗаписью, сама запись, ПриЗаписи, а для документа при проведении — ещё ОбработкаПроведения и запись движений. Ошибка в любом месте откатывает всё целиком.
Явная транзакция нужна, когда в одну логическую операцию входят несколько записей:
НачатьТранзакцию();
Попытка
// блокировка, чтение, запись нескольких объектов
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Вложенных транзакций в 1С по сути нет. Если внутри уже открытой транзакции вызвать НачатьТранзакцию(), платформа лишь увеличит счётчик. Ошибка на любом уровне помечает всю транзакцию к отмене, и зафиксировать её уже нельзя — отсюда сообщение В данной транзакции уже происходили ошибки. Поэтому исключение внутри транзакции не «глушат» пустой веткой Исключение, а пробрасывают выше.
Управляемые блокировки
Режим блокировок задаётся в свойствах конфигурации: автоматический, управляемый или смешанный. Типовые конфигурации на 8.3 работают в управляемом режиме. В нём СУБД не держит лишних блокировок, а целостность обеспечивает сам разработчик через объект БлокировкаДанных:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Склад", Склад);
Блокировка.Заблокировать();
// только теперь читаем остатки и контролируем их
Ключевой приём проведения: сначала блокировка, потом чтение остатков. Если прочитать остаток без блокировки, два документа одновременно увидят одни и те же 10 штук и оба спишут по 8 — в минус уйдёт 6.
Блокировка живёт до конца транзакции. Если другой сеанс держит пересекающуюся блокировку, текущий ждёт и по истечении таймаута получает ошибку о превышении времени ожидания установки блокировки. Два сеанса, которые блокируют одни и те же ресурсы в разном порядке, ловят взаимоблокировку — платформа отменит одну из транзакций.
Объектные блокировки
Это отдельный механизм, не связанный с транзакциями. Он защищает от одновременной правки одного элемента:
- Оптимистическая. Работает всегда. При записи платформа сверяет версию данных, и если запись уже изменил другой пользователь, выдаёт ошибку записи вместо тихой перезаписи чужих правок.
- Пессимистическая. Вызов
Объект.Заблокировать()или открытая на редактирование форма. Второй пользователь при попытке изменить элемент получит сообщение, что объект уже заблокирован.
Внешние компоненты, COM и другие способы расширить язык
Встроенный язык закрыт: нельзя подключить произвольную DLL и вызвать её функцию. Всё, что платформа не умеет сама, подключают через внешние компоненты или стандартные механизмы интеграции.
| Способ | Где работает | Когда выбирать |
|---|---|---|
| Внешняя компонента по технологии Native API | Windows, Linux, macOS, клиент и сервер | торговое оборудование, криптография, своя быстрая логика на C++ |
| Внешняя компонента COM | только Windows | старые компоненты, которые уже есть в виде COM |
COMОбъект | только Windows | управление Excel, Word и другими COM-серверами |
HTTP-сервисы, веб-сервисы, HTTPСоединение | везде | обмен с сайтами, маркетплейсами, внешними API |
| Внешнее соединение (COM-коннектор) | Windows | вызов кода 1С из другой программы или другой базы |
Подключение Native-компоненты выглядит так:
Подключено = ПодключитьВнешнююКомпоненту("ОбщийМакет.МояКомпонента", "МояКомп", ТипВнешнейКомпоненты.Native);
Если Подключено Тогда
Компонента = Новый("AddIn.МояКомп.ИмяОбъекта");
КонецЕсли;
Компоненту обычно хранят в макете конфигурации, чтобы не таскать файлы по компьютерам. На клиенте её сначала устанавливают, а затем подключают, причём в веб-клиенте и на Linux-клиенте нужна сборка компоненты именно под эту платформу.
Компонента работает внутри процесса 1С. Падение её кода роняет весь процесс: на клиенте — сеанс пользователя, на сервере — рабочий процесс со всеми его сеансами. Новую компоненту проверяйте на копии, а не на рабочем сервере в разгар дня.
COMОбъект на сервере под Linux не создать вовсе — для выгрузки в Excel на сервере используют ТабличныйДокумент.Записать() в формат xlsx, это работает без установленного Office.
Частые ошибки разработчика и как их избежать
- 🐢 Обращение к реквизитам ссылки в цикле.
Для Каждого Стр Из ТЧ Цикл Сумма = Сумма + Стр.Номенклатура.Вес— по чтению на строку. Вес лучше получить одним запросом. - 🔁 Несколько серверных вызовов в одном обработчике. Сначала проверка на сервере, потом заполнение на сервере, потом пересчёт — объедините в одну процедуру.
- 🔓 Контроль остатков без управляемой блокировки. Работает в тесте на одном пользователе, даёт минусы на складе при реальной нагрузке.
- 🧱 Пустая ветка
Исключениевнутри транзакции. Ошибка проглатывается, а транзакция уже помечена к отмене — дальше вылезает непонятная ошибка в другом месте. - 🧷 Долгие транзакции. Внешний HTTP-запрос или ожидание ответа пользователя внутри транзакции держит блокировки всё это время. Внешние вызовы делайте до или после записи.
Частые вопросы
Язык 1С компилируемый или интерпретируемый?
Почему на клиенте нельзя выполнить запрос?
Чем отличается язык запросов 1С от SQL?
Можно ли в 1С писать многопоточный код?
Где писать код, который должен выполняться при любой записи справочника?
ПередЗаписью или ПриЗаписи, либо в подписке на событие. Код в форме сработает только при записи из этой формы, но не при записи из обработки или обмена.