To download the Sequential ID Generator, visit: [tulip](https://<span translate=).co/library/apps/sequential-id-generator---multiple-prefix-support">Library
В этой статье один из наших разработчиков библиотеки, Тамаш, рассказывает о решениях, лежащих в основе приложения-строительного блока «Генератор последовательных идентификаторов», доступного в библиотеке.
Почему я создал его именно так
Я хотел, чтобы этот логический блок решал очень практическую задачу: генерировать идентификаторы надежным, удобным для повторного использования и быстрым для внедрения во многие приложения способом.
Самой важной целью проектирования было не просто «сгенерировать следующее число». Настоящая цель заключалась в том, чтобы создать нечто такое, что, будучи однажды определённым, можно скопировать и повторно использовать с минимальными усилиями. Если кнопка создана один раз правильным образом, то должно быть возможно разместить её в другом приложении или на другом шаге, просто настроив параметры. Я не хотел, чтобы каждый разработчик приложений каждый раз вновь открывал логику и переписывал триггеры.
Именно поэтому этот логический блок разработан так, чтобы приоритет отдавался настройкам, а не самой логике. При использовании этого логического блока вам в основном нужно подумать о следующем:* какой префикс необходим* какой длины должна быть числовая часть* из какой таблицы должна считываться информация кнопкой
Как только вы определите префикс, формат числа и таблицу, логические блоки уже сами должны позаботиться об остальном: считывании последнего ID, его проверке, генерации следующего значения и проверке, соответствует ли число настроенной длине.
Сама таблица должна быть «источником истины». Отдельный счетчик (например, переменная) может отклоняться от реальных данных, если записи вставляются, удаляются или редактируются непредвиденным образом. Чтение из таблицы позволяет привязать нумерацию к тому, что действительно существует.
Еще одним важным решением было сосредоточить основную логику вычислений в одном месте. Я не хотел, чтобы одна ветвь триггера отвечала за первый ID, другая — за следующий, а затем появлялись дополнительные мелкие фрагменты триггеров, которые впоследствии стало бы сложно отслеживать. Я хотел, чтобы основная логика находилась в одном блоке « expression », чтобы поведение оставалось последовательным, и любой, кто будет вносить в него изменения позже, точно знал, где искать.
Таким образом, блок логики построен на основе нескольких практических решений:* простота копирования и повторного использования* достаточная безопасность для защиты таблицы от некорректных шаблонов ID* простота настройки* централизованная логика, обеспечивающая удобство обслуживания
Как выглядит идентификатор
Каждый сгенерированный идентификатор состоит из двух частей:префикс + числовая последовательность
Примеры:* ORD00000125* MAT00000003* TEST00004567
Префикс указывает, с каким типом записи мы имеем дело. Числовая часть представляет собой последовательность, нарастающую по порядку. Я решил сделать настройку формата чисел очень простой. Вместо того чтобы запрашивать у пользователя отдельное число, например «8 цифр», логический блок использует шаблон с нулями, например:* 00000000
Так это легче воспринять с первого взгляда. Если кто-то видит восемь нулей, он сразу понимает, что числовая часть всегда должна состоять из восьми символов. Именно поэтому я оставил idNumberFormat в таком виде, а не заменил его на чистое поле длины.
Примеры:* 00000000 → 8-значное число* 0000 → 4-значное число
Это небольшое решение, но оно имеет значение. Оно делает логический блок более доступным для людей, которые не хотят мыслить категориями технических параметров.
Как устроена сборка
В реальной реализации используются три триггера. Такая структура выбрана намеренно. Я хотел, чтобы логический блок было легко проследить сверху вниз, при этом сохранив основную логику отдельной и пригодной для повторного использования. Поэтому вместо того, чтобы распределять логику по множеству редактируемых триггеров, я организовал её в три этапа:1. НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ: ЭТАП ВХОДА 2. МАГИЯ 3. НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ: ЭТАП ВЫХОДА
Это обеспечивает хороший баланс между удобством использования и возможностью управления.* В первом триггере вы определяете входные данные.* Второй триггер содержит внутреннюю логику, которую обычно не нужно редактировать для работы логического блока.* В третьем триггере вы решаете, что делать с полученным результатом.
Это означает, что обычно вам не нужно работать внутри триггера основной логики. Достаточно настроить только входную и выходную стороны. Это было одним из ключевых проектных решений: сделать логический блок многоразовым, открыв доступ только к тем частям, которые обычно требуют изменения.
Триггер 1 — НАСТРОЙКА ПОЛЬЗОВАТЕЛЯ: СТАДИЯ ВХОДА
Это первый триггер, с которым вы взаимодействуете. Его цель — определить входные значения, необходимые логическому блоку перед генерацией любого идентификатора.На практике этот триггер используется для определения:* префикса* формата идентификатора (idNumberFormat)* идентификатора таблицы, передаваемого в коннекторПример:префикс = ORформат идентификатора = 00000000идентификатор таблицы = целевая таблица
Этот этап намеренно сделан редактируемым, поскольку именно эти значения обычно меняются в зависимости от конкретного случая использования.
Вы можете взять ту же кнопку, скопировать её в другое приложение или шаг, обновить эти значения конфигурации и повторно использовать логический блок, не затрагивая внутреннюю логику.
Это ускоряет реализацию, поскольку после определения входных значений остальная часть потока работает автоматически. Этот триггер также запускает вызов коннектора, который считывает последнюю соответствующую запись из выбранной таблицы.
Почему коннектор является частью проекта
Коннектор — это не просто техническая деталь. Он является частью стратегии повторного использования. Я использую коннектор для чтения последнего существующего ID из целевой таблицы. Коннектор принимает ID таблицы в качестве входных данных, применяет фильтр префиксов, сортирует записи по _sequenceNumber в порядке убывания и возвращает последнюю соответствующую запись. Это важно по двум причинам. Во-первых, это позволяет блоку логики продолжить нумерацию с реального состояния таблицы. Во-вторых, это значительно упрощает повторное использование блока логики. Если скопировать кнопку в другое приложение, не нужно переписывать логику. Достаточно лишь направить коннектор на нужную таблицу и указать необходимые настройки. Вот практический паттерн, лежащий в основе этой реализации: 1. создать кнопку один раз; 2. задать входные данные таблицы в коннекторе; 3. определить префикс и формат номера; 4. повторно использовать её везде, где это необходимо.
Это был осознанный выбор, направленный на ускорение разработки приложений.
Что делает коннектор в логическом блоке?
Что происходит здесь: * коннектор получает ID таблицы; * коннектор получает префикс; * коннектор фильтрует записи по префиксу; * коннектор сортирует записи по _sequenceNumber в порядке убывания; * коннектор возвращает последний совпадающий ID
Коннектор также поддерживает очень важный сценарий использования: наличие нескольких сущностей в одной таблице.
Если в одной и той же таблице содержатся такие записи: ORD00000011 MAT00000003, то логический блок не должен рассматривать их как одну общую последовательность. Каждый префикс должен продолжаться только от своего собственного последнего значения.
Поэтому коннектор сначала фильтрует по префиксу. Это означает, что следующими значениями станут: ORD → ORD00000012, MAT → MAT00000004
Я выбрал эту модель, потому что в одной таблице могут находиться разные сущности, но это не означает, что они должны использовать один и тот же порядковый номер.
Стоит также чётко указать одно требование к настройке: коннектор требует аутентификации, поэтому необходимо сгенерировать токен и добавить его в настройки аутентификации коннектора. Я рассматриваю это как работу по настройке, а не как часть самой логики логического блока, но это всё же важная составляющая, обеспечивающая работоспособность логического блока.
Я оставил это на этапе ввода, поскольку это относится к настройке и сбору контекста. Прежде чем внутренняя логика сможет заработать, логический блок сначала должен знать, с чем он работает.
Триггер 2 — MAGIC
Это триггер основной логики. Я специально назвал его так. Идея проста: эту часть обычно не нужно редактировать. Логический блок уже должен содержать эту логику. Этот триггер принимает настроенные входные значения и последний возвращённый ID от предыдущего триггера, а затем выполняет внутреннее принятие решения. Хотя это один триггер, внутри него происходит несколько действий.
1. Проверка формата — часть триггера «if»
Первая проверка подтверждает, что последний ID, возвращённый из таблицы, по-прежнему соответствует ожидаемой структуре. Ожидаемая структура: **префикс + число фиксированной длины
Пример допустимого значения: ORD00000025 Примеры недопустимых значений: ORD25, ORD-00025, ABC00000025, ORD00025A
Expression
REGEX_REPLACE( LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat), '^' + Variable._configPrefix + '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$', 'ok')
Что делает этот expression
Этот блок « expression » проверяет, соответствует ли последний ID точно той структуре, которую ожидает логический блок. Он делает это в три этапа:* LINK(...) определяет, какое значение следует проверить. Если коннектор не возвращает идентификатор, вместо него используется вторая часть. Эта вторая часть представляет собой настроенную начальную структуру и позволяет логическому блоку сгенерировать первый идентификатор для новой последовательности префиксов.* '^' + Variable._configPrefix означает, что значение должно начинаться с настроенного префикса.* '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$' означает, что значение должно заканчиваться ровно ожидаемым количеством цифр, и после этого не допускается ничего лишнего.
Если всё значение соответствует ожидаемой структуре, результат становится «ok». Если оно не соответствует, результат « expression » остаётся отличным от «ok», и триггер останавливает поток генерации и выводит сообщение об ошибке
Почему я выбрал именно это
Я добавил эту проверку, потому что не хотел, чтобы логический блок продолжал работу на основе некорректного идентификатора. Если последнее сохраненное значение неверно, а логика его принимает, то неверный шаблон может начать распространяться на будущие записи. Поэтому я хотел, чтобы первоочередной задачей триггера MAGIC была остановка этого процесса на ранней стадии. Если формат неверный: * _stateOutput обнуляется * отображается сообщение с объяснением, почему генерация остановилась
2. Проверка на переполнение
Вторая проверка анализирует следующее числовое значение и удостоверяет, что оно по-прежнему укладывается в настроенную длину числа.
Пример: idNumberFormat = 0000
Допустимый диапазон: от 0000 до 9999
Если следующее значение станет: 10000, то оно уже не будет соответствовать настроенной структуре.
Expression
LEN(Variable._configIdNumberFormat) < LEN( TOTEXT( TEXTTOINTEGER( REGEX_REPLACE( LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat), '^' + Variable._configPrefix, '' ) ) + 1 ))
Что делает эта функция expression
Этот выражение типа « expression » сравнивает две длины:* допустимую длину числового значения, взятую из idNumberFormat* длину следующего числа после инкремента текущего значения
Правая часть работает следующим образом:* LINK(...) использует ID, возвращаемый коннектором, если таковой имеется. Если коннектор не возвращает ID, вместо него используется настроенная начальная структура. Именно это делает возможным генерацию первого ID.* REGEX_REPLACE(...) удаляет префикс, оставляя только числовую часть* TEXTTOINTEGER(...) + 1 вычисляет следующее числовое значение* TOTEXT(...) преобразует это следующее значение обратно в текст* LEN(...) определяет, сколько цифр требуется для этого следующего значения
Если следующему числу требуется больше цифр, чем допускает формат, условие становится истинным, и триггер останавливает процесс.
Почему я выбрал именно это
Я решил остановить процесс на этом этапе, а не автоматически изменять структуру. Это решение было важным, поскольку я хотел, чтобы длина ID оставалась постоянной, пока кто-то явно не изменит конфигурацию. Если обнаружен переполнение:* _stateOutput очищается* отображается сообщение с объяснением, что достигнута максимальная длина
3. Генерация ID
Если обе проверки пройдены успешно, триггер генерирует следующий ID. Пример: ORD00000015 → ORD00000016
Этот же триггер « expression » также обрабатывает случай, когда для данного префикса ещё не существует записи. В этом случае логика переключается на настроенную начальную структуру и генерирует первое значение в последовательности. Пример: ORD00000001
Expression
Variable._configPrefix + RIGHT( Variable._configIdNumberFormat + TOTEXT( TEXTTOINTEGER( REGEX_REPLACE( LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat), '^' + Variable._configPrefix, '' ) ) + 1 ), LEN(Variable._configIdNumberFormat))
Что делает эта функция « expression »
Это функция « expression », которая формирует окончательный идентификатор. Она работает в следующем порядке:* LINK(...) использует последний идентификатор, возвращённый коннектором, если таковой имеется. Если коннектор не возвращает идентификатор, вместо него используется настроенная начальная структура. Именно это позволяет одной и той же функции « expression » обрабатывать также и первый сгенерированный идентификатор.* REGEX_REPLACE(...) удаляет префикс, чтобы можно было обработать числовую часть* TEXTTOINTEGER(...)+ 1 увеличивает число* TOTEXT(...) преобразует новое число обратно в текст* Variable._configIdNumberFormat + ... добавляет нулевой префикс перед новым значением* RIGHT(..., LEN(Variable._configIdNumberFormat)) сохраняет только необходимое количество цифр* Variable._configPrefix + ... добавляет префикс обратно для формирования окончательного идентификатора. Результат сохраняется в _stateOutput.
Почему я выбрал именно это
Я намеренно оставил это в виде одного центрального блока « expression ». Я не хотел создавать отдельные внутренние ветвления для первого и следующего идентификаторов, если можно этого избежать. Сохранение вычислений в одном месте упрощает повторное использование и делает будущие изменения более безопасными. В конце этого триггера сгенерированное значение сохраняется в _stateOutput.
Триггер 3 — НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ: ЭТАП ВЫВОДА
Это последний триггер в логическом блоке. Его цель — определить, что должно произойти с сгенерированным ID, как только _stateOutput будет содержать допустимое значение. Важно отметить, что этот этап намеренно оставлен открытым для вас. Если _stateOutput не пуст, вы можете решить, что должен делать логический блок дальше.В текущей настройке триггер отображает подсказку, объясняющую, что делать дальше:* добавить настроенную таблицу в приложение* заменить этот шаг с сообщением на триггер «Создать запись »* использовать _stateOutput в качестве ID новой записи
Я реализовал это именно так, поскольку поведение вывода может варьироваться в зависимости от сценария использования. Возможно, вам захочется создать запись сразу. Возможно, вы сначала захотите сохранить значение в другой переменной. Или, возможно, вы захотите использовать его в более обширном рабочем процессе, прежде чем записывать что-либо в таблицу. Поэтому вместо того, чтобы жестко закрепить одно фиксированное поведение вывода, я оставил этап вывода настраиваемым. Именно поэтому часть, видимая пользователю, присутствует на обоих концах:* входные данные можно настроить* выходные данные можно настроить* основная логика остается посередине
Почему триггеры разделены именно таким образом
Эта структура отражает один из основных проектных решений во всём логическом блоке: разделять ответственности, но сохранять централизацию основной логики.Именно поэтому я не поместил всё в один огромный триггер. Один гигантский триггер, возможно, и сработал бы, но он становится сложнее для чтения, сложнее для объяснения и сложнее для отладки. С другой стороны, разбиение фактических математических вычислений на множество мелких частей также приводит к беспорядку. Поэтому я выбрал золотую середину:* отдельные триггеры для удобства чтения отдельных шагов* один централизованный триггер « expression » для фактического вычисления ID
Это даёт мне преимущества обоих подходов:* сборку проще отслеживать* логику проще поддерживать* повторное использование упрощается* будущие изменения сопряжены с меньшим риском
Почему валидация и защита важны в этом логическом блоке
Этот логический блок предназначен не только для генерации следующего числа. Он также служит для защиты таблицы от некорректных данных. Существует два основных аспекта защиты:
Защита формата
Перед продолжением работы логический блок проверяет, соответствует ли последний существующий ID ожидаемой структуре. Это предотвращает продолжение последовательности на основе ID, не соответствующих ожидаемой структуре.
Защита от переполнения
Логический блок проверяет, укладывается ли следующее число в допустимую длину числа. Это предотвращает непредвиденное изменение структуры по мере роста последовательности. Я счел оба этих аспекта важными, поскольку некорректные данные ID могут быстро распространиться. Как только неверный шаблон попадает в таблицу и генератор начинает его использовать, эта проблема может повлиять на каждую будущую запись. Поэтому логический блок спроектирован так, чтобы останавливаться на ранней стадии, а не молча продолжать работу в неправильном направлении.
Подход к именованию переменных
Вы, возможно, заметили, что имена переменных, используемые внутри логического блока, следуют определённому шаблону. Примеры:_configPrefix, _configIdNumberFormat, _stateLastId, _sequenceNumber
Я намеренно использовал стиль именования, который легко отличить от переменных, обычно создаваемых внутри приложений. Цель здесь не техническая, а практическая. Когда вы импортируете или копируете логический блок в приложение, должно быть сразу видно, какие переменные относятся к логическому блоку, а какие — к логике приложения.Переменные логического блока имеют единообразную структуру, благодаря чему они визуально сгруппированы и с меньшей вероятностью вступают в конфликт с пользовательскими переменными. Это упрощает повторное использование логического блока и делает его интеграцию в существующие приложения более безопасной, без конфликтов при переименовании и путаницы.
Краткое изложение решений по построению
Если свести весь логический блок к лежащим в его основе ключевым решениям, то они следующие:* Мне требовалось быстрое повторное использование, поэтому логический блок управляется конфигурацией.* Мне требовалось, чтобы таблица была «источником истины», поэтому следующий ID основан на последней реальной записи.* Мне требовалось, чтобы логика оставалась удобной для обслуживания, поэтому вычисления централизованы.* Я хотел, чтобы несколько сущностей в одной таблице работали корректно, поэтому у каждого префикса есть своя последовательность.* Я хотел защитить таблицу от некорректных шаблонов, поэтому проверки формата и переполнения предотвращают попадание некорректных данных до создания записи.* Я хотел, чтобы поток триггеров был понятным, поэтому обязанности разделены на четкие шаги.
Именно поэтому блок логики выглядит именно так. Это не просто генератор идентификаторов. Это многократно используемый шаблон для контролируемого создания идентификаторов, который стремится к балансу между простотой, возможностью повторного использования и защитой данных.
Присоединяйтесь
Присоединяйтесь к сообществу, чтобы делиться улучшениями, предлагать дополнительные модули, сообщать о проблемах или обсуждать архитектурные решения.
