Как на клиенте 1С найти и обработать дубликаты по дате в колонке табличной части и исключить их?

Программист 1С v8.3 (Управляемые формы)
← На главную

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

Анализируем первоначальный подход к исключению дублей на клиенте

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

Автор предложил следующий вариант для обработки даты при окончании редактирования новой строки в табличной части:


&НаКлиенте
Процедура НоменклатураПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
    Если НоваяСтрока Тогда
        Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
        Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = ТекущаяДата;
        УДата = Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки;
        НельЗя = Истина;
        Пока НельЗя Цикл
            Нашел = Ложь;
            Для Каждого Стр ИЗ Объект.Номенклатура Цикл
                Если Стр.ДатаУстановки = УДата Тогда
                    УДата = УДата + 1;
                    Нашел = Истина;
                    Прервать;
                КонецЕсли;
            КонецЦикла;
            Если НЕ Нашел Тогда
                НельЗя = Ложь;
            КонецЕсли;
        КонецЦикла;
        Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = УДата;
    КонецЕсли;
КонецПроцедуры

Разберем логику этого кода по шагам:

  1. Процедура срабатывает только для новой строки при завершении ее редактирования.
  2. Вносится текущий пользователь и текущая дата в поле ДатаУстановки.
  3. Инициализируется переменная УДата текущей датой, которая будет изменяться.
  4. Запускается цикл Пока НельЗя Цикл, который будет продолжаться до тех пор, пока не будет найдена уникальная дата.
  5. Внутри этого цикла, мы перебираем все существующие строки табличной части (Для Каждого Стр ИЗ Объект.Номенклатура Цикл).
  6. Если найдено совпадение (Если Стр.ДатаУстановки = УДата Тогда), текущая дата УДата увеличивается на 1 секунду, устанавливается флаг Нашел в Истина, и цикл по строкам прерывается (Прервать).
  7. Если после перебора всех строк табличной части (или прерывания и повторного запуска цикла) совпадений не найдено (Если НЕ Нашел Тогда), переменная НельЗя устанавливается в Ложь, и внешний цикл завершается.
  8. В итоге, в поле ДатаУстановки новой строки записывается найденная уникальная дата.

Какие недостатки мы можем выделить в этом подходе?

Оптимизированный клиентский подход с использованием СпискаЗначений

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

Рассмотрим предложенное решение:


&НаКлиенте
Процедура НоменклатураПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
    Если НоваяСтрока Тогда
        Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
        УДата = ТекущаяДата;
        СпЗнач = Новый СписокЗначений;
        Для Каждого Стр ИЗ Объект.Номенклатура Цикл
            СпЗнач.Добавить(Стр.ДатаУстановки);
        КонецЦикла;
        СпЗнач.СортироватьПоЗначению(НаправлениеСортировки.Убыв);
        Если СпЗнач.Количество > 0 Тогда
            Если УДата <= СпЗнач[0].Значение Тогда //Вероятность совпадения 2-5% при методе "Скопировать" строку
                УДата = СпЗнач[0].Значение + 1;
            КонецЕсли;
        КонецЕсли;
        СпЗнач.Очистить();
        Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = УДата;
    КонецЕсли;
КонецПроцедуры

Разберем этот код по шагам:

  1. Инициализация: Как и в предыдущем случае, процедура работает для новой строки. Устанавливается текущий пользователь, а УДата инициализируется текущей датой.
  2. Сбор дат: Мы создаем новый объект СписокЗначений и добавляем в него все даты из табличной части документа.
  3. Сортировка: Ключевой шаг – мы сортируем СписокЗначений по убыванию (СпЗнач.СортироватьПоЗначению(НаправлениеСортировки.Убыв)). Это позволяет нам быстро получить максимальную дату.
  4. Проверка и корректировка:
    • Если список не пуст (т.е. в табличной части уже есть строки), мы проверяем, не меньше ли или равна ли наша УДата самой большой существующей дате (которая теперь находится в СпЗнач[0].Значение).
    • Если УДата меньше или равна максимальной существующей дате, это означает потенциальное совпадение или конфликт, поэтому мы увеличиваем УДата на 1 секунду от максимальной даты.
  5. Очистка и присвоение: СписокЗначений очищается, и найденная уникальная УДата присваивается полю ДатаУстановки новой строки.

Преимущества этого подхода:

Недостатки:

Обработка существующих дублей методом НайтиСтроки

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

Рассмотрим пример использования НайтиСтроки:


НайденныеСтроки = ТабличнаяЧасть.НайтиСтроки(Новый Структура("ДатаУстановки", НужнаяДата));
Если НайденныеСтроки.Количество > 1 Тогда
    НайденныеСтроки.Удалить(0); // Удаляем первую найденную строку, чтобы не менять ее дату
    Н = 1;
    Для Каждого Строка ИЗ НайденныеСтроки Цикл
        Строка.ДатаУстановки = Строка.ДатаУстановки + Н;
        Н = Н + 1;
    КонецЦикла;
КонецЕсли;

Пояснение к этому коду:

  1. Метод НайтиСтроки ищет все строки, у которых поле ДатаУстановки равно значению НужнаяДата.
  2. Если количество найденных строк больше 1, это означает, что есть дубликаты.
  3. В примере, первая строка из найденных дублей остается неизменной, а остальные дубликаты корректируются путем увеличения их даты на 1, 2, 3 и т.д. секунды.

Однако, здесь возникает важный нюанс, на который обратили внимание участники обсуждения (сообщение 12): "поменял дату, а она уже стала пересекаться с другой - вне 'НайденныеСтроки'". Если мы корректируем дату одной строки, она может случайно совпасть с датой другой строки, которая изначально не входила в группу найденных дубликатов. Чтобы избежать этого, может потребоваться дополнительная логика:

Использование функции ПолучитьОперативнуюОтметкуВремени для уникальных меток

Для создания уникальных идентификаторов времени в 1С существует функция ПолучитьОперативнуюОтметкуВремени(). Эта функция возвращает отметку времени, которая гарантированно не повторяется в течение текущего сеанса пользователя.

Ее доступность (сообщение 17):

Как мы можем ее использовать?

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

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

Наиболее экономичный и рекомендованный подход: предварительное получение максимальной даты на сервере

Анализируя все предложенные варианты и стремясь к максимальной оптимизации производительности при минимальных затратах на серверные вызовы, мы приходим к выводу, что наиболее эффективным решением является предварительное получение максимальной даты из табличной части на сервере при открытии формы. Этот подход был предложен и подтвержден участниками обсуждения (сообщения 4, 32, 33, 39).

Разберем это решение по шагам:

  1. Получение максимальной даты при открытии формы на сервере

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

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

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

    Таким образом, мы однократно при открытии формы получаем самую "позднюю" дату, которая уже существует в документе или в связанном регистре.

  2. Передача максимальной даты на клиент

    После получения максимальной даты на сервере, мы сохраняем ее в реквизит формы (например, МаксДатаТЧ), который будет доступен на клиенте. Этот реквизит формы должен быть создан заранее.

  3. Формирование новой уникальной даты на клиенте

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

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

    
    &НаКлиенте
    Процедура НоменклатураПриДобавлении(Элемент, Отказ)
        // Получаем последнюю известную максимальную дату
        ПоследняяМаксДата = ЭлементыФормы.МаксДатаТЧ;
                
        // Если текущая дата больше, начинаем от нее
        Если ТекущаяДата() > ПоследняяМаксДата Тогда
            НоваяДата = ТекущаяДата();
        Иначе
            НоваяДата = ПоследняяМаксДата + 1; // Увеличиваем на 1 секунду
        КонецЕсли;
                
        // Присваиваем новую дату текущей строке табличной части
        Элементы.Номенклатура.ТекущиеДанные.Вносил = Пользователь;
        Элементы.Номенклатура.ТекущиеДанные.ДатаУстановки = НоваяДата;
                
        // Обновляем реквизит формы, чтобы следующая строка использовала эту новую максимальную дату
        ЭлементыФормы.МаксДатаТЧ = НоваяДата;
    КонецПроцедуры
    

    Мы также можем реализовать эту логику в процедуре ПередОкончаниемРедактирования, как в исходном коде, если есть дополнительные условия для проверки.

Преимущества этого метода:

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

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

Когда оправдано переносить логику на клиент:

Когда предпочтительнее сервер:

Важно помнить, что любой серверный вызов несет накладные расходы, связанные с передачей контекста формы туда и обратно. Поэтому стремление минимизировать их обосновано. Однако, преждевременная оптимизация – это попытка оптимизировать то, что еще не показало себя как проблему. Сначала убедитесь, что есть реальная проблема с производительностью, прежде чем усложнять код и архитектуру.

Заключение

Мы рассмотрели несколько подходов к решению задачи по исключению дубликатов по дате в колонке табличной части на клиенте 1С. Каждый из них имеет свои преимущества и недостатки:

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

← На главную