データ連携のID設計|自然キー・UUID・外部IDで重複を防ぐ

「データ連携のID設計|自然キー・UUID・外部IDで重複を防ぐ」の内容を表す技術イラスト

データ連携では、値を正しく変換できても「同じものを同じものとして認識できない」と処理が破綻します。

同じ部品の二重登録、名称変更による別レコード化、異なるシステム間での123の衝突。こうした問題の中心にあるのがID設計です。

この記事では、自然キー、サロゲートキー、外部ID、UUIDの役割を分け、API、CSV、SQLite、Pythonへ再利用できる形で整理します。

目次

IDが解決する問題

IDは、データの同一性を判断し、別のレコードから参照するための契約です。

IDには、少なくとも次の性質が求められます。

  • 対象範囲内で一意である
  • 一度割り当てた後は原則として変わらない
  • 名称や説明など、変更される属性から独立している
  • 再送時に既存レコードを特定できる

社内DBだけで一意ならよいのか、複数拠点や外部サービスまで含めるのかを先に決めます。

IDを役割で分ける

一つのIDにすべての役割を持たせず、内部ID、業務キー、外部IDを分けます。

内部ID

自システムがレコードを識別するためのIDです。DBの主キーやAPIリソースIDに使います。

c7c62688-8d7d-43fe-8f8e-4f2b913f8ed2

内部IDには業務上の意味を埋め込まず、名称や分類が変わっても同じ値を維持します。このような人工的な識別子はサロゲートキーと呼ばれます。

自然キー・業務キー

業務データの中にもともと存在し、対象を一意にできる値です。部品番号、図面番号、社員番号などが候補になります。

CYL-040-200

人が検索しやすい一方、採番規則の変更や改番があり得ます。DBの主キーにすると参照先まで影響するため、内部IDとは分ける設計が安全です。

外部ID

連携元システムが持つ識別子です。外部IDだけでは範囲が分からないため、通常は連携元を示す値と組み合わせます。

{
  "source_system": "cad-master",
  "source_record_id": "A-004812"
}

別システムにもA-004812が存在し得るため、source_systemとsource_record_idの組み合わせを一意にします。

最小のデータ構造

設計部品を連携する例では、次のように役割を分離できます。

{
  "id": "c7c62688-8d7d-43fe-8f8e-4f2b913f8ed2",
  "asset_code": "CYL-040-200",
  "source": {
    "system": "cad-master",
    "record_id": "A-004812"
  },
  "name": "Hydraulic cylinder"
}
  • id:自システム内で不変の主識別子
  • asset_code:人が扱う業務コード
  • source.system:連携元の名前空間
  • source.record_id:連携元でのレコードID
  • name:変更可能な属性

名称や部品番号が変わっても、同一部品ならidは変えません。

入力から保存までの流れ

IDを使った連携処理は、次の順序に分けられます。

外部データ受信
  ↓
型・必須項目を検証
  ↓
ID表記を正規化
  ↓
source_system + source_record_idで既存検索
  ↓
存在しない → 内部IDを発行してINSERT
存在する   → 同じ内部IDのレコードをUPDATE
  ↓
内部IDを結果として返す

内部IDの発行前に外部IDを照合すれば、同じデータが再送されても同じレコードへ収束します。

SQLiteで一意性を制約にする

重複防止をアプリケーションのif文だけに任せず、DB側にも制約を置きます。

CREATE TABLE assets (
    id               TEXT PRIMARY KEY NOT NULL,
    asset_code       TEXT NOT NULL UNIQUE,
    source_system    TEXT NOT NULL,
    source_record_id TEXT NOT NULL,
    name             TEXT NOT NULL,
    created_at       TEXT NOT NULL,
    updated_at       TEXT NOT NULL,
    UNIQUE (source_system, source_record_id)
);

このテーブルでは、DBが次を保証します。

  • idは内部主キーとして重複不可
  • asset_codeは業務上重複不可
  • 外部IDは連携元との組み合わせで重複不可
  • 必須IDへNULLを保存できない

SQLiteには主キー列でのNULLに歴史的な例外があるため、文字列主キーにはNOT NULLも明示します。SQLite公式仕様でも各制約は別々に説明されています。

履歴として同じasset_codeを複数保持する場合は、履歴モデルに合う制約を設計します。

UUIDv4とUUIDv7の使い分け

RFC 9562はUUIDを128bitの識別子として定義しています。中央管理なしで生成でき、分散システムに向きます。

代表的な選択肢はUUIDv4とUUIDv7です。

UUIDv4

ランダムまたは疑似ランダムな値を基に生成します。広く利用されていますが、生成順には並びません。

UUIDv7

先頭側にUnix Epochのミリ秒時刻を持ち、DBインデックス上で近く並びやすいUUIDです。

ただし、UUIDv7には生成時刻に由来する情報が含まれます。IDからおおよその生成順や時刻を知られたくない用途では、公開範囲を含めて検討が必要です。

一般的なAPIではUUIDv4から始められます。大量挿入時のインデックス局所性が重要ならUUIDv7を候補にし、フィールドごとに方式を固定します。

Pythonで再実行可能な登録処理を作る

次の例では、外部IDの組み合わせが初回なら追加し、再送なら同じレコードを更新します。

import sqlite3
import uuid


def upsert_asset(conn: sqlite3.Connection, data: dict) -> str:
    new_id = str(uuid.uuid4())

    conn.execute(
        """
        INSERT INTO assets (
            id, asset_code, source_system, source_record_id,
            name, created_at, updated_at
        )
        VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
        ON CONFLICT (source_system, source_record_id)
        DO UPDATE SET
            asset_code = excluded.asset_code,
            name = excluded.name,
            updated_at = CURRENT_TIMESTAMP
        """,
        (
            new_id,
            data["asset_code"],
            data["source_system"],
            data["source_record_id"],
            data["name"],
        ),
    )

    row = conn.execute(
        """
        SELECT id
        FROM assets
        WHERE source_system = ? AND source_record_id = ?
        """,
        (data["source_system"], data["source_record_id"]),
    ).fetchone()

    conn.commit()
    return row[0]

SQLiteのUPSERTは、一意性制約との衝突時にDO UPDATEまたはDO NOTHINGへ切り替えます。外部IDのUNIQUE制約と組み合わせれば、再実行してもレコード数を増やしません。

IDのバリデーションと正規化

IDは文字列として受け取るだけでなく、境界で規則を検証します。

import uuid


def normalize_uuid(value: str) -> str:
    if not isinstance(value, str):
        raise TypeError("IDは文字列で指定してください")

    try:
        return str(uuid.UUID(value))
    except ValueError as exc:
        raise ValueError("UUIDの形式が不正です") from exc

入力を柔軟に受け付けるか、正規形だけを許可するかはAPI契約で決めます。

次の点も明文化します。

  • 大文字と小文字を同一視するか
  • 00123と123を同じIDとして扱うか
  • 数値型ではなく文字列型で渡すか
  • 空文字、null、フィールド欠落をどう扱うか

特に先頭ゼロを持つIDは数値へ変換してはいけません。ExcelやCSV取り込みでも文字列として保持します。

よくある失敗

名称をIDとして使う

名称は修正、翻訳、表記統一で変わります。識別子ではなく属性として扱います。

外部IDだけを保存する

連携元が増えると同じ値が衝突します。外部IDには必ず名前空間となるsource_systemを付けます。

APIへDB連番をそのまま公開する

連番は件数や登録順を推測させ、DB統合時には衝突します。内部連番と公開IDを分ける方法がありますが、UUIDでも認可は必要です。

IDへ分類や状態を埋め込む

PUMP-ACTIVE-001のようなIDは、分類変更や状態変更で矛盾します。変わり得る情報は別フィールドへ分離します。

重複判定を全フィールド一致にする

名称や更新日時が変わると別レコードと判定されます。同一性を決めるキーと、更新される属性を分けます。

セキュリティとID

IDは認証情報ではありません。UUIDでも、サーバー側で閲覧・更新権限を確認します。

また、ログや公開データでは次を確認します。

  • 外部の顧客番号や社員番号を不用意に公開しない
  • UUIDv7などに含まれる時系列情報を公開してよいか確認する
  • 廃棄後のIDを別対象へ再利用しない
  • ID変換表へのアクセスを制限する

再利用可能なID契約として残す

ID設計は文章だけでなく、機械可読な契約へできます。

field: id
role: internal_surrogate_key
type: string
format: uuid
version: 4
nullable: false
mutable: false
case: lowercase
issued_by: receiving_system

deduplication_key:
  fields:
    - source_system
    - source_record_id
  database_constraint: unique

この定義から、JSON Schema、DB制約、Python、CSV列定義、API仕様、テストへ展開できます。CAD、図面、計算条件、部品表の参照にも同じ考え方を使えます。

まとめ

安定したデータ連携には、「IDを付ける」だけでなく、各IDの役割を分ける設計が必要です。

  • 内部IDは不変のサロゲートキーにする
  • 業務コードは人が使う変更可能な属性として扱う
  • 外部IDは連携元との組み合わせで一意にする
  • DBのPRIMARY KEY、UNIQUE、NOT NULLで規則を強制する
  • 再送時は外部IDを照合してUPSERTする
  • UUIDv4とUUIDv7は生成環境、並び順、公開範囲で選ぶ
  • IDを認可の代わりにしない

同一性のルールをデータ契約として先に決めれば、API、DB、CSV、Python、CADデータの間で、重複や参照切れを抑えながら資産を再利用できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次