Почему не 1С?

v2kmbqxsspgycqhw3tjgewrtafo.png

Совсем недавно мы опубликовали статью с описанием проблем одной из самых популярных технологий, используемых в IT, и на наше удивление она вызвала достаточно живой интерес (во всяком случае для технической статьи). Поэтому мы решили на этом не останавливаться, и сегодня мы «идем в гости» к одному из самых популярных продуктов на российском рынке разработки бизнес-приложений — платформе 1С.

Так сложилось, что на хабре многие 1С не любят, но порой складывается впечатление, что немногие из этих людей хорошо понимают, за что они его не любят. Этой статьей мы восполним этот пробел и убьем сразу двух зайцев: с одной стороны, расскажем, как в 1С все устроено изнутри, а с другой стороны — почему это все работает не так как нужно / хотелось бы. Надо сказать, что 1С многими своими решениями смог реально нас удивить, впрочем, не будем забегать вперед.

Статей с критикой 1С на хабре достаточно (например, один, два, три), но, на мой взгляд, они либо слишком много внимания уделяют всяким мелочам, вроде неправильной организации меню, либо рассуждают о слишком абстрактных вещах, в которых 1С, возможно, и не виноват. В этой же статье, как и в статье про SQL, речь пойдет исключительно о фундаментальных (и вполне осязаемых) проблемах, которые касаются всех и каждого, кто разрабатывает / дорабатывает решения на 1С, и приводят либо к существенному росту порога вхождения, либо к серьезному падению производительности, либо к значительным трудозатратам со стороны разработчика.
Итак, поехали. Проблем в 1С достаточно много, поэтому, чтобы в них удобнее было ориентироваться, начнем с оглавления (со списком всех этих проблем):


Разделы я постарался выстроить в порядке от базовых понятий / проблем к более сложным, хотя по факту в большинстве своем они никак не связаны друг с другом. Поэтому, если кому-то лень читать всю статью целиком, можете просто прочитать отдельные заинтересовавшие его разделы из оглавления, сквозного сюжета там практически нет.

Объекты: Справочники, Документы и т.д.


Как обычно устроены ORM фреймворки / платформы? В языке разработки ORM фреймворка в том или ином виде поддерживаются классы объектов. Для каждого из этих классов разработчик может задать его отображение на некоторую таблицу. Как правило, один класс соответствует одной таблице, единственный ключ которой, в свою очередь, соответствует идентификатору объекта этого класса. Тут, конечно, возникает вопрос, что делать с таблицами у которых несколько ключей. Для них тоже создаются соответствующие классы, благо большинство ORM поддерживают в качестве идентификаторов несколько полей одного класса. Порой, конечно, при таком отображении получаются достаточно дырявые абстракции вроде ТоварНаСкладе, но зато работа с данными в большинстве случаев идет в одной парадигме (ООП).

В 1С решили пойти другим путем и поддержать сразу обе парадигмы, у них одновременно есть и объекты, и записи. Логику записей используют регистры и запросы (о них в следующих разделах), логика объектов напоминает обычный ORM, правда, со своими особенностями:

  • Отображением на таблицы разработчик никак не управляет, и вообще оно скрыто от него (хотя ничего особенного в нем нет)
  • Никаких one-to-many, many-to-many отображений нет, их функцию выполняют так называемые табличные части — коллекции внутренних объектов, фактически агрегированных в основной объект.


Неэффективное получение данных объектов


Так как несвоевременное или избыточное чтение данных с сервера БД и передача их на сервер приложений может приводить к существенному падению производительности, обычно ORM-фреймворки предоставляют разработчику целый набор инструментов по управлению получаемыми ими данными. Но не 1С. В 1С объект читается всегда целиком, в том числе с табличными частями, но не более того (без каких либо связанных данных). Как следствие, данных читается:

  • либо слишком много — если надо получить только одно поле (реквизит)
  • либо слишком мало — если в цикле надо обращаться к другим объектам по ссылке, мы получаем классическую проблему N+1 (один запрос для получения N объектов и по одному запросу для каждой ссылки).


Такая убогость механизма 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С наследуют (а иногда даже и усугубляют) все проблемы представлений в 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С все по человечески, весь приведенный код можно было бы заменить на ровно одну строку.

В параметрах виртуальных таблиц можно использовать только константы


Также как и в параметризованных представлениях в SQL в параметрах виртуальных таблиц можно использовать только константы. То есть, если вы попробуете выполнить запрос:

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
		|	Товар.МояДата КАК МояДата,
		|	Товар.Цена КАК Цена,
		|	ДвиженияОстатков.ЧислоОстаток КАК Число
		|ИЗ
		|	Справочник.Товар КАК Тов
		|		ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияОстатков.Остатки(Товар.МояДата, Товар = Тов.Ссылка) КАК ДвиженияОстатков
		|		ПО ДвиженияОстатков.Товар = Тов.Ссылка
		|		ГДЕ Товар.Наименование = &Имя "
		;
    РезультатЗапроса = Запрос.Выполнить();


То получите сразу 2 ошибки: «Неверные параметры» и «Таблица Тов не найдена». И если со второй ошибкой еще как-то можно справиться при помощи конструкции IN (В) и подзапроса (это тоже одна из проблем 1С, но о ней позже), то что делать, когда дата не является константой, а лежит, например, в поле другой таблицы, — непонятно. В SQL для хотя бы частичного решения этой проблемы есть специальный вид JOIN — LATERAL JOIN или APPLY, в 1С же даже этого нет. И, соответственно, нужно самому находить все возможные разновидности дат, хранящихся в заданном поле, после чего для каждой даты выполнять отдельный запрос. Ну или находить минимум / максимум дат и высчитывать остатки и обороты. В любом случае оба этих способа как неудобны, так и не производительны одновременно.

Справедливости ради, ограничение, что параметром момента времени должна быть константа, 1С умеет достаточно эффективно использовать, если в регистре материализованы итоги для промежуточных дат. В этом случае 1С в подзапросе виртуальной таблицы может бежать по записям регистра, начиная не от текущей даты, а от ближайшей даты, для которой материализованы итоги. С другой стороны, учитывая что в оперативной базе (OLTP) древность данных обычно обратно пропорциональна вероятности их использования, а на поддержание промежуточных итогов нужны дополнительные ресурсы (в частности место на диске), практическая польза от такой оптимизации весьма сомнительна (если, конечно, оперативную базу одновременно активно не используют как аналитическую, но об этом позже).

Запросы


Запросы в 1С пишутся на своем внутреннем диалекте SQL, который по большому счету мало чем отличается от самого SQL. Правда, в отличие от последнего и других платформ, использующих SQL, работа с запросами в 1С осталась на очень примитивном уровне, и соответственно имеет ряд недостатков.

Запросы в строках


Запросы по старинке пишутся в строках (смотри примеры выше). Это создает как минимум две проблемы:

  • Со стороны IDE непонятно, как поддерживать автоподстановку, подсветку ошибок, синтаксиса, поиск использований и т.п.
  • Ошибки в запросах обнаруживаются только при выполнении, а не сохранении, запуске или компиляции.


Впрочем, стоит отметить, в 1С есть возможность конструирования запросов программно (CхемаЗапроса, СКД), но с верхними двумя проблемами это не сильно поможет (в упомянутых механизмах все равно в конечном итоге используется очень много строк). Единственное с чем это теоретически может помочь, так это с отсутствием в 1С механизма представлений. Но из-за громоздкости этих механизмов, а также невозможности использования визуального программирования, для декомпозиции запросов на практике схемы запросов и СКД используются очень редко.

Отсутствие оптимизатора запросов


Существующий механизм запросов в 1С — это не более чем транслятор SQL-диалекта 1С в синтаксис РСУБД, используемой системой в качестве хранилища данных. И вот тут есть тонкий момент. Дело в том, что кроме больших коммерческих СУБД с кучей оптимизаторов (MS SQL, Oracle), 1С также поддерживает свою файловую СУБД и PostgreSQL, которые работают по принципу «что вижу, то и выполняю». В частности, как было отмечено в предыдущей статье, PostgreSQL не поддерживает Join Predicate Push Down (возможность проталкивания условия запроса внутрь подзапроса). Соответственно у 1С разработчика при написании любого запроса возникает дилемма, можно ли полагаться на оптимизаторы СУБД или нет.

Общая рекомендация самой 1С — нет (и это понятно, так как в противном случае потребовалась бы установка дорогостоящих и громоздких СУБД). И во всяком случае во всех типовых они следуют этим рекомендациям. Более того, судя по этому разделу:

В частности, виртуальная таблица по-разному формирует запросы к базе данных в зависимости от того, какой режим работы у регистра (включено ли хранение актуальных итогов, рассчитаны ли итоги и на какой последний период). Она может как использовать временные таблицы базы данных, так и сформировать подзапрос.


1С и в самой платформе следует принципу «не полагаться на оптимизаторы СУБД», поэтому у разработчика и выбора то особо не остается. А значит ему приходится ориентироваться на «самое слабое звено» и все оптимизации запросов делать вручную. В частности проталкивать условия запроса внутрь подзапросов. И если с подзапросами это еще как-то можно сделать, то с виртуальными таблицами все не так просто. В них хоть немного сложные условия внешних запросов необходимо преобразовывать в конструкции вида В (Подзапрос). Например:

ВЫБРАТЬ
    РасходнаяНакладнаяСостав.Номенклатура,
    УчетНоменклатурыОстатки.КоличествоОстаток
ИЗ
    Документ.РасходнаяНакладная.Состав КАК РасходнаяНакладнаяСостав
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УчетНоменклатуры.Остатки(,
                             Номенклатура В (
                                   ВЫБРАТЬ Номенклатура
                                   ИЗ Документ.РасходнаяНакладная.Состав
                                   ГДЕ Ссылка = &Документ)) КАК УчетНоменклатурыОстатки
        ПО УчетНоменклатурыОстатки.Номенклатура = РасходнаяНакладнаяСостав.Номенклатура
ГДЕ
    РасходнаяНакладнаяСостав.Ссылка = &Документ И
    (УчетНоменклатурыОстатки.КоличествоОстаток < РасходнаяНакладнаяСостав.Количество ИЛИ
        УчетНоменклатурыОстатки.КоличествоОстаток ЕСТЬ NULL)


Также, судя по тому, что 1С не рекомендует создавать сложные условия в виртуальных таблицах, самым «слабым звеном» в линейке РСУБД, похоже, является не PostgreSQL, а файловая СУБД (потому как во всяком случае с оптимизацией IN / EXISTS даже PostgreSQL успешно справляется). Как следствие, для нормальной работы, в том числе с файловой СУБД, все запросы надо декомпозировать вплоть до самых примитивных (что в типовых и делают).

Отсутствие расширенных SQL возможностей


Как и с оптимизатором запросов, с расширенными SQL возможностями 1С также, похоже, попали в ловушку поддержки различных СУБД, а точнее своей файловой СУБД. Так, для того чтобы поддержать такие уже де-факто стандартные вещи, как оконные функции и рекурсивные CTE, 1С пришлось бы поддержать их и в своей файловой СУБД, что является не такой уж тривиальной задачей. Как следствие, либо по причине сложности реализации, либо все же решив, что эти механизмы слишком сложны для разработчика, в 1С решили этого не делать, и сейчас SQL-диалект 1С соответствует стандарту SQL года так 92. А значит для решения таких простых задач, как расчет нарастающего итога, придется либо серьезно попотеть (чтобы добиться нормальной производительности), либо вернуться в ORM, который, как мы выяснили выше, в 1С тоже не фонтан.

Отсутствие запросов на изменение


Активное повсеместное использование запросов позволяет решить проблемы, связанные с чтением большого количества данных. Но так как, в отличии от SQL, в запросах 1С отсутствует механизм изменения данных (так называемый DML), использование запросов никак не может помочь в вопросах записи большого количества данных. То есть, если вам понадобится создать сто тысяч дисконтных карт, вам придется вернуться назад в ORM (то есть к объектам) и генерировать эти объекты по одному с соответствующей не самой высокой производительностью.

Отказ от автоматических блокировок


Одной из главных функций SQL серверов является обеспечение целостности данных (буковки CI в ACID). Самым простым способом ее обеспечения является блокировка всех читаемых данных (с защитой их от изменения). Проблема такого подхода в том, что при его использовании очень сильно страдает масштабируемость, в частности, из-за того, что читатель блокирует писателя. То есть грубо говоря, один сложный отчет, читающий большое количество данных, может ввести в ступор всю базу.

Для борьбы с этим явлением в свое время появились так называемые версионные СУБД. Основная идея этих СУБД состоит в том, чтобы для каждой записи хранить не одну, а несколько версий. Соответственно при начале любой транзакции СУБД запоминает версию базы на момент начала этой транзакции, после чего внутри транзакции читает не текущую запись в таблице, а запись именно на эту «запомненную» версию. Реализуется поддержка различных версий записей (MVCC) в Oracle и PostgreSQL по-разному, но с точки зрения логики обеспечения целостности это не так важно, поэтому останавливаться на этом здесь подробно не имеет особого смысла.

Такой подход действительно значительно повышает масштабируемость СУБД, но на самом деле это улучшение масштабируемости является следствием частичного отказа от целостности. Так, например, если вы будете проверять, что сумма значений полей из двух разных таблиц должна быть больше некоторого значения, то при одновременном редактировании значений этих полей в обеих таблицах в блокировочнике все будет хорошо (точнее возникнет дедлок, в результате которого одна из транзакций откатится), версионник же благополучно запишет оба этих изменения в базу, нарушив тем самым ваше ограничение. Что же тогда поддерживает версионник в плане целостности? На самом деле фактически он поддерживает только целостность данных, которые физически хранятся в таблицах (для них работает механизм конфликтов записи). Соотв

© Habrahabr.ru