Что делать, если строк в документе больше 99'999?

Программирование - Практика программирования

9
Решение не претендует на уникальность и, вероятно, имеет определенные изъяны. Готов обсудить, чтобы найти более элегантный вариант.

Преамбула

  1. Программа для обработки сметной документации, ведения сметного документооборота и исполнительной документации;
  2. Имеем более 7 тысяч смет на одном объекте (а объектов не 1). В каждой из которых в среднем более 100 строк. В каждой строке при этом есть определенные ресурсы (материалы, трудозатраты, механизмы);
  3. Необходимо выполнить сводный расчет материалов по N количеству смет (в среднем более 100).

Сам расчет – это, свернутая таблица по материалам. И понятно, что в итоге мы имеем ну 1000-2000 разных материалов для выполнения работ. Но, эти пару тысяч материалов собираются путем агрегирования данных из нескольких сотен (и более) смет, т.е. нескольких тысяч сметных позиций, а также вложенных в них ресурсов.

Расчет выполняется в документе, поскольку результат должен быть сохранен, для дальнейшей отправки в обработку, тендер и т.д.

Для понимания пользователя, что расчет выполнен верно, требуется реализовать ряд возможностей:

  • из формы документа, при нажатии на свернутой позиции материалов, вывести данные: Смета - Позиция / Ресурс – Количество;
  • печать утвержденной формы расшифровки к данной материальной ведомости;
  • при объединении, перемещении и других операциях со строками все расшифровки должны переходить к новым строкам владельцам и т.д.

Табличная часть и 99'999 строк

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

В результате был придуман обходной путь:

  • регистр сведений, не подчиненный регистратору (сбор выполняется редко, при каждой перезаписи документа нет нужды перезаписывать регистр) с ведущим измерением ДокументСсылка.МатериальнаяВедомость (имя документа не важно);
  • на форме при создании на сервере записываем пустое значение во временное хранилище с указанием уникального идентификатора формы:
    • таким образом, получили постоянный адрес для хранения данных;
    • гарантировали жизнь данного адреса, до момента закрытия формы.
  • реализовали фоновое чтение данных из регистра по частям, с указанием условия какие данные запросил пользователь. При повторном чтении, обращения к БД не происходит, программа хранит информацию что данные по указанной строке прочитаны;
    • Маленькая деталь: чтение данных не обязательно делать при открытии, она может быть реализована порциями, согласно отбору, полученному на основании запроса пользователя.
  • перед записью на сервере, передаем адрес хранилища в модуль объекта документа, с целью создания записей в регистре сведений. Естественно, не производим запись, если в этом нет необходимости (например: повторный сбор данных сводной ведомости не производился).

Таким образом, кроме того, чтобы мы обеспечили хранение и чтение данных, мы к тому же, не помещаем их в реквизит формы, в конечном итоге облегчая "вес пакета данных формы" при обмене между клиентом и сервером. Хотя, мы нагружаем сервер, но кто его жалеет, да? Принимая во внимание, что с данным документом работу выполняет крайне ограниченный круг лиц, а для остальных есть отчеты и поэтому посчитали, что сервер нагрузку переживет.

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

Реализация в коде

Модуль формы документа

&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    // АдресХранилища - реквизит формы, тип Строка, длина - 0
    ЭтотОбъект.АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, ЭтотОбъект.УникальныйИдентификатор);
КонецПроцедуры
 
&НаКлиенте
Процедура ПриОткрытии(Отказ)
    // запуск фоновой процедуры считывания данных во временное хранилище
    // может быть выполнен полностью, или частями, или по запросу пользователя
КонецПроцедуры
 
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    // для записи данных из хранилища в регистр сведений
    // АдресХранилища - переменная модуля объекта (см. МодульОбъекта)
    ТекущийОбъект.АдресХранилища = ЭтотОбъект.АдресХранилища;  
КонецПроцедуры
 
&НаСервереБезКонтекста
Функция ПолучитьДанныеИзХранилищаНаСервере(знач АдресХранилища, знач НастройкаОтбора)
    // пример получения данных из хранилища
    Если НЕ ЭтоАдресВременногоХранилища(АдресХранилища) Тогда
        Возврат;
    КонецЕсли;
    
    ТаблицаДанных = ПолучитьИзВременногоХранилища(АдресХранилища);
    Если НЕ ТипЗнч(ТаблицаДанных) = Тип("ТаблицаЗначений") Тогда
        // здесь можно разместить процедуру считывания данных согласно настройкам отбора
        Возврат;
    КонецЕсли;
     
    КопияТаблицы = ТаблицаДанных.Скопировать(НастройкаОтбора);
    Возврат ОбщегоНазначения.ТаблицаЗначенийВМассив(КопияТаблицы);
КонецФункции

Модуль объекта документа

// обязательная переменная, для передачи данных между формой и модулем объекта
Перем АдресХранилища Экспорт;
 
Процедура ПриЗаписи(Отказ)
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
     
    ЗаписатьДанныеВременногоХранилища(Отказ);
КонецПроцедуры
 
Процедура ЗаписатьДанныеВременногоХранилища(Отказ)
    Если НЕ ЭтоАдресВременногоХранилища(АдресХранилища) Тогда
        Возврат;
    КонецЕсли;
     
    ТаблицаДанных = ПолучитьИзВременногоХранилища(АдресХранилища);
    Если НЕ ТипЗнч(ТаблицаДанных) = Тип("ТаблицаЗначений") Тогда
        Возврат;
    КонецЕсли;

    // дополнительно можно проверить, что таблица данных получена путем чтения
    // а не повторного заполнения пользователем
 
    Попытка
        // обратите внимание, у регистра сведений есть Измерение - ДокументСсылка
        // рекомендуется делать данное измерение ведущим, чтобы при удалении документа записи очищались
        // при этом сам регистр не подчинен регистратору, иначе при повторной записи данные будут очищаться
        БлокировкаДанных = Новый БлокировкаДанных;
        ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.ХранениеДанныхДокумента");
        ЭлементБлокировки.УстановитьЗначение("ДокументСсылка", ЭтотОбъект.Ссылка);
        БлокировкаДанных.Заблокировать();
         
        НаборЗаписей = РегистрыСведений.ХранениеДанныхДокумента.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.ДокументСсылка.Установить(ЭтотОбъект.Ссылка);
        Для Каждого СтрокаЗаписи Из ТаблицаДанных Цикл
            ЗаписьРегистра = НаборЗаписей.Добавить();
            ЗаполнитьЗначенияСвойств(ЗаписьРегистра, СтрокаЗаписи);
            ЗаписьРегистра.ДокументСсылка = ЭтотОбъект.Ссылка;
        КонецЦикла;
        НаборЗаписей.Записать(Истина);
    Исключение
        ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
        ОбщегоНазначенияКлиентСервер.СообщитьПользователю(НСтр("ru='При записи доп. сведений произошла ошибка: '") + Символы.ПС + ТекстОшибки,,,, Отказ);
    КонецПопытки;
КонецПроцедуры
 
Процедура ОчиститьРегистрСведенийХранениеДанныхДокумента()
     
    НаборЗаписей = РегистрыСведений.ХранениеДанныхДокумента.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.ДокументСсылка.Установить(ЭтотОбъект.Ссылка);
    НаборЗаписей.Прочитать();
    НаборЗаписей.Очистить();
    НаборЗаписей.Записать(Истина);
     
КонецПроцедуры

Ну вот как то так. 

Постскриптум

Мы понимаем, что могли бы использовать что-нибудь "стильное, модное, молодежное", внешнюю базу данных и может быть даже NoSQL. Но, у нас просто было мало времени на подумать и еще меньше времени на реализовать. Как всегда результат нужен был "здесь и вчера". Решение было создано очень быстро, запущено в работу и проходит анализ на предмет скрытых просчетов. Более того, не у всех заказчиков возможно использование сторонних БД, поэтому данный вопрос будет скорее всего повторно обсуждаться гораздо позже.

Повторюсь, решение возможно не уникальное и не лишенное недостатков. Поэтому прошу направить на путь истинный, если имеются дельные предложения.

9

См. также

Комментарии
Сортировка: Древо
1. Merc 14.06.18 09:58 Сейчас в теме
Так и скажи, что не любишь динамические списки.
Dmitri93; +1 Ответить
2. vandalsvq 986 14.06.18 10:05 Сейчас в теме
(1) скажи на милость, чем мне динамический список поможет? Отображать данные? Ну ок. Только в этом и все, хотя для отображения тоже не выйдет, в расшифровке выводится дерево: смета - позиция/ресурс
6. Merc 14.06.18 10:50 Сейчас в теме
(2) Динамический список умеет в дерево

99к + строк в данных формы это абсурд, как ссылка на вх или что-то ещё:

1. Отобразить их невозможно, видимая часть списка это не так много строк
2. Если есть задача отражать изменения при записи, храни изменения в данных документа
3. Изменений больше 99к? Если да - взгляни на закрытие месяца, там тоже много изменений и ни кто не складывает их в ТЧ.

ПС: Твоя проблема и её решение это одна большая ошибка.
8. vandalsvq 986 14.06.18 10:59 Сейчас в теме
(6) 1. Отображать всю таблицу никто не собирался. Отображается только часть, имеющая отношение к текущей строке материала, тем более в момент «запроса» пользователя.
2. Изменять их напрямую пользователь не будет, он управляет материалами, а программа связанными с ними строками «истории» сбора данных.
3. Вот поэтому в тч хранить и не стали. Это ни к чему.

Опять повторюсь, я кажется дал с самого начала достаточно информации для создания альтернативной структуры хранения.
12. Merc 14.06.18 11:53 Сейчас в теме
(8)

Предложение 1:

1. Пользователь, что-то делает с материалами
2. Это что-то сохраняется в документ
3. При записи/проведении рассчитываются изменения и куда-то складываются

Предложение 2:

Из регистра сведений удаляется документ как измерение и устанавливается периодичность по регистратору

1. Создается и записывается документ
2. Все действия пользователя сразу пишутся в РС
3. Пока документ не проведен, активность записей - Ложь
17. vandalsvq 986 14.06.18 12:25 Сейчас в теме
(12) вот это другое дело ;), давай обсудим.

Предложение 1 по сути есть продолжение описанного в статье. После того, как данные были собраны, они могут быть изменены, вот эти изменения будут проанализированы и в расшифровку точечно внесены изменения.

Предложение 2 в принципе предложение рабочее. Есть один нюанс, пока не нажата кнопка "Записать" обычно пользователю считают что все изменения - это "вилами на воде писано". При обрыве соединения сохранятся изменения регистра, но данные объекта документа не изменяться. Или зависнут "неактивные" позиции и их придется как то потом подчищать.
20. Merc 14.06.18 13:15 Сейчас в теме
(17)
Педложение 1 по сути есть продолжение описанного в статье. После того, как данные были собраны, они могут быть изменены, вот эти изменения будут проанализированы и в расшифровку точечно внесены изменения.


Если бы знал бизнес процесс, я бы выбрал №1, но что бы это обсуждать нужен контекст, у меня его нет, посему про предложение 2.

Почему и про нюансы:

1. Сам факт существования объекта иб при создании можно скрыть, для этого достаточно сделать отбор по какому-то его реквизиту в списке
2. При обрыве соединения пользователь не потеряет данных, можно будет дать возможность продолжить творчество с того места где он остановился.
3. Активность как свойство записи входит в индекс и по умолчанию не попадает в срезы, очистка данных может быть не оперативной.

Самое главное:
1. Мы не сталкиваемся с ограничениями при масштабировании в большую сторону.
22. vandalsvq 986 14.06.18 14:10 Сейчас в теме
(20) я возможно не во все въехал сейчас, попробую проанализировать позже. Есть хорошая идея о том что "творчество можно продолжить". Это интересно может показаться, исходя из того с каким отделом мы имеем дело сейчас.
25. Merc 14.06.18 14:39 Сейчас в теме
(22)

Вообще периодичность по регистратору и активность можно не использовать.

Документ и признак активности могут быть самостоятельными измерением, основная идея в следующем:

1. На форме пользователь видит данные полученные объектами с поддержкой чтения по курсору (РегистрСведенийСписок, ДинамическийСписок)
2. Изменения сразу фиксируются в базу данных
3. Окончательное принятие к учету происходит с использованием доп. признака активности.
3. A_Max 16 14.06.18 10:16 Сейчас в теме
Рассматривали вот такой вариант?
При создании документа генерировать новую ссылку документа и везде использовать её (при записи применять её). Соответсвенно вместо таблицы значений использовать динамический список с отбором по ссылке (реальной или сгенерированной при создании нового).

PS: Увидел, что я не первый уже после отправки комметария :о)
4. vandalsvq 986 14.06.18 10:36 Сейчас в теме
(3) тогда бы понадобилось сразу после сбора данных записывать их в базу. А этого делать не хотелось. Пользователь не факт, что документ запишет, да и время на запись тоже необходимо. А после отказа от записи документа удалять данные. В общем это вызывает доп. расходы, а не хотелось этого.
Поэтому при сборе получаем таблицу значений, закидываем во временное хранилище, работаем с ней. А если открыт существующий документ, то считываем либо все сразу, либо по частям и тоже в ТЗ и во временное хранилище.
Кстати, писал выше, поскольку отображение в виде дерева, динамический список не удастся использовать. Хотя для других случаев, берем наш регистр сведений и выводим.
13. A_Max 16 14.06.18 11:57 Сейчас в теме
(4) Именно поэтому и спросил рассматривали такой вариант или нет. Да, именно в вашем случае, когда может быть загружен большой объём и после этого велика вероятность отказа от записи, более логично времено хранить на клиенте. Но тогда мы увеличиваем требования на клиентскую часть (объём памяти и частично процессор на обработку) вместо того чтобы этим занимался сервер (вообще файл передать на сервер для загрузки). В случае с динамическим списком как минимум отпадает необходимость отработки частичного чтения. Возможность "отката" тоже реализовать на вскидку в голове пара способов крутится.

У каждого решения есть своя сфера применимости с плюсами и минусами.


ПС: При очистке регистра не нужно его читать. Можно сразу записывать набор после установки отбора.

ПС2: Не надо так резко воспринимать комметарии, этим только вызываете общий негатив.
15. vandalsvq 986 14.06.18 12:15 Сейчас в теме
(13) маленькая поправка, на клиент весь набор собранных данных (все эти много строк) не передаются, они во временном хранилище остаются. У клиента есть адрес хранилища, и при необходимости через вызов сервера, он может получить ровно ту часть строк, которые в текущий момент необходимо отобразить.
В общем получается такая схема:
- если нажата кнопка "Заполнить", то считанные данные помещаются во временное хранилище. Клиент получает ответ что все собрано и таблицу только уникальных позиций материалов, а вся расшифровка остается на сервере. На клиенте никоим образом отражения она не находит.
- после записи документа,при повторном открытии, мы не сразу заполняем все данные расшифровки. Возможно пользователю она вообще не понадобится в процессе работы. Когда пользователь запрашивает необходимость расшифровки, именно эта часть считывается из регистра и дополняется в таблицу хранилища. Таким образом, мы его наполняем постепенно.

Во всех случаях на клиенте остается только адрес хранилища и видимая часть уникального набора материалов.

Пс. да, это я уже увидел. Не знаю откуда такая привычка писать. Никогда не задумывался. Спасибо за указание ошибки.

Пс2. видимо спал вчера мало, прошу прощения у всех
5. bulpi 131 14.06.18 10:41 Сейчас в теме
Если у вас строк в ТЧ более 999999 , это значит, что вы неправильно спроектировали базу. Это аксиома.
le0nid; cegorach; Merc; Dream_kz; +4 2 Ответить
7. vandalsvq 986 14.06.18 10:51 Сейчас в теме
(5) аксиома говоришь? А теорема то кем была выдвинута? И критикуя - предлагай. Я готов обсуждать если будет что 🤗. В самом начале, постарался объяснить почему так получилось. Думаю достаточно, чтобы сделать иное предложение.
TreeDogNight; +1 Ответить
9. artgen 14.06.18 11:30 Сейчас в теме
Разбить на несколько документов.
10. acanta 44 14.06.18 11:50 Сейчас в теме
Какова зависимость времени записи документа от количества строк в 8ке?
11. VladimirL 665 14.06.18 11:52 Сейчас в теме
Идея хорошая. Возможно будут полезны замечания.

1) Обернув код записи набора записей в Попытку-Исключение в уже открытой транзакции записи документа получили возможность возникновения невосстановимой ошибки во "вложенной" транзакции. Комбинация адекватного сообщения пользователю, выдаваемого методом ОбщегоНазначенияКлиентСервер.СообщитьПользователю и пугающего диалога с "В данной транзакции уже происходили ошибки" - это не лучшее решение. Нужно выбрасывать возникшее исключение выше через ВызватьИсключение а не просто сообщать пользователю. Тогда этот метод записи данных можно использовать и в серверных алгоритмах вне интерактивного взаимодействия с пользователем.

2) Перед заполнением набора записей в методе ЗаписатьДанныеВременногоХранилища блокировка данных не нужна. Вы же не выполняете предварительного чтения чтобы новые данные писались на основе старых. И вы же не ставите такую блокировку в методе ОчиститьРегистрСведенийХранениеДанныхДокумента, где точно такой же код за исключением того, что цикл заполнения набора записей заменен на метод чтения из БД.

3) Зачем в методе ОчиститьРегистрСведенийХранениеДанныхДокумента вообще чтение из БД?

НаборЗаписей.Прочитать();
НаборЗаписей.Очистить();
НаборЗаписей.Записать(Истина);

Вроде бы как в регистре большое количество данных. Зачем их читать на сервер 1С чтобы потом сразу очистить эти данные и снова записать? Здесь ничего кроме установки неявной разделяемой управляемой блокировки не произойдет. И думаю что метод чтения из БД кучи данных вызывается вовсе не с целью установить неявную блокировку. Таким образом этот вызов является ошибкой.

4) Регистр перезаписывается целиком для документа. Если нам нужно добавить данные к уже существующим или удалить только часть данных, то алгоритм придется менять, и в методе ЗаписатьДанныеВременногоХранилища придется все-таки добавлять предварительное чтение всего объема данных по документу из регистра. Чтобы затем поменять только часть записей. Это очень не оптимально, учитывая исходные условия задачи : большой объем данных по документу.

Если обеспечить идентификацию строк в документе в свернутой таблицы материалов (например добавить автозаполняемый реквизит с типом УникальныйИдентификатор или инкрементальный числовой идентификатор), то можно было бы сделать вторым измерением регистра идентификатор строки. В этом случае получили бы больше возможностей по независимому изменению расшифровки каждой строки.
acanta; vandalsvq; A_Max; +3 Ответить
14. vandalsvq 986 14.06.18 12:05 Сейчас в теме
(11) спасибо за замечания все по делу. Действительно полезно.

1. Да, вы правы. Спасибо за подсказку, изменения будут внесены.
2. Использовать блокировку стало какой-то привычкой, причем видимо мозг уже отключается о причинах, зачем это делается. Так же согласен.
3. Чтение тоже ни к чему, согласен также.
4. Вот тут маленькая поправка. Выложил не всю информацию, сознательно ее упростив. Ваше замечание верно, и оно реализовано.
Регистр имеет структуру: Документ / Ключ строки / Смета / Ключ позиции сметы
Т.е. при необходимости можно перезаписать все данные, а можно по отдельным ключам, которые были изменены. Поскольку механизм взаимодействия пользователей сейчас в работе, процедура записи не была пока изменена. Но в будущем, когда из формы будут приходить данные об изменениях только отдельных групп строк, тогда будет реализована и запись части набора, без затрагивания остальных.
19. VladimirL 665 14.06.18 13:03 Сейчас в теме
(14)
Если реализован регистр с измерениями Документ / Ключ строки / Смета / Ключ позиции сметы и данные действительно перезаписываются не целиком по измерению Документ, а по сочетанию измерений, то при запись данных в регистр сведений в методе ЗаписатьДанныеВременногоХранилища скорее всего идет внутри цикла, перебирающего комбинации измерений.

В таком случае управляемая блокировка до начала записи действительно нужна. И тогда пункт 2 из замечаний надо вычеркнуть. Иначе параллельная транзакция читая данные в целом по документу может получить несогласованный набор данных, состоящий из части "новых" расшифровок строк и части "старых" расшифровок. Вообще тут думать надо исходя из того, возможно ли такое чтение в других сеансах.
23. vandalsvq 986 14.06.18 14:12 Сейчас в теме
(19) чтение возможно, но критичного в несогласованности ничего не будет, хотя бы потому что пока ведомость не подписана как часть договора (соглашения) она доступна к редактированию, а после подписания она недоступна и только тогда она нужна уже как источник данных.
Все таки я частенько блокировки накидываю по привычке. Долго работал над собой чтобы писать блокировки, теперь надо работать над собой чтобы не писать блокировки )))))))
16. Infector 115 14.06.18 12:17 Сейчас в теме
В общем-то пользуюсь подобным механизмом чтобы не добавлять нестандартные табличные части в стандартные объекты УПП. При создании на сервере формы существующего объекта читаем набор записей в реквизит формы, после записи формы - пишем содержимое этого реквизита в набор записей. Не забываем принудительно распихать ссылку объекта в соответствующий реквизит.
18. vandalsvq 986 14.06.18 12:26 Сейчас в теме
(16) я не хотел хранить таблицу в реквизите формы, поскольку при любом контекстном вызове сервера мы получим эти данные в пакет.
21. Infector 115 14.06.18 13:20 Сейчас в теме
(18) в общем-то на уровне интерфейса решений может быть уйма. Динамический список с отборами тоже неплохое решение. Есть еще вариант с записью регистра с подчинением документу, при этом на форму можно просто кинуть таблицу из движений. Главное тут то, что решения с регистрами сведений неплохо работают.
24. vandalsvq 986 14.06.18 14:16 Сейчас в теме
(21) мне лично показалось интересным заменить реквизит формы с большой таблицей, хранением этой самой таблице на сервере. Я еще отдельно проведу тесты на потери времени на получение и помещение, особенно когда речь пойдет о редактировании ее. Там получится ведь что сначала ты ее извлекаешь, меняешь, потом полностью обратно суешь. Не зная конвертирует ли 1С ТЗ в какой то внутренний формат или нет, сложно сказать накладные потери сходу.
А использование регистра сведений как замены таб. части, тут лежало на поверхности, да и как то раньше в памяти помню как-то на мисте даже поднимался 100 лет назад подобный вопрос.
26. mszsuz 73 17.06.18 13:38 Сейчас в теме
Ещё вариант - сделать ДВА регистра - один для хранения данных документа, второй - временный - вывести на форму для редактирования.
Заполняем временный при открытии документа. После закрытия документа, если были изменения переносим данные из временного в основной. И чистим временный.
27. A_Max 16 18.06.18 10:12 Сейчас в теме
(26) Излишняя сущность, т.к. можно использовать "Активность" записи у одного регистра или ещё какой-то статус добавить. Плюс от отдельного регистра может оказаться только в отсутсвии индексов для ускорения вставки.
Оставьте свое сообщение