- PVSM.RU - https://www.pvsm.ru -

Совсем недавно мы опубликовали статью [1] с описанием проблем одной из самых популярных технологий, используемых в IT, и на наше удивление она вызвала достаточно живой интерес (во всяком случае для технической статьи). Поэтому мы решили на этом не останавливаться, и сегодня мы «идем в гости» к одному из самых популярных продуктов на российском рынке разработки бизнес-приложений — платформе 1С.
Так сложилось, что на хабре многие 1С не любят, но порой складывается впечатление, что немногие из этих людей хорошо понимают, за что они его не любят. Этой статьей мы восполним этот пробел и убьем сразу двух зайцев: с одной стороны, расскажем, как в 1С все устроено изнутри, а с другой стороны — почему это все работает не так как нужно / хотелось бы. Надо сказать, что 1С многими своими решениями смог реально нас удивить, впрочем, не будем забегать вперед.
Статей с критикой 1С на хабре достаточно (например, один [2], два [3], три [4]), но, на мой взгляд, они либо слишком много внимания уделяют всяким мелочам, вроде неправильной организации меню, либо рассуждают о слишком абстрактных вещах, в которых 1С, возможно, и не виноват. В этой же статье, как и в статье про SQL, речь пойдет исключительно о фундаментальных (и вполне осязаемых) проблемах, которые касаются всех и каждого, кто разрабатывает / дорабатывает решения на 1С, и приводят либо к существенному росту порога вхождения, либо к серьезному падению производительности, либо к значительным трудозатратам со стороны разработчика.
Итак, поехали. Проблем в 1С достаточно много, поэтому, чтобы в них удобнее было ориентироваться, начнем с оглавления (со списком всех этих проблем):
Разделы я постарался выстроить в порядке от базовых понятий / проблем к более сложным, хотя по факту в большинстве своем они никак не связаны друг с другом. Поэтому, если кому-то лень читать всю статью целиком, можете просто прочитать отдельные заинтересовавшие его разделы из оглавления, сквозного сюжета там практически нет.
Как обычно устроены ORM фреймворки / платформы? В языке разработки ORM фреймворка в том или ином виде поддерживаются классы объектов. Для каждого из этих классов разработчик может задать его отображение на некоторую таблицу. Как правило, один класс соответствует одной таблице, единственный ключ которой, в свою очередь, соответствует идентификатору объекта этого класса. Тут, конечно, возникает вопрос, что делать с таблицами у которых несколько ключей. Для них тоже создаются соответствующие классы, благо большинство ORM поддерживают в качестве идентификаторов несколько полей одного класса. Порой, конечно, при таком отображении получаются достаточно дырявые абстракции вроде ТоварНаСкладе, но зато работа с данными в большинстве случаев идет в одной парадигме (ООП).
В 1С решили пойти другим путем и поддержать сразу обе парадигмы, у них одновременно есть и объекты, и записи. Логику записей используют регистры и запросы (о них в следующих разделах), логика объектов напоминает обычный ORM, правда, со своими особенностями:
Так как несвоевременное или избыточное чтение данных с сервера БД и передача их на сервер приложений может приводить к существенному падению производительности, обычно ORM-фреймворки предоставляют разработчику целый набор инструментов по управлению получаемыми ими данными. Но не 1С. В 1С объект читается всегда целиком, в том числе с табличными частями, но не более того (без каких либо связанных данных). Как следствие, данных читается:
Такая убогость механизма ORM в 1С на самом деле обусловлена тем, что в 1С в какой-то момент попросту решили отказаться от ORM и сделать ставку на голый SQL (то есть регистры и запросы). Правда, немного забегая вперед, с учетом отсутствия расширенных возможностей SQL и DML в 1С периодически все-таки возвращаются к ORM, но это скорее вынужденная необходимость. А в целом типичный код типовых решений на 1С выглядит приблизительно так:
Процедура ИнициализироватьДанныеДокумента(ДокументСсылка, ДополнительныеСвойства, Регистры = Неопределено) Экспорт
////////////////////////////////////////////////////////////////////////////
// Создадим запрос инициализации движений
Запрос = Новый Запрос;
ЗаполнитьПараметрыИнициализации(Запрос, ДокументСсылка);
////////////////////////////////////////////////////////////////////////////
// Сформируем текст запроса
ТекстыЗапроса = Новый СписокЗначений;
ТекстЗапросаТаблицаЗаказыКлиентов(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаСвободныеОстатки(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаОбеспечениеЗаказов(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыКОтгрузке(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыНаСкладах(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаДвиженияСерийТоваров(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаПереданнаяВозвратнаяТара(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыОрганизаций(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыПереданныеНаКомиссию(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаДатыПередачиТоваровНаКомиссию(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыОрганизацийКПередаче(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаТоварыКОформлениюОтчетовКомитенту(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаСебестоимостьТоваров(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаВыручкаИСебестоимостьПродаж(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаРасчетыСКлиентами(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаСуммыДокументовВВалютеРегл(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаНДССостояниеРеализации0(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаНДСЗаписиКнигиПродаж(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаМатериалыИРаботыВПроизводстве(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаДвиженияНоменклатураНоменклатура(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаУслугиКОформлениюОтчетовПринципалу(Запрос, ТекстыЗапроса, Регистры);
ТекстЗапросаТаблицаРеестрДокументов(Запрос, ТекстыЗапроса, Регистры);
ПроведениеСерверУТ.ИницализироватьТаблицыДляДвижений(Запрос, ТекстыЗапроса, ДополнительныеСвойства.ТаблицыДляДвижений, Истина);
КонецПроцедуры
Функция ТекстЗапросаТаблицаЗаказыКлиентов(Запрос, ТекстыЗапроса, Регистры)
ИмяРегистра = "ЗаказыКлиентов";
Если НЕ ПроведениеСерверУТ.ТребуетсяТаблицаДляДвижений(ИмяРегистра, Регистры) Тогда
Возврат "";
КонецЕсли;
ТекстЗапроса =
"ВЫБРАТЬ
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход) КАК ВидДвижения,
| &ДатаРаспоряжения КАК Период,
| ТаблицаТовары.ЗаказКлиента КАК ЗаказКлиента,
| ТаблицаТовары.Номенклатура КАК Номенклатура,
| ТаблицаТовары.Характеристика КАК Характеристика,
| ВЫБОР
| КОГДА ТаблицаТовары.СтатусУказанияСерий В (10, 14)
| ТОГДА ТаблицаТовары.Серия
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| КОНЕЦ КАК Серия,
| ТаблицаТовары.КодСтроки КАК КодСтроки,
| ВЫБОР
| КОГДА ТаблицаТовары.Номенклатура.ТипНоменклатуры В (ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар), ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.МногооборотнаяТара))
| ТОГДА ТаблицаТовары.Склад
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ КАК Склад,
| 0 КАК Заказано,
| ТаблицаТовары.Количество КАК КОформлению,
| ТаблицаТовары.СуммаВзаиморасчетов КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК ТаблицаТовары
|ГДЕ
| ТаблицаТовары.Ссылка = &Ссылка
| И ТаблицаТовары.КодСтроки <> 0
| И &Статус <> ЗНАЧЕНИЕ(Перечисление.СтатусыРеализацийТоваровУслуг.Отгружено)
| И &РеализацияПоЗаказу
| И &ИспользоватьРасширенныеВозможностиЗаказаКлиента
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход),
| &ДатаРаспоряжения,
| ТаблицаТовары.ЗаказКлиента,
| ТаблицаТовары.Номенклатура,
| ТаблицаТовары.Характеристика,
| ВЫБОР
| КОГДА ТаблицаТовары.СтатусУказанияСерий В (10, 14)
| ТОГДА ТаблицаТовары.Серия
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| КОНЕЦ,
| ТаблицаТовары.КодСтроки,
| ВЫБОР
| КОГДА ТаблицаТовары.Номенклатура.ТипНоменклатуры В (ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар), ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.МногооборотнаяТара))
| ТОГДА ТаблицаТовары.Склад
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ,
| ТаблицаТовары.Количество,
| ТаблицаТовары.Количество,
| ТаблицаТовары.СуммаВзаиморасчетов
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК ТаблицаТовары
|ГДЕ
| ТаблицаТовары.Ссылка = &Ссылка
| И ТаблицаТовары.КодСтроки <> 0
| И &Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыРеализацийТоваровУслуг.Отгружено)
| И &РеализацияПоЗаказу
| И &ИспользоватьРасширенныеВозможностиЗаказаКлиента
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход),
| &ДатаРаспоряжения,
| ТаблицаТовары.ЗаказКлиента,
| ТаблицаТовары.Номенклатура,
| ТаблицаТовары.Характеристика,
| ВЫБОР
| КОГДА ТаблицаТовары.СтатусУказанияСерий В (10, 14)
| ТОГДА ТаблицаТовары.Серия
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| КОНЕЦ,
| ТаблицаТовары.КодСтроки,
| ВЫБОР
| КОГДА ТаблицаТовары.Номенклатура.ТипНоменклатуры В (ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар), ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.МногооборотнаяТара))
| ТОГДА ТаблицаТовары.Склад
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ,
| 0,
| ТаблицаТовары.Количество,
| ТаблицаТовары.СуммаВзаиморасчетов
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК ТаблицаТовары
|ГДЕ
| ТаблицаТовары.Ссылка = &Ссылка
| И ТаблицаТовары.КодСтроки = 0
| И &РеализацияПоЗаказу
| И &ИспользоватьРасширенныеВозможностиЗаказаКлиента
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход),
| &ДатаРаспоряжения,
| ТаблицаТовары.ЗаказКлиента,
| ТаблицаТовары.Номенклатура,
| ТаблицаТовары.Характеристика,
| ВЫБОР
| КОГДА ТаблицаТовары.СтатусУказанияСерий В (10, 14)
| ТОГДА ТаблицаТовары.Серия
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| КОНЕЦ,
| ТаблицаТовары.КодСтроки,
| ВЫБОР
| КОГДА ТаблицаТовары.Номенклатура.ТипНоменклатуры В (ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар), ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.МногооборотнаяТара))
| ТОГДА ТаблицаТовары.Склад
| ИНАЧЕ ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ,
| 0,
| ТаблицаТовары.Количество,
| ТаблицаТовары.СуммаВзаиморасчетов
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК ТаблицаТовары
|ГДЕ
| ТаблицаТовары.Ссылка = &Ссылка
| И ТаблицаТовары.КодСтроки = 0
| И &РеализацияПоЗаказу
| И &ИспользоватьРасширенныеВозможностиЗаказаКлиента";
ТекстыЗапроса.Добавить(ТекстЗапроса, ИмяРегистра);
Возврат ТекстЗапроса;
КонецФункции
Регистры в 1С это большой комбайн, который выполняет сразу несколько функций:
С функцией таблиц все более-менее понятно. Так таблицы регистров — это и есть обычные таблицы с ключами, а менеджеры регистров — интерфейс по замещению строк, удовлетворяющих заданному условию (отбору) другими строками (ну или просто добавлению строк)
Функционал работы со временем — это особенность ERP-платформ и 1С в частности. Так, с точки зрения SQL (да и других ЯП), например, дата — это один из примитивных типов, не сильно отличающийся от строки или числа. В 1С же работа со временем поддерживается в большом количестве различных абстракций, например, в документах, таблицах периодических регистров и регистров накоплений. Но во всех этих абстракциях поддержка времени — не более чем дополнительные поля. Существенно интереснее поддержка времени в представлениях (которые в 1С называются виртуальными таблицами).
Одновременно с созданием регистра в 1С неявно создаются различные представления в зависимости от типа регистра:
Являясь по сути не более чем представлениями в SQL, усовершенствованными для работы со временем, представления в 1С наследуют (а иногда даже и усугубляют) все проблемы представлений [31] в SQL.
Фактически, регистры в 1С поддерживают всего две операции: сумма и последнее по дате, сгруппированные по ключам таблицы. Это на порядок меньше, чем даже индексированные представления в том же MS SQL, не говоря уже про Oracle. Соответственно, шаг влево, шаг вправо — и разработчику необходимо самому создавать таблицу (непериодический регистр сведений / справочник) или поле (реквизит) и вручную поддерживать актуальность данных в них.
Если же говорить про представления вообще (а не только материализованные), то при сравнении с SQL все еще хуже: аналога этого механизма в 1С нет в принципе, соответственно, для повторного использования / декомпозиции нужно либо создавать процедуры, возвращающие / заполняющие временные таблицы, либо производить различные манипуляции с текстами запросов (вроде склейки или замены).
Но даже если для расчета некоторого показателя вам удастся обойтись существующими в 1С регистрами, у вас в дальнейшем скорее всего возникнет вопрос, как создать ограничения или события на этот рассчитанный показатель. Например, запретить ситуацию, когда количество к отгрузке становится меньше 0, или послать какое-нибудь уведомление в этом случае. И если в SQL это можно декларативно сделать хотя бы для материализованных представлений, то в 1С даже такой возможности нет. Как это обходится? Разработчики типовых, например, используют такой трюк:
В триггере ПередЗаписью (в SQL соответствует per-statement trigger on before) запоминают во временную таблицу старые записи регистра для сгенерировавшего его документа:
// Текущее состояние набора помещается во временную таблицу,
// чтобы при записи получить изменение нового набора относительно текущего.
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Регистратор", Отбор.Регистратор.Значение);
Запрос.МенеджерВременныхТаблиц = ДополнительныеСвойства.ДляПроведения.СтруктураВременныеТаблицы.МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Таблица.ВидДвижения КАК ВидДвижения,
| Таблица.ДокументОтгрузки КАК ДокументОтгрузки,
| Таблица.Номенклатура КАК Номенклатура,
| Таблица.Характеристика КАК Характеристика,
| Таблица.Назначение КАК Назначение,
| Таблица.Серия КАК Серия,
| Таблица.Склад КАК Склад,
| Таблица.Получатель КАК Получатель,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА Таблица.ВРезерве + Таблица.КОтгрузке
| ИНАЧЕ -Таблица.ВРезерве - Таблица.КОтгрузке
| КОНЕЦ КАК ВРезервеКОтгрузкеПередЗаписью,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА Таблица.КОтгрузке
| ИНАЧЕ -Таблица.КОтгрузке
| КОНЕЦ КАК КОтгрузкеПередЗаписью,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА -Таблица.Собирается
| ИНАЧЕ Таблица.Собирается
| КОНЕЦ КАК СобираетсяПередЗаписью,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА -Таблица.Собрано
| ИНАЧЕ Таблица.Собрано
| КОНЕЦ КАК СобраноПередЗаписью
|ПОМЕСТИТЬ ДвиженияТоварыКОтгрузкеПередЗаписью
|ИЗ
| РегистрНакопления.ТоварыКОтгрузке КАК Таблица
|ГДЕ
| Таблица.Регистратор = &Регистратор";
Запрос.Выполнить();
В триггере ПослеЗаписи (on after) запоминают во временную таблицу новые + старые записи:
СтруктураВременныеТаблицы = ДополнительныеСвойства.ДляПроведения.СтруктураВременныеТаблицы;
Запрос = Новый Запрос;
ОформлятьСначалаНакладные = Константы.ПорядокОформленияНакладныхРасходныхОрдеров.Получить() = Перечисления.ПорядокОформленияНакладныхРасходныхОрдеров.СначалаНакладные;
Запрос.УстановитьПараметр("ОформлятьСначалаНакладные", ОформлятьСначалаНакладные);
Запрос.УстановитьПараметр("Регистратор", Отбор.Регистратор.Значение);
Запрос.МенеджерВременныхТаблиц = СтруктураВременныеТаблицы.МенеджерВременныхТаблиц;
// Рассчитывается изменение нового набора относительно текущего с учетом накопленных изменений
// и помещается во временную таблицу.
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаИзменений.ДокументОтгрузки КАК ДокументОтгрузки,
| ТаблицаИзменений.Номенклатура КАК Номенклатура,
| ТаблицаИзменений.Характеристика КАК Характеристика,
| ТаблицаИзменений.Назначение КАК Назначение,
| ТаблицаИзменений.Серия КАК Серия,
| ТаблицаИзменений.Склад КАК Склад,
| ТаблицаИзменений.Получатель КАК Получатель,
| СУММА(ТаблицаИзменений.КОтгрузкеИзменение) КАК КОтгрузкеИзменение,
| СУММА(ТаблицаИзменений.СобираетсяИзменение) КАК СобираетсяИзменение,
| СУММА(ТаблицаИзменений.СобираетсяИзменение) КАК СобраноИзменение
|ПОМЕСТИТЬ ДвиженияТоварыКОтгрузкеИзменение
|ИЗ
| (ВЫБРАТЬ
| Таблица.ВидДвижения КАК ВидДвижения,
| Таблица.ДокументОтгрузки КАК ДокументОтгрузки,
| Таблица.Номенклатура КАК Номенклатура,
| Таблица.Характеристика КАК Характеристика,
| Таблица.Назначение КАК Назначение,
| Таблица.Серия КАК Серия,
| Таблица.Склад КАК Склад,
| Таблица.Получатель КАК Получатель,
| Таблица.КОтгрузкеПередЗаписью КАК КОтгрузкеИзменение,
| Таблица.СобираетсяПередЗаписью КАК СобираетсяИзменение,
| Таблица.СобраноПередЗаписью КАК СобраноИзменение
| ИЗ
| ДвиженияТоварыКОтгрузкеПередЗаписью КАК Таблица
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ВЫБРАТЬ
| Таблица.ВидДвижения,
| Таблица.ДокументОтгрузки,
| Таблица.Номенклатура,
| Таблица.Характеристика,
| Таблица.Назначение,
| Таблица.Серия,
| Таблица.Склад,
| Таблица.Получатель,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА -Таблица.КОтгрузке
| ИНАЧЕ Таблица.КОтгрузке
| КОНЕЦ,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА Таблица.Собирается
| ИНАЧЕ -Таблица.Собирается
| КОНЕЦ,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА Таблица.Собрано
| ИНАЧЕ -Таблица.Собрано
| КОНЕЦ
| ИЗ
| РегистрНакопления.ТоварыКОтгрузке КАК Таблица
| ГДЕ
| Таблица.Регистратор = &Регистратор) КАК ТаблицаИзменений
|
|СГРУППИРОВАТЬ ПО
| ТаблицаИзменений.ВидДвижения,
| ТаблицаИзменений.ДокументОтгрузки,
| ТаблицаИзменений.Номенклатура,
| ТаблицаИзменений.Характеристика,
| ТаблицаИзменений.Назначение,
| ТаблицаИзменений.Серия,
| ТаблицаИзменений.Склад,
| ТаблицаИзменений.Получатель
|
|ИМЕЮЩИЕ
| (СУММА(ТаблицаИзменений.КОтгрузкеИзменение) > 0
| ИЛИ СУММА(ТаблицаИзменений.СобираетсяИзменение) > 0
| ИЛИ СУММА(ТаблицаИзменений.СобраноИзменение) > 0)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Характеристика КАК Характеристика,
| Т.Назначение КАК Назначение,
| Т.Серия КАК Серия,
| Т.Склад КАК Склад,
| Т.Склад КАК Получатель,
| СУММА(Т.УвеличениеПрихода) КАК УвеличениеПрихода
|ПОМЕСТИТЬ ДвиженияТоварыКОтгрузкеИзменениеСводно
|ИЗ
| (ВЫБРАТЬ
| Таблица.ВидДвижения КАК ВидДвижения,
| Таблица.Номенклатура КАК Номенклатура,
| Таблица.Характеристика КАК Характеристика,
| Таблица.Назначение КАК Назначение,
| Таблица.Серия КАК Серия,
| Таблица.Склад КАК Склад,
| Таблица.Склад КАК Получатель,
| -Таблица.ВРезервеКОтгрузкеПередЗаписью КАК УвеличениеПрихода
| ИЗ
| ДвиженияТоварыКОтгрузкеПередЗаписью КАК Таблица
|ГДЕ
| Таблица.Серия <> ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| И Таблица.Склад.КонтролироватьОперативныеОстатки
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ВЫБРАТЬ
| Таблица.ВидДвижения,
| Таблица.Номенклатура,
| Таблица.Характеристика,
| Таблица.Назначение,
| Таблица.Серия,
| Таблица.Склад,
| Таблица.Получатель,
| ВЫБОР
| КОГДА Таблица.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
| ТОГДА Таблица.ВРезерве + Таблица.КОтгрузке
| ИНАЧЕ -Таблица.ВРезерве - Таблица.КОтгрузке
| КОНЕЦ
| ИЗ
| РегистрНакопления.ТоварыКОтгрузке КАК Таблица
| ГДЕ
| Таблица.Регистратор = &Регистратор) КАК Т
|ГДЕ
| Т.Серия <> ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
| И Т.Склад.КонтролироватьОперативныеОстатки
|
|СГРУППИРОВАТЬ ПО
| Т.ВидДвижения,
| Т.Номенклатура,
| Т.Склад,
| Т.Получатель,
| Т.Характеристика,
| Т.Назначение,
| Т.Серия
|
|ИМЕЮЩИЕ
| СУММА(Т.УвеличениеПрихода) > 0
|;
|
|////////////////////////////////////////////////////////////////////////////////
|УНИЧТОЖИТЬ ДвиженияТоварыКОтгрузкеПередЗаписью";
ЗапросПакет = Запрос.ВыполнитьПакет();
Выборка = ЗапросПакет[0].Выбрать();
Выборка.Следующий();
// Новые изменения были помещены во временную таблицу.
// Добавляется информация о ее существовании и наличии в ней записей об изменении.
СтруктураВременныеТаблицы.Вставить("ДвиженияТоварыКОтгрузкеИзменение", Выборка.Количество > 0);
Выборка = ЗапросПакет[1].Выбрать();
Выборка.Следующий();
СтруктураВременныеТаблицы.Вставить("ДвиженияТоварыКОтгрузкеИзменениеСводно", Выборка.Количество > 0);
В КонтролеПроведения (единой точке входа записи всех документов) читают текущие значения регистра для измененных ключей и проверяют на то, что они должно быть больше 0.
Если ЕстьИзмененияВТаблице(ДанныеТаблиц,"ДвиженияТоварыКОтгрузкеИзменениеСводно") Тогда
МассивКонтролей.Добавить(Врег("ДвиженияТоварыКОтгрузкеСводно"));
ТекстЗапроса = ТекстЗапроса +
"
|ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Номенклатура.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
| Остатки.Характеристика КАК Характеристика,
| Остатки.Назначение КАК Назначение,
| Остатки.Склад КАК Склад,
| Остатки.Серия КАК Серия,
| СУММА(Остатки.Количество) КАК Количество
|
|ИЗ
|(ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Характеристика КАК Характеристика,
| Т.Назначение КАК Назначение,
| Т.Склад КАК Склад,
| Т.Серия КАК Серия,
| -Т.ВРезервеОстаток - Т.КОтгрузкеОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыКОтгрузке.Остатки(
| ,
| (Номенклатура, Характеристика, Назначение, Склад, Серия) В
| (ВЫБРАТЬ
| Т.Номенклатура,
| Т.Характеристика,
| Т.Назначение,
| Т.Склад,
| Т.Серия
| ИЗ
| ДвиженияТоварыКОтгрузкеИзменениеСводно КАК Т)) КАК Т
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Характеристика КАК Характеристика,
| Т.Назначение КАК Назначение,
| Т.Склад КАК Склад,
| Т.Серия КАК Серия,
| Т.ВНаличииОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| ,
| (Номенклатура, Характеристика, Назначение, Склад, Серия) В
| (ВЫБРАТЬ
| Т.Номенклатура,
| Т.Характеристика,
| Т.Назначение,
| Т.Склад,
| Т.Серия
| ИЗ
| ДвиженияТоварыКОтгрузкеИзменениеСводно КАК Т)) КАК Т
|) КАК Остатки
|
|СГРУППИРОВАТЬ ПО
| Остатки.Номенклатура,
| Остатки.Характеристика,
| Остатки.Назначение,
| Остатки.Склад,
| Остатки.Серия
|
|ИМЕЮЩИЕ
| СУММА(Остатки.Количество) < 0
|;
|///////////////////////////////////////////////////////////////////
|";
КонецЕсли;
PS: Проверено, эти временные таблицы нигде больше не используются, только для проверки одного простого ограничения.
Описанный выше способ вряд ли можно охарактеризовать как универсальный (ну и выглядит он диковато), с другой стороны лучшего способа сделать это в 1С я не нашел (как и похоже разработчики типовых). Хотя сделай 1С все по человечески, весь приведенный код можно было бы заменить на ровно одну строку.
Также как и в параметризованных представлениях [32] в SQL в параметрах виртуальных таблиц можно использовать только константы. То есть, если вы попробуете выполнить запрос:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товар.МояДата КАК МояДата,
| Товар.Цена КАК Цена,
| ДвиженияОстатков.ЧислоОстаток КАК Число
|ИЗ
| Справочник.Товар КАК Тов
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияОстатков.Остатки(Товар.МояДата, Товар = Тов.Ссылка) КАК ДвиженияОстатков
| ПО ДвиженияОстатков.Товар = Тов.Ссылка
| ГДЕ Товар.Наименование = &Имя "
;
РезультатЗапроса = Запрос.Выполнить();
То получите сразу 2 ошибки: “Неверные параметры” и “Таблица Тов не найдена”. И если со второй ошибкой еще как-то можно справиться при помощи конструкции IN (В) и подзапроса (это тоже одна из проблем 1С, но о ней позже), то что делать, когда дата не является константой, а лежит, например, в поле другой таблицы, — непонятно. В SQL для хотя бы частичного решения этой проблемы есть специальный вид JOIN — LATERAL JOIN или APPLY, в 1С же даже этого нет. И, соответственно, нужно самому находить все возможные разновидности дат, хранящихся в заданном поле, после чего для каждой даты выполнять отдельный запрос. Ну или находить минимум / максимум дат и высчитывать остатки и обороты. В любом случае оба этих способа как неудобны, так и не производительны одновременно.
Справедливости ради, ограничение, что параметром момента времени должна быть константа, 1С умеет достаточно эффективно использовать, если в регистре материализованы итоги для промежуточных дат. В этом случае 1С в подзапросе виртуальной таблицы может бежать по записям регистра, начиная не от текущей даты, а от ближайшей даты, для которой материализованы итоги. С другой стороны, учитывая что в оперативной базе (OLTP) древность данных обычно обратно пропорциональна вероятности их использования, а на поддержание промежуточных итогов нужны дополнительные ресурсы (в частности место на диске), практическая польза от такой оптимизации весьма сомнительна (если, конечно, оперативную базу одновременно активно не используют как аналитическую, но об этом позже).
Запросы в 1С пишутся на своем внутреннем диалекте SQL, который по большому счету мало чем отличается от самого SQL. Правда, в отличие от последнего и других платформ, использующих SQL, работа с запросами в 1С осталась на очень примитивном уровне, и соответственно имеет ряд недостатков.
Запросы по старинке пишутся в строках (смотри примеры выше). Это создает как минимум две проблемы:
Впрочем, стоит отметить, в 1С есть возможность конструирования запросов программно (CхемаЗапроса, СКД), но с верхними двумя проблемами это не сильно поможет (в упомянутых механизмах все равно в конечном итоге используется очень много строк). Единственное с чем это теоретически может помочь, так это с отсутствием в 1С механизма представлений. Но из-за громоздкости этих механизмов, а также невозможности использования визуального программирования, для декомпозиции запросов на практике схемы запросов и СКД используются очень редко.
Существующий механизм запросов в 1С — это не более чем транслятор SQL-диалекта 1С в синтаксис РСУБД, используемой системой в качестве хранилища данных. И вот тут есть тонкий момент. Дело в том, что кроме больших коммерческих СУБД с кучей оптимизаторов (MS SQL, Oracle), 1С также поддерживает свою файловую СУБД и PostgreSQL, которые работают по принципу «что вижу, то и выполняю». В частности, как было отмечено [33] в предыдущей статье, PostgreSQL не поддерживает Join Predicate Push Down (возможность проталкивания условия запроса внутрь подзапроса). Соответственно у 1С разработчика при написании любого запроса возникает дилемма, можно ли полагаться на оптимизаторы СУБД или нет.
Общая рекомендация самой 1С — нет (и это понятно, так как в противном случае потребовалась бы установка дорогостоящих и громоздких СУБД). И во всяком случае во всех типовых они следуют этим рекомендациям. Более того, судя по этому разделу [34]:
В частности, виртуальная таблица по-разному формирует запросы к базе данных в зависимости от того, какой режим работы у регистра (включено ли хранение актуальных итогов, рассчитаны ли итоги и на какой последний период). Она может как использовать временные таблицы базы данных, так и сформировать подзапрос.
1С и в самой платформе следует принципу «не полагаться на оптимизаторы СУБД», поэтому у разработчика и выбора то особо не остается. А значит ему приходится ориентироваться на «самое слабое звено» и все оптимизации запросов делать вручную. В частности проталкивать условия запроса внутрь подзапросов. И если с подзапросами это еще как-то можно сделать, то с виртуальными таблицами все не так просто. В них хоть немного сложные условия внешних запросов необходимо преобразовывать в конструкции вида В (Подзапрос). Например [35]:
ВЫБРАТЬ
РасходнаяНакладнаяСостав.Номенклатура,
УчетНоменклатурыОстатки.КоличествоОстаток
ИЗ
Документ.РасходнаяНакладная.Состав КАК РасходнаяНакладнаяСостав
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УчетНоменклатуры.Остатки(,
Номенклатура В (
ВЫБРАТЬ Номенклатура
ИЗ Документ.РасходнаяНакладная.Состав
ГДЕ Ссылка = &Документ)) КАК УчетНоменклатурыОстатки
ПО УчетНоменклатурыОстатки.Номенклатура = РасходнаяНакладнаяСостав.Номенклатура
ГДЕ
РасходнаяНакладнаяСостав.Ссылка = &Документ И
(УчетНоменклатурыОстатки.КоличествоОстаток < РасходнаяНакладнаяСостав.Количество ИЛИ
УчетНоменклатурыОстатки.КоличествоОстаток ЕСТЬ NULL)
Также, судя по тому, что 1С не рекомендует [36] создавать сложные условия в виртуальных таблицах, самым «слабым звеном» в линейке РСУБД, похоже, является не PostgreSQL, а файловая СУБД (потому как во всяком случае с оптимизацией IN / EXISTS даже PostgreSQL успешно справляется). Как следствие, для нормальной работы, в том числе с файловой СУБД, все запросы надо декомпозировать вплоть до самых примитивных (что в типовых и делают).
Как и с оптимизатором запросов, с расширенными SQL возможностями 1С также, похоже, попали в ловушку поддержки различных СУБД, а точнее своей файловой СУБД. Так, для того чтобы поддержать такие уже де-факто стандартные вещи, как оконные функции и рекурсивные CTE, 1С пришлось бы поддержать их и в своей файловой СУБД, что является не такой уж тривиальной задачей. Как следствие, либо по причине сложности реализации, либо все же решив, что эти механизмы слишком сложны для разработчика, в 1С решили этого не делать, и сейчас SQL-диалект 1С соответствует стандарту SQL года так 92. А значит для решения таких простых задач, как расчет нарастающего итога, придется либо серьезно попотеть [37] (чтобы добиться нормальной производительности), либо вернуться в ORM, который, как мы выяснили выше, в 1С тоже не фонтан.
Активное повсеместное использование запросов позволяет решить проблемы, связанные с чтением большого количества данных. Но так как, в отличии от SQL, в запросах 1С отсутствует механизм изменения данных (так называемый DML), использование запросов никак не может помочь в вопросах записи большого количества данных. То есть, если вам понадобится создать сто тысяч дисконтных карт, вам придется вернуться назад в ORM (то есть к объектам) и генерировать эти объекты по одному с соответствующей не самой высокой производительностью.
Одной из главных функций SQL серверов является обеспечение целостности данных (буковки CI в ACID). Самым простым способом ее обеспечения является блокировка всех читаемых данных (с защитой их от изменения). Проблема такого подхода в том, что при его использовании очень сильно страдает масштабируемость, в частности, из-за того, что читатель блокирует писателя. То есть грубо говоря, один сложный отчет, читающий большое количество данных, может ввести в ступор всю базу.
Для борьбы с этим явлением в свое время появились так называемые версионные СУБД. Основная идея этих СУБД состоит в том, чтобы для каждой записи хранить не одну, а несколько версий. Соответственно при начале любой транзакции СУБД запоминает версию базы на момент начала этой транзакции, после чего внутри транзакции читает не текущую запись в таблице, а запись именно на эту «запомненную» версию. Реализуется поддержка различных версий записей (MVCC) в Oracle и PostgreSQL по-разному, но с точки зрения логики обеспечения целостности это не так важно, поэтому останавливаться на этом здесь подробно не имеет особого смысла.
Такой подход действительно значительно повышает масштабируемость СУБД, но на самом деле это улучшение масштабируемости является следствием частичного отказа от целостности. Так, например, если вы будете проверять, что сумма значений полей из двух разных таблиц должна быть больше некоторого значения, то при одновременном редактировании значений этих полей в обеих таблицах в блокировочнике все будет хорошо (точнее возникнет дедлок, в результате которого одна из транзакций откатится), версионник же благополучно запишет оба этих изменения в базу, нарушив тем самым ваше ограничение. Что же тогда поддерживает версионник в плане целостности? На самом деле фактически он поддерживает только целостность данных, которые физически хранятся в таблицах (для них работает механизм конфликтов записи). Соответственно, чтобы сделать «версионную» базу целостной, существует два очевидных варианта:
Тут важно понимать, что целостность, как правило, важна только для ограничений (и других декларативных правил), тогда как в остальных случаях порядок транзакций все равно не гарантирован и важно лишь то, чтобы зафиксированные транзакции считывались целиком (а это версионники как раз обеспечивают).
Но вернемся к 1С. Как же они решили бороться с проблемой масштабируемости автоматических блокировок? Барабанная дробь. Они просто решили переложить эту проблему на разработчика. Одну из самых сложных задач в программировании (организацию многопоточного доступа). Самую неочевидную задачу с точки зрения человеческого . Задачу, которую максимально тяжело тестировать и отлаживать. На разработчика, которым когда-то планировалось должен был быть бухгалтер. Решать императивно — ручной установкой и снятием блокировок.
Но самое интересное, что большинство разработчиков эту «манипуляцию» со стороны 1С особо и не заметили. Видимо, потому что описанная проблема весьма неочевидна, проявляется только при высококонкурентном доступе и в случае чего списывается на сбой оборудования / плохую погоду, после чего запускается пересчет, перепроверка, данные исправляются и все благополучно забывают про эту проблему до следующего инцидента.
Одной из особенностей ERP и других высокоуровневых платформ является то, что логика представлений у них, как правило, идет в комплекте. В частности в 1С за интерактивные представления отвечают так называемые формы. Изначально в 1С этот механизм был относительно простым и понятным и не сильно отличался от условного Access, но по мере роста требований к масштабируемости все изменилось.
Первым пал единый поток выполнения. Выполнять бизнес-логику на клиенте, как это делается в двухзвенной архитектуре, неэффективно как с точки зрения масштабируемости (обращений к СУБД по сети, невозможности перераспределения вычислительных мощностей), так и с точки зрения безопасности. Самое очевидное решение этой проблемы — перенести всю логику (как бизнес-логику, так и логику формы) на сервер, а реактивное обновление формы на клиенте и синхронизацию клиентского и серверного потоков выполнения реализовать автоматически средствами платформы. То есть весь поток выполнения хранить и выполнять на сервере (который многопоточен по своей природе), а клиенту оставить чисто техническую функцию — обновлять свое состояние по требованию сервера.
Что же сделал 1С? Как уже, наверное, многие догадались — переложили все на разработчика. Основной поток выполнения остался на клиенте, разработчику дали возможность создавать серверный поток выполнения, но при этом в клиентском потоке выполнения запретили обращаться к данным, а в серверном потоке выполнения — к пользователю, то есть открывать формы, диалоги и т.п. (реактивное обновление формы на клиенте они все же реализовали, то есть обращаться на сервере к данным формы можно). Как следствие простой случай общения с пользователем:
f() <- someData(); // читаем данные из базы необходимые для myForm |
превращается в кашу из пяти различных процедур с надуманными именами. Если при этом еще надо передать данные между myForm и otherForm, то придется использовать так называемые временные хранилища (что тоже было бы не нужно, если бы поток выполнения был на сервере) в результате чего код усложняется еще больше.
Возможно, причина того, почему 1С реализовали трехзвенную архитектуру именно таким образом, заключается в том, что для обеспечения более высокой отказоустойчивости и масштабируемости они не хотели хранить данные формы на сервере приложений. То есть чтобы при падении / очень большой загрузке сервера приложений можно было легко переключить пользователя на другой сервер без потери данных. Но у такой теории есть ряд белых пятен:
Если вы думаете, что на этом ваши проблемы с потоками выполнения закончились, вы ошибаетесь. В результате того, что 1С оставил основной поток выполнения на клиенте, появилась еще одна проблема. Для того, чтобы остановить поток выполнения на клиенте и вызвать другой поток выполнения (например, открыть новую форму), на клиенте должна в принципе поддерживаться возможность создания нескольких потоков выполнения (то есть многопоточность). И если в десктоп-клиентах многопоточность с определенными оговорками еще как-то поддерживается, то в браузерах с этим все существенно сложнее. Вся их архитектура построена на однопоточности и одном цикле обработки событий. Поэтому, чтобы, например, показать диалог, 1С приходилось создавать новое окно (вкладку) браузера, что, в свою очередь:
В мобильных же браузерах ситуация с новыми вкладками в качестве диалогов еще хуже и это, похоже, стало последней каплей, которая заставила 1С что-то делать по этому поводу.
Что же они сделали? Думаете, попытались сохранить состояние потока выполнения, передать выполнение новому потоку, а по его завершению автоматически восстановить старое состояние? Как бы не так, делать что-то автоматически это, похоже, не их стиль. В 1С опять-таки решили все переложить на разработчика. Причем с весьма издевательским комментарием [41] по этому поводу:
Следует отметить, что поначалу асинхронная модель может показаться сложнее привычной синхронной. На самом деле, понимая основные различия этих моделей, разработка асинхронных приложений становится не многим труднее.
Это подтверждается тем фактом, что все современные веб-приложения построены именно с использованием асинхронной модели и их количество постоянно растет.
Верхом садизма при этом является отсутствие в 1С замыканий и передачи функций в качестве параметров. То есть переменные контекста нужно самому сохранять, например, в какую-нибудь структуру, после чего передавать эту структуру параметром явно созданной и именованной процедуре обработке результата. В итоге получается что-то зубодробительное вроде:
&НаКлиенте
Процедура ВыполнитьОперацию(Команда)
Оп = Новый ОписаниеОповещения("ВыполнитьОперациюЗавершение", ЭтотОбъект);
ПолучитьТекстВыбора(Оп);
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьОперациюЗавершение(Результат, Параметры) Экспорт
Сообщить(Результат);
КонецПроцедуры
&НаКлиенте
Процедура ПолучитьТекстВыбора(ОбработкаОповещения)
Контекст = Новый Структура("СледующееОповещение", ОбработкаОповещения);
Оп = Новый ОписаниеОповещения("ПолучитьТекстВыбораЗавершение", ЭтотОбъект, Контекст);
ПоказатьВопрос(Оп, "Продолжить операцию?", РежимДиалогаВопрос.ДаНет);
КонецПроцедуры
&НаКлиенте
Процедура ПолучитьТекстВыбораЗавершение(Результат, Контекст) Экспорт
Стр = ?(Результат = КодВозвратаДиалога.Да,
"Продолжаем выполнять операцию...",
"Операция прервана!");
Если Контекст <> Неопределено И Контекст.СледующееОповещение <> Неопределено Тогда
ВыполнитьОбработкуОповещения(Контекст.СледующееОповещение, Стр);
КонецЕсли;
КонецПроцедуры
Следующей жертвой гонки за производительностью / масштабируемостью стал WYSIWYG. Здесь, конечно, не совсем правильно говорить о жертве, потому как в ранних версиях 1С его тоже не было, поэтому будем считать WYSIWYG жертвой по сравнению с тем, как в 1С могли бы реализовать свои интерфейсы.
В ранних версиях 1С было всего несколько вариантов работы с данными в списках. К списку можно было привязать:
Естественно, с таким набором возможностей даже для такой простой задачи, как выдать на форме список товаров с их ценами и остатками, нужно было основательно потрудиться (и это даже без требований нормальной производительности). Поэтому разработчики 1С в последние версии платформы добавили так называемые динамические списки — списки, у которых в качестве источника данных могут быть любые запросы. Вроде бы все красиво, но дьявол, как известно, кроется в деталях:
ВЫБРАТЬ
Товар.МояДата КАК МояДата,
Товар.Цена КАК Цена,
ДвиженияОстатков.ЧислоОстаток КАК Число
ИЗ
Справочник.Товар КАК Тов
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияОстатков.Остатки(&Дата, ) КАК ДвиженияОстатков
ПО ДвиженияОстатков.Товар = Тов.Ссылка"
то при каждом открытии формы с этим динамическим списком будут рассчитываться остатки на заданную дату для всех (а не только видимых) товаров.
Что сделали в 1С? Ну вы поняли. Просто запретили (а точнее, не поддержали) редактирование динамических списков. Причем, так как механизм динамических списков стал основным механизмом работы со списками, заодно запретили редактирование и списков с первичными данными (справочниками и документами, то есть то, что работало в предыдущих версиях). Ну и объяснили это в стиле «просто не держите его так». А если очень хочется, то используйте таблицы значений. Правда, чтобы с использованием таблиц значений реализовать функционал динамического списка (с чтением только видимых записей), нужно, мягко говоря, серьезно повозиться.
Как следствие, на практике в 1С есть строгое разделение интерфейса на списки, где идет чтение данных, и на списки, где идет ввод данных (такой анти-WYSIWYG). Особенно наглядно эта разница видна на примере подбора товаров в типовых конфигурациях, где пользователь в одном списке выбирает товары, а во втором вводит количество этих товаров. Впрочем, про это была отдельная статья [42], поэтому подробно на этой теме останавливаться не будем.
Эта проблема может показаться менее существенной по сравнению с описанными выше, но, на мой взгляд, ее также не стоит недооценивать. Фактически эта проблема означает, что все списки / объекты на форме в 1С сами по себе, а связывать их друг с другом разработчику необходимо самостоятельно императивным кодом в подписках на различные события.
Так, например, если мы хотим в форме документа показать список договоров поставщика этого документа, нам надо:
И это в самом простом случае. Если условие «соединения данных» на форме сделать еще более сложным, событий и кода их обработки понадобится еще больше.
Соответственно, почему возможность обращаться к другим реквизитам формы (и автоматически обновлять динамические списки при их изменении) не поддержали непосредственно на уровне платформы — еще одна из загадок 1С (хотя, возможно, я просто не нашел такую возможность).
Собственно, исходя из всего вышесказанного, неудивительно почему большинство форм в 1С достаточно примитивны и состоят в лучшем случае из одного списка, причем зачастую только с первичными данными. Конечно, в типовых решениях с их неограниченными бюджетами на разработку все немного получше, но в отраслевых и решениях, разработанных с нуля, пользователей, как правило, не сильно балуют множеством информации на форме и удобством работы с ней.
Когда я начинал изучать 1С, я все не мог избавиться от мысли, что в 1С есть отдельная система поощрения для тех, кто придумает какое-то новое понятие. Там, где вроде можно было немного отрефакторить и обойтись меньшим числом абстракций, в 1С создают все новые и новые уровни, как будто специально усложняя разработку. И когда видишь вот такую картинку:

Первая мысль: кто все эти люди? Впоследствии, конечно, когда со всеми этими понятиями познакомишься немного поближе, эта картинка уже не выглядит настолько пугающе, но все равно не покидает ощущение, что многие вещи в 1С, на мой взгляд, явно лишние.
Это разделение уже упоминалось в разделе про ORM. Причем тут вопрос не к понятию объект, как таковому (это понятие есть везде), а в том, что в 1С объекты, как и записи (регистры), также отвечают за хранение данных. Собственно, верхняя картинка и получается настолько большой и сложной по причине того, что 1С пытается усидеть сразу на двух стульях.
Очень странное разделение, которое поначалу сильно сбивает с толку. Я сначала думал, что это своего рода аналог ссылок / объектов в C#, C++. Но это не так. Фактически ключевое отличие этих двух понятий заключается в том, что объекты используются для записи и чтения, а ссылки — только для чтения. Конечно физически там больше нюансов — при обращении по ссылке объект читается неявно и сохраняется в кэше на какое-то время (разработчик этим никак не управляет), а чтением объекта (и остальным циклом его жизни) управляет разработчик. Но логически разницы никакой, и ни в какой другой технологии (что ORM, что не ORM) такого разделения нет.
Это разделение является следствием борьбы за масштабируемость (разделения логики на сервер и клиент) с одной стороны, и дублирования логики данных в объектах и записях с другой. То есть 1С нужно было поддержать что-то среднее, что решало бы проблемы сериализации данных на клиента и при этом подводило бы объекты (справочники, документы) и записи (регистры, запросы) под общий знаменатель. Соответственно для этого в 1С и отделили данные формы от просто данных, но при этом в попытках сделать это разделение менее явным создали ряд очень странных абстракций. Например, класс в скобочках. Который выглядит как класс, крякает как класс, но не класс (например методы этого класса вызывать нельзя). А чтобы сделать из него настоящий класс, нужно использовать специальную процедуру РеквизитФормыВЗначение (ну и соответственно ДанныеФормыВЗначение в обратную сторону).
И если с данными формы такое разделение было во многом вынужденным, то зачем понадобилось разделять команды формы и действия, я, если честно, не совсем понял. Может в комментариях кто-нибудь сможет это объяснить.
В любом случае с управляемыми формами разработчики 1С явно перемудрили, все можно было сделать гораздо проще. Правда, скорее всего избавившись от первоначальных проблем (то есть разделения логики на сервер и клиент, и на объекты и записи).
Обычно бизнес-приложения принято разделять на два контура: OLTP и OLAP. В первом люди работают по более-менее конкретным бизнес-процессам: получают и вводят одну и ту же информацию и нажимают на одни и те же кнопки в одних и тех же доступных им формах. OLAP — творческий контур, в нем пользователи обычно «играют» с информацией, получая эту информацию в различных заранее неизвестных срезах. В OLTP люди обычно принимают оперативные решения и отражают уже произошедшие события, OLAP же используется для принятия стратегических решений. Понятно, что в жизни есть полутона, когда пользователю и в OLTP нужна какая-то гибкость, но как правило она ограничивается простыми отборами, сортировками и выбором / изменением размеров колонок, так как бОльшая гибкость будет, во-первых, слишком сложна для пользователя (многим даже отборы с трудом даются), а во-вторых, необходимость этой гибкости говорит о несовершенстве бизнес-процессов и если один более продвинутый пользователь догадается это несовершенство побороть, то второй пользователь так и будет продолжать работать неэффективно. Плюс надо понимать, что при сильном изменении структуры информации формы часто требуется изменение физической модели, например, материализация промежуточной информации (как это обычно делается в OLAP), что с одной стороны позволяет не нагружать оперативную базу долгими запросами, а с другой стороны позволяет пользователю получать информацию за секунды.
Для OLAP есть большое количество уже готовых решений, которые умеют забирать данные из существующих SQL баз через веб-сервисы и многими другими способами, после чего агрегировать их и давать пользователю максимально быстрые и эргономичные интерфейсы по работе с этими данными. И самым логичным решением для любой платформы было бы максимально упростить интеграцию с такими инструментами.
Но в 1С решили сделать не как все. Они реализовали так называемую систему компоновки данных (СКД) с очень непонятной областью применения:
В общем получился какой-то франкенштейн, который при этом по функционалу пересекается с половиной других абстракций в 1С. В которые (например, формы) они еще и попытались интегрировать этот СКД. В результате понять что, где, когда и как нужно использовать — задача весьма неочевидная.
В этом вопросе у 1С похоже какая-то принципиальная позиция. Что им мешает дать возможность разработчику самому задавать имена таблиц и полей, я до сих пор не понимаю. Да, местами возникнут вопросы, например, с составными типами реквизитов (где они, насколько я помню, в определенных случаях создают три колонки в базе), с тем, что имена всех метаданных на русском (хотя большинство СУБД позволяют создавать таблицы и поля на русском) и так далее, но все эти проблемы решаемы.
Открытая же физическая модель дала бы огромное количество возможностей: от простой интеграции на чтение со стороны тех же OLAP систем до возможности использования встроенных средств СУБД, таких как рекомендация построения индексов, профилировщиков и так далее (без необходимости гадания, что же это за поля и таблицы — _Fld16719 и _Document5759).
Но 1С не то что решили не открывать физическую модель, а наоборот, лицензионно запретили лезть разработчикам в базу без явного на то разрешение со стороны самой 1С.
Тема, о которую сломано много копий. За это часто критикуют 1С, но тут надо понимать, что так как 1С в какой-то момент для производительности решил перейти на голый SQL, а даже в самом SQL наследование и полиморфизм не реализовали [43], требовать реализацию этих механизмов от 1С достаточно наивно.
Ну и, конечно, с аргументом «покажите, что можно реализовать с наследованием и полиморфизмом, чего нельзя реализовать с помощью if'ов» тяжело спорить. Но к чему приводит отсутствие наследования, как раз хорошо иллюстрируют типовые 1С, где для того, чтобы поддержать некую общую логику, скажем для всех строк документов:
Процедура ОбработатьСтрокуТЧ(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения) Экспорт
Если КэшированныеЗначения = Неопределено Тогда
КэшированныеЗначения = ОбработкаТабличнойЧастиКлиентСервер.ПолучитьСтруктуруКэшируемыеЗначения();
КонецЕсли;
ПроверитьАссортиментСтроки( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьНоменклатуруПоНоменклатуреПоставщикаВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьНоменклатуруПоставщикаПоНоменклатуреВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьСопоставленнуюНоменклатуруПоставщикаВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьСлужебныеРеквизитыПоНоменклатуреВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьКорректностьЗаполнитьХарактеристикиИУпаковки( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьКорректностьЗаполнитьХарактеристикиКиЗ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоНеОтгружатьСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьGTINВСтроке( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокПоФактуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокСуффиксИзОтклоненияВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоЕдиницВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоЕдиницСуффиксВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокСуффиксВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоОтклонение( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокПоВесу( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьКоличествоУпаковокПоОбъему( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьВесОбъемВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуЗаУпаковкуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуЗаказаЗаУпаковкуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьВидЦеныВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьЦенуПродажиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьЦенуПродажиПоАссортиментуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьЦенуЗакупкиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьУсловияПродажВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьУсловияЗакупокВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПартиюТМЦВЭксплуатации( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСтавкуНДСВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьСтавкуНДСПоНоменклатуреВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ПересчитатьЦенуСНДС( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьСтавкуНДСВозвратнойТарыВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуРучнойСкидкиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуАвтоматическойСкидкиВСтрокеТЧ(
ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьПроцентРучнойСкидкиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуВСтрокеТЧРасхождения( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуСкидкуПоСуммеВПродажахВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуСкидкуПоСуммеВЗакупкахВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуПоСуммеВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПересчитатьСуммуПродажиПоСуммеСНДС( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьЦенуПродажиПоСуммеПродажиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуСУчетомАвтоматическойСкидкиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуСУчетомРучнойСкидкиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуСУчетомСкидкиБонуснымиБалламиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПересчитатьСуммуСУчетомПогрешностиОкругленияВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПересчитатьСуммуНДСВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуСНДСВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуБезНДСВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуРеглВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьНДСРеглВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуПродажиВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуПродажиНДСВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьРеквизитыПоНоменклатуреВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПроверитьСтатьюАналитикуРасходов( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ОчиститьСуммуВознагражденияВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.УстановитьАктивностьСтроки( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьЗаполнитьСкладВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьЗаполнитьОбеспечениеВСтрокеТЧДокументаПродажи( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьЗаполнитьОбеспечениеВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ПроверитьПолучитьВариантКомплектации( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьФлагиРасхождениеИИзлишекПорча( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ОчиститьСуммуВзаиморасчетовВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ОчиститьАвтоматическуюСкидкуВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСодержаниеУслуги( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСтатьюАналитикуРасходовПоУмолчанию( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПартнера( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПомещение( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПродавца( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьУчетноеКоличествоНоменклатуры( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПризнакНаличияНоменклатурыПродаваемойСовместно( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакБезВозвратнойТары( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакОтмененоБезВозвратнойТары( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакАктивностьБезВозвратнойТары( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьДубликатыЗависимыхРеквизитов( ТекущаяСтрока, СтруктураДействий);
ПроверитьСериюРассчитатьСтатус( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПересчитатьСуммуСверхЗаказаВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
УстановитьПризнакДляЗаполненияТекстовогоОписания( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьВариантОтраженияКорректировкиРеализации( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьВариантОбеспеченияПоДатеОтгрузки( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.НоменклатураПриИзмененииПереопределяемый( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ХарактеристикаПриИзмененииПереопределяемый( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакПринимаетсяКНУ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакАналитикаРасходовОбязательна( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакРаспределениеНДС( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакАналитикаДоходовОбязательна( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакАналитикаАктивовПассивовНеИспользуется( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьПризнакАналитикаРасходовЗаказРеализация( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьТипСтатьи( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПризнакАдресногоХранения( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПризнакИспользованияПомещений( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакиКатегорииЭксплуатации( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьРасхожденияПослеОтгрузки( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьРасхожденияПослеПриемки( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьРасхождения( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакНаличияКомментарияПриемка( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакНаличияКомментарияОтгрузка( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакНаличияКомментария( ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ЗаполнитьПризнакТоварногоМестаТребуетсяОбработка( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьПризнакДвиженияПоСкладскимРегистрам(ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПриИзмененииТипаНоменклатурыИлиВариантаОбеспечения(ТекущаяСтрока, СтруктураДействий);
ОбработкаТабличнойЧастиКлиентСервер.ПроверитьЗаполнитьНазначениеВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ОбработкаТабличнойЧастиКлиентСервер.ПриИзмененииНазначенияВСтрокеТЧ( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
ЗаполнитьНоменклатуруЕГАИС( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
АкцизныеМаркиКлиентСервер.ЗаполнитьИндексАкцизнойМаркиДляСтрокиТабличнойЧасти( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСтрануПроисхожденияДляНомераГТД( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСпособОпределенияСебестоимости( ТекущаяСтрока, СтруктураДействий);
ЗаполнитьСпособОпределенияСебестоимостиСтрокой( ТекущаяСтрока, СтруктураДействий);
ПоместитьОбработанныеСтрокиВКэшированныеЗначения( ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения);
КонецПроцедуры
Процедура ЗаполнитьПризнакНаличияКомментарияПриемка(ТекущаяСтрока, СтруктураДействий) Экспорт
Если НЕ СтруктураДействий.Свойство("ПризнакНаличиеКомментарияПриемка") Тогда
Возврат;
КонецЕсли;
ТекущаяСтрока.ЕстьКомментарийПоставщика = НЕ ПустаяСтрока(ТекущаяСтрока.КомментарийПоставщика);
ТекущаяСтрока.ЕстьКомментарийМенеджера = НЕ ПустаяСтрока(ТекущаяСтрока.КомментарийМенеджера);
КонецПроцедуры
Я участвовал в написании кода в таком стиле лет 20 назад на Foxpro (тоже на IF TYPE(«tableA.fldB») = «C») и думал, что такой подход умер еще тогда. Но нет, оказывается он даже в 2019 году живее всех живых.
P.S.: Как могли бы выглядеть наследование и полиморфизм в 1С разбиралось в недавней статье [44], поэтому подробно на этой теме здесь также останавливаться не будем.
С явной типизацией в 1С все не так однозначно. Частично она есть — при создании любого реквизита можно и нужно указать его тип (класс). Конечно, тут возникает вопрос, что делать если значение реквизита может быть не одного типа (а наследование, как мы помним, в 1С отсутствует). Для таких случаев в 1С поддерживаются так называемые составные типы, когда в качестве типа можно указать не один, а сразу несколько типов. Эффективность реализации этих составных типов, конечно, оставляет желать лучшего [45]:
ВЫБРАТЬ ПриходнаяНакладная.Контракт
ИЗ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная
ПО ПриходнаяНакладная.Контракт = РасходнаяНакладная.Контракт
Другими словами, не рекомендуется использовать никогда (ключевой момент тут на самом деле в неэффективном сравнении полей с разным количеством типов). Но как говорится: «На безрыбье и рак — рыба».
В любом случае это все касается только реквизитов (а также ресурсов, измерений и т.п.). С кодом же ситуация куда печальнее. Типов переменных нет, и какие у данной переменной есть поля и методы, можно только гадать. И если внутри одной процедуры IDE (Конфигуратор) еще как-то может вывести тип переменной, то как только эта переменная передается в другую процедуру, вся информация о ней теряется, а с ней теряется и подсветка ошибок, автоподстановка и прочие радости жизни разработчика.
Конечно, существует значительное количество языков, которые также как и 1С обходятся без явной типизации. Но на больших проектах со сложной логикой, которыми являются современные бизнес-приложения, разобраться, для чего нужен данный кусок кода и что именно в нем происходит, когда у каждой переменной не известен ее класс, на порядок сложнее.
Как по идее должно выглядеть идеальное бизнес-решение в идеальном мире. Есть большой набор (а точнее граф) готовых модулей с различным функционалом, из которых заказчик может, как из кубиков, собрать решение, которое надо именно ему (с учетом специфики его бизнеса), а если каких-то кубиков не хватает, разработать их уже лично для себя. Возможность такой декомпозиции информационной системы обычно принято называть модульностью.
Частично решить проблему модульности в 1С попытались при помощи так называемого механизма расширений. Но, во-первых, сам этот механизм, больше адаптирован под кастомизацию, а не под модульную разработку. Так, если модулей будет достаточно много (несколько сотен), работать с ними в текущих интерфейсах 1С будет крайне неудобно. Собственно, в самом 1С это понимают [46]:
Есть соблазн использовать расширения для создания тиражных прикладных решений, однако делать этого не стоит. Во-первых, потому, что расширения не проектировались под такие задачи. А во-вторых, потому, что другие механизмы платформы, например механизмы поставки и поддержки, ничего не знают о расширениях.
…
Мы анализировали все эти задачи и пришли к выводу, что наиболее приоритетной на данный момент является адаптация конфигураций к пожеланиям пользователей во время внедрений.
Во-вторых, одних только расширений для обеспечения модульности недостаточно — нужны еще:
Но в целом инициатива с расширениями, конечно, правильная, хотя на текущем этапе до реальной модульности 1С еще ой как далеко. И последние версии типовых решений — это по прежнему один большой монолит. Причем по мере роста сложности этих решений возникает следующая парадоксальная ситуация:
Как следствие, в последних версиях 1С постепенно приобретает «болезнь SAP» — на любое пожелание заказчика, ему говорится: «вы просто неправильно работаете, есть best practice, вы тоже должны работать именно так». И не важно, что бизнес по торговле автомобильными запчастями отличается от бизнеса по торговле нефтью чуть менее, чем полностью (если конечно речь не идет об автоматизации уровня «пришло-ушло-осталось»). А именно максимальная гибкость и свои ноу-хау позволяет малому и среднему бизнесу противостоять крупным корпорациям в борьбе за рынок. Нет — все должны работать одинаково. Да, конечно, в монолите часто делают миллионы настроек, но:
Тема с визуальным программированием, а точнее low-code / no-code платформами то и дело всплывает то тут, то там с завидной регулярностью. Позиционируют себя эти платформы, как средства ускоряющие / упрощающие разработку. При этом почему программирование мышкой по их мнению должно быть априори эффективнее программирования клавиатурой — непонятно. Разработку действительно можно ускорить / упростить повышением уровня абстракций, но никак не способом ввода этих абстракций в систему. Одна строка кода в этом смысле лучше, чем пять кликов / перетаскиваний мышкой, не говоря уже о том, что скорость работы с клавиатурой у опытного разработчика как правило выше, чем скорость работы с мышкой. Поэтому утверждения «ну раз код писать не надо, то и разработчик не нужен» не более, чем маркетинговая уловка.
Но попробуем взвесить все за и против обоих подходов (Все-в-виде-кода vs Визуальное программирование). Итак, преимущества всего в виде кода:
Преимущества визуального программирования:
Конечно противопоставлять эти два подхода тоже не всегда правильно. Так визуальное программирование можно использовать для генерации кода, то есть в режиме конструктора. Но в таком режиме предполагается, что код первичен и разработчик должен уметь одинаково хорошо пользоваться как «текстовым», так и «графическим» интерфейсом разработки. А в таких случаях, как показывает мировой опыт, он просто отказывается от «графического» интерфейса, так как «текстовым» интерфейсом разработчику все равно придется пользоваться гораздо чаще, а зачем ему два механизма? Поэтому в современных технологиях использование конструкторов, как правило, сведено к минимуму.
Но вернемся к 1С. Судя по тому, что основную ставку 1С решили сделать на EDT, там все вышесказанное тоже отлично понимают и поэтому старательно пытаются исправить «ошибки прошлого». Но так как платформа у них принципиально не language-based, в 1С решили сделать это, преобразовав все свои прикладные абстракции не в свой язык, а в XML. Выглядит это конечно на любителя [48]. Мало того что язык XML сам по себе достаточно громоздкий, так они еще:
Соответственно просматривать / редактировать такой код руками — так себе удовольствие. Поэтому ни о каких преимуществах подхода Все-в-виде-кода, в том виде в котором он реализован в 1С, к сожалению говорить не приходится. То есть, как говорится, хотели как лучше, а получилось как всегда.
Шутка про фатальный недостаток, пришедшая из Microsoft в 1С, заиграла новыми красками. Попробуем пересчитать все то, что 1С написали сами, и аналоги чего уже существуют и используются во всем мире:
Ну и конечно своя сборка PostgreSQL, когда можно было обойтись правильным его использованием (судя по списку изменений, которые 1С там сделал), это конечно за гранью. Очень напоминает старый анекдот [49].
Стоит отметить, что тупиковость первых двух веток в самом 1С также отлично понимают, и поэтому уже лет пять пишут свой плагин под Eclipse. Правда, судя по отзывам, делают это они с переменным успехом, потому как обеспечить нормальную производительность на кодовой базе даже УТ (не говоря уже про ERP) не так то просто. Особенно с учетом того, что не факт, что в том же Eclipse есть инфраструктура stub индексов, ленивого парсинга (chameleon токенов), language injection и т.п., как в той же IDEA (если же в Eclipse это все есть, то непонятно, что можно разрабатывать такое количество времени и не сделать EDT основным инструментом разработки).
В любом случае, с EDT 1С кроме всего вышеперечисленного нужно поддерживать и разрабатывать еще одну IDE. Поэтому неудивительно, что в последнее время они начали действовать по принципу «нужно больше золота» [50]. Страшно представить, что будет, если Eclipse совсем умрет под натиском IDEA, и в 1С решат написать еще одну IDE.
У 1С конечно очень своеобразная политика лицензирования [51]. Например: 1С: Предприятие 8. Управление торговлей стоит 22 600 рублей. На неограниченное число пользователей (!). В чем спросите вы подвох? А он в том, что основная стоимость в платформе: 1С: Предприятие 8 ПРОФ. Клиентская лицензия на 100 рабочих мест — 360 000. 1С: Предприятие 8.3 ПРОФ. Лицензия на сервер — 86 400. И тут важны не конкретные цифры, а то, что решение стоит на порядок (!) дешевле платформы. Платформы, в которой, как мы выяснили, разработчику приходится делать практически все самому вручную. И если я вдруг решу написать свое решение и продавать его серийно, мне придется конкурировать с решением за 22 600 рублей. А это не так просто с учетом следующей проблемы.
1С не только лицензирует, но и брендирует все свои решения, как будто самое главное — это 1С как платформа, а решения (которые они брезгливо называют конфигурациями) это так — люди мышкой понатыкали. Поэтому люди, не разбирающиеся в IT, часто говорят: «мы смотрим / купили 1С». Ты у них спрашиваешь, какое именно решение, а они смотрят на тебя как на идиота с немым вопросом в глазах: что мне непонятно, я же сказал — 1С. И такой подход в брендировании — это палка о двух концах. Да, с одной стороны разработчикам так действительно проще продавать свое решение, так как уже есть готовый поток лидов, синергия с продажей других продуктов (например бухгалтерии) и так далее. Но с другой стороны:
Понятно, что уровень IT-грамотности в бизнесе постепенно растет. Люди учатся отличать платформы от решений и уделять больше внимания компаниям, у которых они покупают решение, а не самим решениям. Но 1С своей политикой сам очень сильно препятствует этому процессу, и разработчикам на 1С это должно быть как минимум обидно. Хотя, возможно, у людей, привыкших к платной документации, пониженный болевой порог.
Итак, подводя итог, посчитаем, от чего 1С отказались:
Полагаю, такими темпами они в следующих версиях откажутся от условных переходов и перейдут на goto. И назовут их управляемыми переходами. Кстати, с этой «управляемостью» у них весьма оригинальная манипуляция получилась. По хорошему управляемые приложение, формы и блокировки должны были бы называться ручными, но тогда это звучало бы как недостаток. А так они назвали их управляемыми и вуаля — теперь это возможность. Почему, например, производители машин с ручной коробкой передач до такого трюка до сих пор не додумались — непонятно.
Конечно, некоторые скажут, что отказ от всех вышеперечисленных вещей — это неизбежное зло, необходимое для обеспечения масштабируемости. И если бы я не знал, что это неправда, то не писал бы эту статью. Но, например, в том же lsFusion [52] всех описанных разделений, ограничений и других проблем нет, а при этом он спокойно работает с терабайтными базами, тысячами одновременных пользователей и очень непростым [53] функционалом. Плюс все это дело крутится на весьма бюджетных серверах и с использованием бесплатных СУБД и ОС.
Но ладно, что разработчики 1С отказываются от возможностей, свойственных высокоуровневым платформам, так они при этом и возможности языков общего назначения (такие, как явная типизация, наследование и лямбды) не добавляют. То есть берут самое худшее от обоих подходов, оставляя за бортом самое лучшее. Гениально, я считаю. И как со всем этим они собираются выходить на англоязычный рынок [54], где у них не будет их двух главных преимуществ: армии разработчиков и поддержки местного законодательства в типовых решениях, для меня загадка. Благо на рынке IT деньги решают далеко не все, иначе в мире (в том числе и в России) ничего кроме Oracle, SAP и Microsoft не существовало бы.
Понятно, что самому 1С на качество платформы в общем-то наплевать. У них такие бюджеты на разработку типовых решений, что они могут их хоть на ассемблере писать. Конечно, дорабатывать такие решения — занятие не самое простое и приятное, но, как говорится, «проблемы индейцев шерифа не волнуют». А вот зачем 1С в качестве платформы разработки выбирают разработчики отраслевых решений, а также ИТ-директора для создания решений внутренней автоматизации (так называемые «самописки») — это, конечно, загадка. Черт с ним с lsFusion, он только пару месяцев назад на рынок вышел, но любая из связок .Net+MSSQL или Python+PostgreSQL будет уж точно не хуже. При этом не надо будет мучаться с ключами защиты, и платить лицензии за платформу, которая местами не то что не помогает, а наоборот мешает. Все ради одной реально полезной абстракции — регистры (которая легко реализуется как средствами СУБД, так и средствами того же .Net) и сомнительной пользы визуального программирования? Причем с конечными клиентами все более-менее понятно: на постсоветском пространстве для многих из них покупка программы — это приблизительно тоже самое, что и покупка мебели в офис. Но решение о создании отраслевых решений и самописок принимают люди с большим опытом в IT. Они-то чем думают? Впрочем, как говорил Эйнштейн, есть две бесконечные вещи, и ими, видимо, все и объясняется.
В конце статьи хотелось бы обратиться к разработчикам Axapta и SAP. Дело в том, что мы сейчас готовим материалы для выхода на англоязычный рынок (где про 1С никто и не знает), поэтому нам желательно знать, как со всеми вышеперечисленными проблемами дела обстоят у местных, не скажу что лидеров, но, скажем так, самых известных продуктов на рынке. Соответственно, если кому не лень, просьба поделиться этой информацией в комментариях. Мы, конечно, изучим все эти продукты, насколько это возможно, самостоятельно, но хотелось бы узнать необходимую нам информацию, так сказать, из первых рук (а найти специалистов по Axapta и SAP на постсоветском пространстве гораздо тяжелее, чем по 1С).
Автор: Veidt
Источник [55]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/interfejsy/331662
Ссылки в тексте:
[1] статью: https://habr.com/ru/company/lsfusion/blog/463095/
[2] один: https://habr.com/ru/post/344616/
[3] два: https://habr.com/ru/company/trinion/blog/244727/
[4] три: https://habr.com/ru/post/346324/
[5] Объекты: Справочники, Документы и т.д.: #objects
[6] Неэффективное получение данных объектов: #fetching
[7] Таблицы / Представления: Регистры: #tables
[8] Регистры поддерживаются в очень частных случаях: #mat
[9] Отсутствие ограничений и событий для значений регистров: #events
[10] В параметрах виртуальных таблиц можно использовать только константы: #const
[11] Запросы: #query
[12] Запросы в строках: #string
[13] Отсутствие оптимизатора запросов: #opt
[14] Отсутствие расширенных SQL возможностей: #ext
[15] Отсутствие запросов на изменение: #modify
[16] Отказ от автоматических блокировок: #block
[17] Формы: #forms
[18] Отказ от единого потока выполнения: разделение логики на сервер и клиент: #controlflow
[19] Отказ от синхронности: #sync
[20] Отказ от WYSIWYG: разделение интерфейса на запись и чтение: #wysiwyg
[21] Невозможность обращаться в списках к реквизитам форм / текущим значениям других списков: #join
[22] Избыточные уровни абстракций: #excess
[23] Закрытая физическая модель: #phys
[24] Отсутствие наследования и полиморфизма: #poly
[25] Отсутствие явной типизации в коде: #typing
[26] Отсутствие модульности: #module
[27] Ставка на визуальное программирование: #visual
[28] Фатальный недостаток: #phatal
[29] Неуважительные по отношению к разработчикам лицензирование и брендирование: #license
[30] много: https://its.1c.ru/db/metod8dev#content:2754:hdoc
[31] проблемы представлений: https://habr.com/ru/company/lsfusion/blog/463095/#view
[32] параметризованных представлениях: https://habr.com/ru/company/lsfusion/blog/463095/#paramview
[33] отмечено: https://habr.com/ru/company/lsfusion/blog/463095/#commjppd
[34] разделу: https://its.1c.eu/db/metod8dev#content:5457:hdoc
[35] Например: https://its.1c.ru/db/metod8dev#content:2657:hdoc
[36] не рекомендует: https://its.1c.ru/db/metod8dev#content:5457:hdoc
[37] попотеть: http://catalog.mista.ru/public/201526/
[38] прозрачной материализации: https://habr.com/ru/company/lsfusion/blog/459066/
[39] материализация представлений: https://habr.com/ru/company/lsfusion/blog/463095/#matview
[40] мозга: http://www.braintools.ru
[41] комментарием: https://its.1c.ru/db/metod8dev#content:5272:hdoc
[42] статья: https://habr.com/ru/company/lsfusion/blog/464487/
[43] наследование и полиморфизм не реализовали: https://habr.com/ru/company/lsfusion/blog/463095/#inherit
[44] статье: https://habr.com/ru/company/lsfusion/blog/465221/
[45] оставляет желать лучшего: https://its.1c.ru/db/metod8dev#content:1828:hdoc
[46] понимают: http://v8.1c.ru/o7/201410ext/
[47] наследования недостаточно: https://habr.com/ru/company/lsfusion/blog/458376/#aggr
[48] на любителя: https://github.com/BlizD/Tasks/blob/develope/src/cf/Documents/%D0%A2%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9%D0%97%D0%B2%D0%BE%D0%BD%D0%BE%D0%BA.xml
[49] старый анекдот: https://www.anekdot.ru/id/449766/
[50] «нужно больше золота»: https://infostart.ru/journal/news/mir-1s/opros-perekhod-na-prof-i-korp-litsenzii-10-09-2019_1120902/
[51] политика лицензирования: http://v8.1c.ru/price/
[52] lsFusion: https://lsfusion.org
[53] очень непростым: https://luxsoft.by/produkty/lsfusion-erp/
[54] выходить на англоязычный рынок: https://habr.com/ru/news/t/457708/
[55] Источник: https://habr.com/ru/post/468415/?utm_source=habrahabr&utm_medium=rss&utm_campaign=468415
Нажмите здесь для печати.