年次リセット機能付き連番IDジェネレータ - マルチプレフィックス対応

Prev Next

To download the Yearly Resetting Sequential ID Generator - Multi-Prefix Support, visit: [tulip](https://<span translate=).co/library/apps/yearly-resetting-sequential-id-generator---multi-prefix-support/">Library

この記事では、当社のライブラリ開発者の一人であるタマシュが、ライブラリで利用可能な「年次リセット型連番IDジェネレータ - マルチプレフィックス対応」ビルディングブロックアプリの開発背景について解説します。

なぜこの方式で構築したのか

私は、信頼性が高く、再利用が容易で、多くのアプリに迅速に展開できる方法でIDを生成するという、非常に実用的な問題を解決するために、このロジックブロックを設計しました。私の目標は、ID自体に次のIDを計算するために必要なすべての情報を盛り込むことでした。IDの各部分には明確な役割があります:* プレフィックスは、そのIDがどのシーケンスに属するかを示します* 年は、シーケンスを時間軸で区切ります* 数字は、その年内の順序を示します

この構造のおかげで、リセットロジックを導入する必要はありませんでした。既存の最新のIDには、次のIDを生成するために必要なすべての情報がすでに含まれているからです。最後のIDが有効である限り、次のIDは常に予測可能な方法で計算できます。

IDに年を直接含めたのは、それが記録を時間軸で自然に区切るためです。また、これにより、定期的なリセットや手動での更新を必要とせずに、毎年自動的に番号付けを再開することができます。

プレフィックスは、異なるシーケンス間の境界として機能します。これにより、同じテーブル内で複数の独立した番号付けシーケンスを実行することが可能になります。プレフィックスを変更することは、単に別のシーケンスを継続することを意味します。

このアプローチ全体は一貫したID構造に依存しているため、検証機能を追加しました。既存のIDが想定された形式に従っていない場合、シーケンスを継続すると、混乱を招いたり一貫性を欠く結果になったりする恐れがあります。むしろ、早い段階で処理を停止し、問題を可視化して、誤ったIDが生成されるのを防ぎたいと考えました。

コネクタベースのルックアップを採用したのは、コアロジックを変更することなく、ロジックブロックを異なるテーブルで動作させられるようにするためです。コネクタの設定が完了すれば、tableIdとプレフィックスを指定するだけで済み、同じロジックブロックを複数の場所で再利用できます。

私にとっての目標は、単にIDを生成することだけではなく、IDの形式そのものがシーケンスの仕組みを定義する、再利用可能なパターンを作成することでした。それによって、ロジックをシンプルかつ予測可能にし、アプリ間で容易に再利用できるようになるのです。

このロジックブロックの機能

このロジックブロックは、以下の構造に従って、毎年リセットされる連番IDを生成します:プレフィックス + 年 + 連番

例:TEST20240001、TEST20240002

年が変わると、番号付けは自動的に1から再スタートします:TEST20250001

同じテーブル内に複数のプレフィックスが存在できます。各プレフィックスは、それぞれ独立した連番を維持します:ORD20240001ORD20240002INV20240001INV20240002

このロジックブロックにより、以下が保証されます:* 一貫したID形式* 毎年の自動リセット* プレフィックスごとの独立した番号付け* 形式不備のある過去のIDの早期検出* 数値オーバーフローの早期検出

コネクタが設計に含まれる理由

次のIDを生成する前に、ロジックブロックは指定されたプレフィックスに対する最新の既存IDを必要とします。コネクタは、フィルタリングされた検索を実行します:* IDがプレフィックスで始まるレコードをフィルタリング* _sequenceNumber で降順にソート* 最新のレコードを返す

プレフィックスがフィルタの一部であるため、同じテーブル内であっても各プレフィックスには独自の連番カウンターが割り当てられます。これにより、シーケンスが分離され、予測可能になります。

ビルドの構成

実際の実装では3つのトリガーを使用しています。この構造は意図的なものです。メインロジックを分離して再利用可能に保ちつつ、ロジックブロックを上から下まで追跡しやすいようにしたかったからです。 そこで、ロジックを多数の編集可能なトリガーに分散させるのではなく、3つの段階に整理しました:1. ユーザー設定:入力段階2. MAGIC 検証 + ID生成3. ユーザー設定:出力段階

これにより、使いやすさと制御性のバランスがうまく取れています。* 最初のトリガーでは、入力を定義します。* 2番目のトリガーには、ロジックブロックが機能するために通常は編集する必要のない内部ロジックが含まれています。* 3番目のトリガーでは、生成された結果をどう処理するかを決定します。

つまり、通常はコアロジック・トリガー内部を操作する必要はありません。ユーザーは入力側と出力側を設定するだけで済みます。これが重要な設計上の決定事項の一つでした。通常変更が必要な部分のみを公開することで、ロジックブロックを再利用可能にするというものです。

トリガー 1 – USER CONFIG: INPUT STAGE

これは、ユーザーが最初に操作するトリガーです。その目的は、IDを生成する前に入力値としてロジックブロックが必要とする値を定義することです。実際には、このトリガーを使用して以下を定義します:*プレフィックス:_configPrefix* IDの長さ:_configIdNumberFormat* コネクタに渡されるテーブルID:tableId

各値の意味

_configPrefix:これは、生成されるすべてのIDの先頭に付加されるテキストです。これを使用して、生成したいシーケンスを定義します。

例:* ORD* INV* TEST

_configPrefix = ORD の場合、ロジックブロックは ORD で始まる既存の ID のみを参照し、そのシーケンスのみを継続します。したがって、この値には 2 つの役割があります:* 最終的な ID に表示されるプレフィックスを定義する* コネクタが次の番号のソースとして使用する過去の ID を定義する

つまり、同じテーブル内で異なるプレフィックスを設定すると、異なるシーケンスが生成されます**。_configIdNumberFormat**

これは、ID の数値部分の長さを定義します。ゼロで埋められた文字列として設定します。例:000000000000000000

ゼロの数は、数値部分の幅を定義します。

例:0000 → 0001、0002、0003 … 9999000000 → 000001、000002 … 999999

この値は実際の数値を設定するものではありません。数値が収まるべき形式を定義するものです。私はこれを2つの目的で使用しています:* 生成された数値に先頭のゼロを追加するため* 次の数値が許容長内に収まるかどうかを検証するため

tableId

これは、コネクタがどのテーブルに対してクエリを実行すべきかを定義します。

実際には、これによりロジックブロックを異なるテーブル間で再利用できるようになります。同じロジックブロックをコピーし、設定トリガー内のこの値のみを変更することで、別のテーブルを指定することができます。

ロジックブロック自体はテーブルを検索しません。tableIdと**_configPrefix**をコネクタに渡すと、コネクタがそのテーブルから一致する最新の ID を返します。

これらを組み合わせて使用する方法

これら 3 つの値が組み合わさって、ジェネレータの動作を定義します:*tableId は、ロジックブロックがどこを参照すべきかを指定します*_configPrefix は、どのシーケンスを継続すべきかを指定します*_configIdNumberFormat は、数値部分がどのように表示されるべきかを指定します

実践的な設定ガイド

ロジックブロックを設定する際は、以下を確認してください:*tableId が正しいテーブルを指していること*_configPrefix が継続したいプレフィックスと一致していること*_configIdNumberFormat が適用したい数値の長さと一致していること

プレフィックスを変更すると、以前のシーケンスを継続することはできなくなります。別のプレフィックス固有のシーケンスを開始、または継続することになります。後で数値形式の長さを増やした場合、ロジックブロックはそのテーブルおよびプレフィックスの範囲内で既存のIDを検証する際、その長い形式を想定するようになります。コネクタは、プレフィックスに一致する最新のIDを取得し、それを**_stateLastId**に格納します。まだIDが存在しない場合、**LINK()**により、ジェネレータが最初の有効なIDを生成できるよう保証されます。

コネクタが設計の一部である理由

コネクタは単なる技術的な詳細ではありません。これは再利用戦略の一部です。私はコネクタを使用して、ターゲットテーブルから最新の既存IDを読み取ります。コネクタはテーブルIDを入力として受け取り、プレフィックスフィルターを適用し、レコードを**_sequenceNumberで**降順にソートして、一致する最新のレコードを返します。

これが重要な理由は2つあります。

第一に、これによりロジックブロックがテーブルの実際の状態から連番を継続できるようになります。

第二に、これによりロジックブロックの再利用がはるかに容易になります。ボタンを別のアプリにコピーする場合でも、ロジックを書き直す必要はありません。 コネクタを正しいテーブルに指定し、必要な設定を行うだけで済みます。これが、このビルドの背後にある実用的なパターンです:* ボタンを一度作成する* コネクタでテーブル入力を設定する* プレフィックスと数値の形式を定義する* 必要な場所で再利用する

これは、アプリ構築の高速化を支援するための意図的な選択でした。

ロジックブロック内のコネクタはどのような処理を行うのでしょうか?

ここでは次のような処理が行われます:* コネクタがテーブルIDを受け取ります* コネクタがプレフィックスを受け取ります* コネクタがプレフィックスでレコードをフィルタリングします* コネクタが_sequenceNumberでレコードを降順にソートします* コネクタが一致する最新のIDを返します

また、コネクタは「同じテーブル内の複数のエンティティ」という非常に重要なユースケースにも対応しています。

同じテーブルに次のようなレコードが含まれている場合:ORD20240001MAT20240011

といったレコードが含まれている場合、ロジックブロックはこれらを1つの共有シーケンスとして扱ってはなりません。各プレフィックスは、それぞれ自身の最新の値からのみ継続する必要があります。

そのため、コネクタはまずプレフィックスでフィルタリングを行います。つまり、次の値は次のようになります:ORD → ORD20240002、MAT → MAT20240012

このモデルを選んだ理由は、異なるエンティティが同じテーブルに存在することはあっても、それらが同じ連番を共有すべきではないからです。

設定要件として、明確に述べておくべき点がもう1つあります。コネクタには認証が必要であるため、トークンを生成し、コネクタの認証設定に追加する必要があります。私はこれをロジックブロック自体のロジックの一部ではなく、セットアップ作業として扱っていますが、ロジックブロックを実用的にするために重要な要素であることに変わりはありません。

トリガー 2 – MAGIC:検証 + ID 生成

このトリガーは、ロジックブロックのコアロジックです。ここでは、次の3つの処理を順に行います:1. 最新の一致するIDが期待される構造になっているかを確認する2. 次の番号が、設定された数値の長さに収まるかどうかを確認する3. 次のIDを生成する

これらのステップは同じ決定フローに属するため、1つのトリガーにまとめました。新しいIDを作成する前に、次の3つの質問に答えたいと考えています:* 前のIDは構造的に安全に使用できますか?* 次の番号は依然としてフォーマットに適合しますか?* もしそうなら、次のIDは何にするべきですか?

フォーマットの検証

Expression

REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + DATETIMETOTEXT(App info.Current Date and Time,'YYYY') + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}' + '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$','ok')

このexpression の機能

このexpression は、最新のIDがロジックブロックが期待する構造と完全に一致するかどうかを検証します。動作順序は以下の通りです:1. コネクタが値を返した際、LINK(...) はVariable._stateLastId.idを取得します。2. まだIDが存在しない場合、 **LINK(...)**は以下の要素から構成されたフォールバック値を使用します: *Variable._configPrefix *DATETIMETOTEXT(...,'YYYY') *Variable._configIdNumberFormat3. このフォールバック値を含めることで、これが最初に生成された ID であっても、検証が評価可能な有効な構造を持つようにしています。4.Variable._configPrefixは、ID の先頭に必ず含まれなければならない正確なプレフィックスを定義します。5.**DATETIMETOTEXT(...,'YYYY')**は、正規表現内部での照合値としては使用されませんが、フォールバック構造の一部として含まれることで、フォールバックが実際の ID のような形式に見えるようにしています。6.**LEN(Variable._configIdNumberFormat)**は、数値部分が何桁で構成されるべきかを定義します。7. その後、**REGEX_REPLACE(...)**によって、文字列全体がこのパターンに照合されます: * プレフィックス * 年は正確に 4 桁 * 数値部分は正確に N 桁8. 文字列全体がパターンに一致した場合、値全体が「ok」に置換されます。9. 一致しない場合、元の値は変更されません。10. その後、トリガーは結果が「ok」と等しいかどうかを確認します。等しくない場合、実行は停止します。

したがって、このexpression は計算用の値を抽出するものではありません。その役割は、数値処理が適用される前に、IDが期待される構造になっていることを検証することです。

なぜこの方法を選んだのか

数値処理を行う前に、IDの構造全体を検証することにした。トリガーの残りの部分が以下の固定位置を前提としているため、このように構築した:* 先頭にプレフィックス* プレフィックスの直後に年* 年の直後に番号

前のIDがこの規約に従っていない場合、それを基にインクリメントを行うと信頼性が低下してしまいます。

また、最初に生成されるIDに対して別途検証ロジックを必要としないよう、ここに**LINK(...)**によるフォールバックを含めることにしました。 古いIDが存在するかどうかにかかわらず、同じ検証パスが機能します。私は、形式不備なIDを修復しようとするのではなく、意図的にトリガーを停止させています。自動修正を行うと、データの問題が隠蔽され、一見有効に見えるものの、誤った入力に基づいて構築されたシーケンスが生成される恐れがあるからです。

オーバーフロー防止

Expression

LEN(IF(MID(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),LEN(Variable._configPrefix),4) = DATETIMETOTEXT(App info.Current Date and Time,'YYYY'),RIGHT(Variable._configIdNumberFormat+ TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1),LEN(Variable._configIdNumberFormat)),RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat))))

このexpression の機能

このexpression は、次の数値部分が何になるかを計算し、その長さを測定します。処理の流れは以下の通りです:1.LINK(...)は、IDが存在する場合、その最後のIDを使用します。存在しない場合は、Variable._configPrefix + Variable._configIdNumberFormatにフォールバックします。2.**MID(..., LEN(Variable._configPrefix), 4)**は、プレフィックスの直後に続く 4 文字を読み取ります。これは、前の ID の年部分として扱われます。3.**DATETIMETOTEXT(...,'YYYY')**は、比較用の今年度を提供します。4.IF(...) は、数値部分を継続するかリセットするかを決定します: * 同じ年 → インクリメント * 異なる年 → 15 から開始。 インクリメントの分岐では、**REGEX_REPLACE(...)**によって、前の ID の先頭からプレフィックスと 4 桁の年が削除され、数字の部分のみが残ります。6.TEXTTOINTEGER(...) + 1によって、その数字のテキストが数値に変換され、1 ずつインクリメントされます。7.TOTEXT(...) は、新しい数値を再びテキストに変換します。8.**Variable._configIdNumberFormat + ...**は、設定されたゼロ埋め形式を値の先頭に付加します。9.RIGHT(..., LEN(Variable._configIdNumberFormat)) は、必要な桁数のみを残します。10. リセット分岐では、**RIGHT(Variable._configIdNumberFormat + '1', LEN(...))**によって、新しい年に使用可能な最初の番号が生成されます。11.**LEN(...)**は、結果として得られる数値部分の長さを測定します。

トリガーは、この長さを Variable._configIdNumberFormat で指定された許容長と比較します。次の数値部分が長すぎる場合は、実行が停止します。

これを選んだ理由

許可された長さを超えるのはいつになるかを推測するために別の数式を使用するのではなく、ID が実際に生成されるのと同じ方法で次の番号を計算することにしました。

このように構築したのは、オーバーフローチェックを実際のID生成とまったく同じルールに従わせたかったからです。これにより、後で検証ロジックと生成ロジックの間に乖離が生じる可能性を減らすことができます。

ここで処理を停止する方が、処理を続行するよりも安全です。次の数値が設定された長さに収まらなくなると、生成されるIDは設定した構造に違反することになります。

たとえば、数値形式が「0000」の場合、「9999」は有効ですが、「10000」は無効です。このロジックブロックでは、レコード作成後ではなく、作成前にその問題を検出したいと考えています。

ID生成ロジック

Expression

Variable._configPrefix + DATETIMETOTEXT(App info.Current Date and Time,'YYYY')+ IF(MID(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),LEN(Variable._configPrefix),4) = DATETIMETOTEXT(App info.現在の日付と時刻,'YYYY'),RIGHT(Variable._configIdNumberFormat+ TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1),LEN(Variable._configIdNumberFormat)),RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat)))

このexpression の機能

これは、最終的なIDを生成するexpression です。以下の順序で動作します:1.Variable._configPrefix は、IDの先頭に設定されたプレフィックスを追加します。2.**DATETIMETOTEXT(App info.Current Date and Time,'YYYY')**は、現在の4桁の年を追加します。3.LINK(...) は、コネクタが存在する場合、コネクタから返された最後の ID を使用します。コネクタが ID を返さない場合は、Variable._configPrefix + Variable._configIdNumberFormatをフォールバック構造として使用します。4.**MID(..., LEN(Variable._configPrefix), 4)は、プレフィックスの直後に続く 4 文字を読み取ります。これらは、前の ID の年部分として扱われます。5. ここで再びDATETIMETOTEXT(...,'YYYY')**を使用することで、expression が前の ID が今年のものであるかどうかを判断できるようにします。6.**IF(...)**は、2 つの可能な経路を制御します: * 同じ年 → 連番を継続 * 異なる年 → 1 にリセット7. 同年の分岐では、**REGEX_REPLACE(...)**によって、前の ID の先頭からプレフィックスと 4 桁の年が削除され、数値部分のみが残ります。8.TEXTTOINTEGER(...) + 1 ** によって、その数値に1が加算されます。9. TOTEXT (...) によって、 加算された値が再びテキストに変換されます。10. Variable._configIdNumberFormat + ... は、設定されたゼロ埋め形式を新しい値の先頭に付加します。11. RIGHT (..., LEN(Variable._configIdNumberFormat)) は、必要な桁数のみを保持するため、数値部分は常に固定長になります。12. リセット分岐では、RIGHT (Variable._configIdNumberFormat + '1', LEN(...))** によって、新しい年次シーケンスの最初の番号が生成されます。13. プレフィックス、今年度、およびフォーマットされた数値が連結され、最終的な ID が生成されます。

したがって、最終的な出力は常に次の構造になります:プレフィックス + 年 + 固定長数値

この方法を選んだ理由

次のIDを、テーブル内の最新の実IDから完全に導出する方式を採用しました。このように構築したのは、前のIDにはロジックに必要な情報がすべて含まれているためです:* そのシーケンスがどのプレフィックスに属するか* それがどの年に属するか* 最後の数値が何であったか

つまり、別途カウンターテーブルや年を格納するフィールド、あるいは年次リセットのスケジュール設定は必要ありません。

また、構造が予測可能で検証しやすくなるため、固定長のフォーマットを採用しました。

その代償として、expression は厳格なID構造に依存することになります。そのため、検証ステップを最初に配置しています。ここでは、多くの緩い入力形式をサポートしようとするよりも、厳格で読みやすい契約の方が有用であると考え、このトレードオフを受け入れました。

トリガー 3 – USER CONFIG: 出力ステージ

これはロジックブロック内の最後のトリガーです。その目的は、_stateOutputに有効な値が含まれた際に、生成されたIDに対してどのような処理を行うかを定義することです。

ここで重要な点は、この段階が意図的にユーザーに委ねられていることです。

_stateOutputが空白でない場合、ロジックブロックが次に何を行うべきかを決定できます。現在の設定では、このトリガーは次の手順を説明するガイダンスメッセージを表示します:* 設定されたテーブルをアプリに追加する* メッセージステップを「レコード作成」トリガーに置き換える*_stateOutputを新しいレコードIDとして使用する

ユースケースによって出力の挙動が異なる可能性があるため、このように構築しました。

次のような処理を行いたい場合もあるでしょう:* すぐにレコードを作成する* まず ID を別の変数に格納する* より長いワークフロー内で ID を使用する* テーブルに書き込む前に追加の条件を検証する

1つの固定された出力動作をハードコーディングする代わりに、出力ステージを構成可能にしておきました。

そのため、ロジックブロックの両端にユーザー向けの設定が用意されています:* 入力構造は設定可能* 出力動作は設定可能* 中核となるロジックは中央に配置

変数名の付け方

内部変数にはアンダースコアを接頭辞として使用しています:* _configPrefix* _configIdNumberFormat* _stateLastId* _stateOutput

これにより、設定値と内部処理変数を素早く区別できるようになります。

設計の境界

ロジックブロックは、以下の構造を前提としています:プレフィックス + 年 + 番号。各プレフィックスは、それぞれ独立したシーケンスを維持します。

ロジックブロックでは、複数のプレフィックスにまたがる単一の共有カウンタはサポートされていません。

ID 内で年または数字の部分が移動された場合、expressions もそれに応じて調整する必要があります。

構築に関する決定事項のまとめ

ロジックブロック全体を、その根底にある中核的な決定に絞り込むと、以下のようになります:* 迅速な再利用を望んだため、ロジックブロックは設定駆動型となっています。* テーブルを「真実の源」としたかったため、次のIDは常に最新の実際のレコードに基づいています。* ロジックの保守性を維持したかったため、計算を一箇所に集約した。* 1つのテーブル内の複数のエンティティがスムーズに機能するようにしたかったため、各プレフィックスには独自のシーケンスを設定した。* ID構造を長期的に一貫性を保たせたかったため、年をIDの一部とし、年が変更されるとシーケンスが自動的にリセットされるようにした。* テーブルを不適切なパターンから保護したかったため、フォーマットの検証とオーバーフローチェックにより、レコードが作成される前に不正なIDを阻止するようにしました。* トリガーの流れを読みやすくしたかったため、役割を「設定」「検証」「生成」「出力」という明確なステップに分離しました。

これが、ロジックブロックが現在の形になっている理由です。これは単なるIDジェネレータではありません。IDの構造に年ごとの区分を直接組み込みつつ、シンプルさ、再利用性、データ保護のバランスを図った、制御されたID生成のための再利用可能なパターンなのです。

参加する

コミュニティに参加して、改善案の共有、追加ユニットの提案、問題の報告、あるいはアーキテクチャ上の決定事項について議論しましょう。