При работе с табличными частями в 1С часто возникает задача по обеспечению уникальности данных в определенной колонке. Особую сложность представляют поля типа "Дата", когда требуется уникальность в пределах секунды. Рассмотрим, как эффективно найти и обработать вхождения в колонку табличной части на клиенте, чтобы исключить дубликаты, а также проанализируем различные подходы к этой задаче, включая оптимизацию нагрузки на сервер.
Наш первоначальный вопрос заключался в том, как на клиенте найти и обработать вхождение в колонку табличной части, и если значения равны, то изменить их, чтобы исключить дубли по дате в пределах секунды. Давайте рассмотрим исходный код, предложенный автором, и разберем его логику.
Автор предложил следующий вариант для обработки даты при окончании редактирования новой строки в табличной части:
&НаКлиенте
Процедура НоменклатураПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
Если НоваяСтрока Тогда
Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = ТекущаяДата;
УДата = Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки;
НельЗя = Истина;
Пока НельЗя Цикл
Нашел = Ложь;
Для Каждого Стр ИЗ Объект.Номенклатура Цикл
Если Стр.ДатаУстановки = УДата Тогда
УДата = УДата + 1;
Нашел = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если НЕ Нашел Тогда
НельЗя = Ложь;
КонецЕсли;
КонецЦикла;
Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = УДата;
КонецЕсли;
КонецПроцедуры
Разберем логику этого кода по шагам:
ДатаУстановки.УДата текущей датой, которая будет изменяться.Пока НельЗя Цикл, который будет продолжаться до тех пор, пока не будет найдена уникальная дата.Для Каждого Стр ИЗ Объект.Номенклатура Цикл).Если Стр.ДатаУстановки = УДата Тогда), текущая дата УДата увеличивается на 1 секунду, устанавливается флаг Нашел в Истина, и цикл по строкам прерывается (Прервать).Если НЕ Нашел Тогда), переменная НельЗя устанавливается в Ложь, и внешний цикл завершается.ДатаУстановки новой строки записывается найденная уникальная дата.Какие недостатки мы можем выделить в этом подходе?
Прервать, или с датой, которая также была изменена в предыдущих операциях (хотя в данном случае мы работаем только с новой строкой).В ходе обсуждения, было предложено использовать СписокЗначений для более эффективной проверки. Этот подход, основанный на предварительной сортировке, позволяет значительно улучшить производительность по сравнению с вложенными циклами.
Рассмотрим предложенное решение:
&НаКлиенте
Процедура НоменклатураПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
Если НоваяСтрока Тогда
Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
УДата = ТекущаяДата;
СпЗнач = Новый СписокЗначений;
Для Каждого Стр ИЗ Объект.Номенклатура Цикл
СпЗнач.Добавить(Стр.ДатаУстановки);
КонецЦикла;
СпЗнач.СортироватьПоЗначению(НаправлениеСортировки.Убыв);
Если СпЗнач.Количество > 0 Тогда
Если УДата <= СпЗнач[0].Значение Тогда //Вероятность совпадения 2-5% при методе "Скопировать" строку
УДата = СпЗнач[0].Значение + 1;
КонецЕсли;
КонецЕсли;
СпЗнач.Очистить();
Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = УДата;
КонецЕсли;
КонецПроцедуры
Разберем этот код по шагам:
УДата инициализируется текущей датой.СписокЗначений и добавляем в него все даты из табличной части документа.СписокЗначений по убыванию (СпЗнач.СортироватьПоЗначению(НаправлениеСортировки.Убыв)). Это позволяет нам быстро получить максимальную дату.УДата самой большой существующей дате (которая теперь находится в СпЗнач[0].Значение).УДата меньше или равна максимальной существующей дате, это означает потенциальное совпадение или конфликт, поэтому мы увеличиваем УДата на 1 секунду от максимальной даты.СписокЗначений очищается, и найденная уникальная УДата присваивается полю ДатаУстановки новой строки.Преимущества этого подхода:
Недостатки:
СписокЗначений и последующая сортировка могут все же замедлять работу, так как вся коллекция данных (Объект.Номенклатура) должна быть обработана на клиенте.Иногда требуется не просто присвоить уникальную дату новой строке, но и скорректировать уже существующие дубликаты, если они были введены ранее или возникли при копировании строк. Для таких задач можно использовать метод НайтиСтроки, который доступен для клиентских коллекций, таких как Объект.ТабличнаяЧасть.
Рассмотрим пример использования НайтиСтроки:
НайденныеСтроки = ТабличнаяЧасть.НайтиСтроки(Новый Структура("ДатаУстановки", НужнаяДата));
Если НайденныеСтроки.Количество > 1 Тогда
НайденныеСтроки.Удалить(0); // Удаляем первую найденную строку, чтобы не менять ее дату
Н = 1;
Для Каждого Строка ИЗ НайденныеСтроки Цикл
Строка.ДатаУстановки = Строка.ДатаУстановки + Н;
Н = Н + 1;
КонецЦикла;
КонецЕсли;
Пояснение к этому коду:
НайтиСтроки ищет все строки, у которых поле ДатаУстановки равно значению НужнаяДата.Однако, здесь возникает важный нюанс, на который обратили внимание участники обсуждения (сообщение 12): "поменял дату, а она уже стала пересекаться с другой - вне 'НайденныеСтроки'". Если мы корректируем дату одной строки, она может случайно совпасть с датой другой строки, которая изначально не входила в группу найденных дубликатов. Чтобы избежать этого, может потребоваться дополнительная логика:
Соответствия: Как предложил один из участников (сообщение 13), можно заводить обработанные даты в Соответствие и перед присвоением новой даты проверять, не занята ли она уже.Для создания уникальных идентификаторов времени в 1С существует функция ПолучитьОперативнуюОтметкуВремени(). Эта функция возвращает отметку времени, которая гарантированно не повторяется в течение текущего сеанса пользователя.
Ее доступность (сообщение 17):
Как мы можем ее использовать?
Хотя эта функция доступна на сервере, ее можно вызвать из клиентского кода через серверный вызов (например, в функции ПолучитьНовуюУникальнуюДатуНаСервере()). Полученное значение может служить основой для формирования даты новой строки, которая уже будет уникальна по своей природе в рамках текущего сеанса. После получения этой отметки времени, мы можем скорректировать ее на клиенте, чтобы она соответствовала нашим требованиям (например, округлить до секунды, если ПолучитьОперативнуюОтметкуВремени возвращает более точное значение).
Однако, стоит учитывать, что это не гарантирует уникальность между разными сеансами или при перезапуске системы без дополнительных проверок. В контексте предотвращения дублей в пределах секунды, эта функция может быть использована как основа для формирования даты, но все равно потребует логики для сравнения с уже существующими датами в табличной части, чтобы обеспечить полную уникальность, особенно если данные приходят из разных источников или сеансов.
Анализируя все предложенные варианты и стремясь к максимальной оптимизации производительности при минимальных затратах на серверные вызовы, мы приходим к выводу, что наиболее эффективным решением является предварительное получение максимальной даты из табличной части на сервере при открытии формы. Этот подход был предложен и подтвержден участниками обсуждения (сообщения 4, 32, 33, 39).
Разберем это решение по шагам:
Нам потребуется сделать один серверный вызов при открытии формы документа. В процедуре ПриСозданииНаСервере мы можем запросить максимальную дату из табличной части или, что еще лучше, из регистра сведений, если дата является ключевым полем регистра, куда в конечном итоге будут записываться эти данные.
Предположим, что наша табличная часть называется "Номенклатура", и мы хотим найти максимальное значение в колонке "ДатаУстановки".
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Инициализируем реквизит формы, который будет хранить максимальную дату
// Этот реквизит должен быть создан в форме с типом "Дата"
Если Объект.Номенклатура.Количество() > 0 Тогда
// Находим максимальную дату в существующей табличной части
МаксимальнаяДата = НачалоВека(); // Инициализация минимальной возможной датой
Для Каждого СтрокаТЧ ИЗ Объект.Номенклатура Цикл
Если СтрокаТЧ.ДатаУстановки > МаксимальнаяДата Тогда
МаксимальнаяДата = СтрокаТЧ.ДатаУстановки;
КонецЕсли;
КонецЦикла;
// Сохраняем максимальную дату в реквизит формы для использования на клиенте
// (Предполагается, что в форме создан реквизит "МаксДатаТЧ" типа Дата)
ЭлементыФормы.МаксДатаТЧ = МаксимальнаяДата;
Иначе
// Если табличная часть пуста, используем текущую дату как отправную точку
ЭлементыФормы.МаксДатаТЧ = ТекущаяДата();
КонецЕсли;
// Если дата является уникальным полем регистра сведений,
// можно выполнить запрос к регистру для более точного значения:
// Запрос = Новый Запрос;
// Запрос.Текст = "ВЫБРАТЬ МАКСИМУМ(ДатаУстановки) ИЗ РегистрСведений.НашиДаты";
// МаксДатаИзРегистра = Запрос.Выполнить().Выгрузить().Получить(0).МаксимумДатаУстановки;
// Если МаксДатаИзРегистра <> NULL Тогда
// Если МаксДатаИзРегистра > ЭлементыФормы.МаксДатаТЧ Тогда
// ЭлементыФормы.МаксДатаТЧ = МаксДатаИзРегистра;
// КонецЕсли;
// КонецЕсли;
КонецПроцедуры
Таким образом, мы однократно при открытии формы получаем самую "позднюю" дату, которая уже существует в документе или в связанном регистре.
После получения максимальной даты на сервере, мы сохраняем ее в реквизит формы (например, МаксДатаТЧ), который будет доступен на клиенте. Этот реквизит формы должен быть создан заранее.
Теперь, когда у нас есть максимальная существующая дата, мы можем формировать новые уникальные даты для строк табличной части полностью на клиенте, без дополнительных серверных вызовов.
Мы используем значение, сохраненное в реквизите формы, и увеличиваем его на 1 секунду при каждом добавлении новой строки. Важно также обновлять этот реквизит формы при каждом новом присвоении даты, чтобы следующая новая строка отталкивалась от свежеприсвоенного значения.
&НаКлиенте
Процедура НоменклатураПриДобавлении(Элемент, Отказ)
// Получаем последнюю известную максимальную дату
ПоследняяМаксДата = ЭлементыФормы.МаксДатаТЧ;
// Если текущая дата больше, начинаем от нее
Если ТекущаяДата() > ПоследняяМаксДата Тогда
НоваяДата = ТекущаяДата();
Иначе
НоваяДата = ПоследняяМаксДата + 1; // Увеличиваем на 1 секунду
КонецЕсли;
// Присваиваем новую дату текущей строке табличной части
Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = НоваяДата;
// Обновляем реквизит формы, чтобы следующая строка использовала эту новую максимальную дату
ЭлементыФормы.МаксДатаТЧ = НоваяДата;
КонецПроцедуры
Мы также можем реализовать эту логику в процедуре ПередОкончаниемРедактирования, как в исходном коде, если есть дополнительные условия для проверки.
Преимущества этого метода:
В ходе обсуждения часто поднимался вопрос о "преждевременной оптимизации" (сообщение 6) и о том, когда стоит переносить логику на клиент, а когда лучше оставить на сервере. Это критически важный аспект при разработке на платформе 1С.
Когда оправдано переносить логику на клиент:
Объект.ТабличнаяЧасть, данные строк, видимые и редактируемые пользователем, как правило, уже находятся на клиенте (сообщения 28, 30). Полный перенос всей коллекции на клиент происходит при открытии формы или при определенных действиях, требующих доступа ко всем данным (например, отборы).Когда предпочтительнее сервер:
Важно помнить, что любой серверный вызов несет накладные расходы, связанные с передачей контекста формы туда и обратно. Поэтому стремление минимизировать их обосновано. Однако, преждевременная оптимизация – это попытка оптимизировать то, что еще не показало себя как проблему. Сначала убедитесь, что есть реальная проблема с производительностью, прежде чем усложнять код и архитектуру.
Мы рассмотрели несколько подходов к решению задачи по исключению дубликатов по дате в колонке табличной части на клиенте 1С. Каждый из них имеет свои преимущества и недостатки:
СписокЗначений улучшает производительность на клиенте, но все еще требует обработки всей коллекции.НайтиСтроки подходит для корректировки существующих дублей, но требует внимательной дополнительной логики.ПолучитьОперативнуюОтметкуВремени может быть полезен как источник уникальных меток, но требует серверного вызова и не гарантирует уникальность вне сеанса.Выбирая решение, всегда анализируйте объем данных, сложность логики и реальные требования к производительности, чтобы найти оптимальный баланс между клиентской и серверной обработкой.