SQLiteでユニーク制約を理解する:プログラミング解説とサンプルコード


ユニーク制約の構文

ユニーク制約は、CREATE TABLE ステートメントまたは ALTER TABLE ステートメントを使用して定義できます。

CREATE TABLE table_name (
  column_name data_type UNIQUE,
  ...
);

ALTER TABLE table_name
ADD UNIQUE constraint_name (column_name);

次の例は、customers という名前の表に email 列を追加し、その列にユニーク制約を設定する方法を示しています。

CREATE TABLE customers (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL
);

この制約により、customers 表内のすべての email アドレスが互いに異なることが保証されます。つまり、同じ email アドレスを持つ 2 人の顧客を登録することはできません。

ユニーク制約の利点

ユニーク制約には、次のような利点があります。

  • データの参照を容易にすることができます。
  • 重複データの入力を防ぐことができます。
  • データの整合性を保つことができます。

ユニーク制約の注意点

ユニーク制約を使用する際には、次の点に注意する必要があります。

  • 複数の列でユニーク制約を定義することはできますが、その列の組み合わせがすべて異なる必要があります。
  • 既存のデータにユニーク制約を追加する場合、その制約を満たすデータのみが残されます。
  • ユニーク制約を設定する列には、NULL 値を格納することはできません。


テーブルの作成とユニーク制約の定義

CREATE TABLE customers (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL
);

このコードは、customers という名前の表を作成し、その表に 3 つの列を定義します。

  • email 列は、NULL 値を許さず、テキストを格納し、かつユニーク制約が設定されています。
  • name 列は、NULL 値を許さず、テキストを格納します。
  • id 列は、主キーであり、自動的にインクリメントされる整型数です。

このユニーク制約により、customers 表内のすべての email アドレスが互いに異なることが保証されます。

ユニーク制約の違反

次のコードは、email 列に重複する値を挿入しようと試みるため、ユニーク制約の違反が発生します。

INSERT INTO customers (name, email)
VALUES ('John Doe', '[email protected]');

INSERT INTO customers (name, email)
VALUES ('Jane Doe', '[email protected]');

このコードを実行すると、次のエラーが発生します。

UNIQUE constraint failed: customers.email

既存のテーブルへのユニーク制約の追加

次のコードは、既存のテーブルにユニーク制約を追加する方法を示しています。

ALTER TABLE customers
ADD UNIQUE constraint unique_email (email);

このコードは、customers 表の email 列にユニーク制約を追加します。既存のデータにこの制約が適用されることに注意してください。つまり、email 列に重複する値を持つ既存のレコードは削除されます。

複数の列でユニーク制約を定義する

次のコードは、複数の列でユニーク制約を定義する方法を示しています。

CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY AUTOINCREMENT,
  customer_id INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  UNIQUE (customer_id, product_id)
);

このコードは、orders という名前の表を作成し、その表に 4 つの列を定義します。

  • この表には、customer_id 列と product_id 列の組み合わせがすべて異なる必要があるというユニーク制約が設定されています。
  • product_id 列は、NULL 値を許さず、整型数を格納します。
  • customer_id 列は、NULL 値を許さず、整型数を格納します。
  • order_id 列は、主キーであり、自動的にインクリメントされる整型数です。

CHECK 制約を使用したユニーク制約の実装

次のコードは、CHECK 制約を使用してユニーク制約を実装する方法を示しています。

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL UNIQUE,
  CHECK (username NOT IN (SELECT username FROM banned_users))
);
  • username 列の値が banned_users 表の username 列に存在しないことを保証する CHECK 制約が設定されています。
  • username 列は、NULL 値を許さず、テキストを格納し、かつユニーク制約が設定されています。
  • id 列は、主キーであり、自動的にインクリメントされる整型数です。


チェック制約

CHECK 制約を使用して、列の値が特定の条件を満たしていることを確認できます。この制約を使用して、ユニーク制約をシミュレートできます。

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL,
  CHECK (username NOT IN (SELECT username FROM banned_users))
);

この例では、username 列の値が banned_users 表の username 列に存在しないことを保証する CHECK 制約が設定されています。

部分インデックス

部分インデックスを使用して、列の一部のみを対象としたユニーク制約を定義できます。

CREATE TABLE products (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  UNIQUE (category, SUBSTR(name, 1, 10))
);

この例では、category 列と name 列の最初の 10 文字の組み合わせがすべて異なる必要があるというユニーク制約が設定されています。

トリガー

トリガーを使用して、データ挿入または更新操作の前に、ユニーク制約を検証するロジックを実装できます。

CREATE TRIGGER unique_email_check BEFORE INSERT OR UPDATE customers
FOR EACH ROW
BEGIN
  IF NEW.email IN (SELECT email FROM customers WHERE rowid != NEW.rowid) THEN
    RAISE ROLLBACK "Email must be unique";
  END IF;
END;

この例では、customers 表の email 列に挿入または更新される値が、既存のレコードの email 列値と重複していないことを確認するトリガーが作成されています。

外部キー制約

外部キー制約を使用して、別の表の主キー列を参照する列に制約を定義できます。これにより、参照整合性を保証し、重複データを防ぐことができます。

CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY AUTOINCREMENT,
  customer_id INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  FOREIGN KEY (customer_id) REFERENCES customers(id),
  FOREIGN KEY (product_id) REFERENCES products(id)
);

この例では、orders 表の customer_id 列が customers 表の id 列を参照し、product_id 列が products 表の id 列を参照するように外部キー制約が設定されています。

アプリケーションロジック

アプリケーションロジックを使用して、データの一意性を検証することもできます。これは、データベース制約に頼らずに、より柔軟な制御を提供する場合に役立ちます。

def create_user(username):
  if User.objects.filter(username=username).exists():
    raise ValueError("Username must be unique")

  user = User(username=username)
  user.save()

この例では、create_user 関数は、username がすでに存在する場合はエラーを発生させて、User モデル内に重複するユーザーが作成されないようにします。

適切な代替方法を選択する

どの代替方法が適切かは、要件と状況によって異なります。

  • 既存のテーブルとの関係を定義する必要がある場合
    外部キー制約が適切です。
  • より複雑な検証ロジックが必要な場合
    トリガーまたはアプリケーションロジックが適切です。
  • 部分的な一意制約が必要な場合
    部分インデックスが適切です。
  • シンプルかつ軽量な制約が必要な場合
    CHECK 制約が適切です。
  • 将来的に変更の可能性を考慮する必要があります。制約を変更することは難しい場合があるため、柔軟性を考慮した設計が重要です。
  • パフォーマンスとスケーラビリティを考慮する必要があります。複雑な制約は、データベースのパフォーマンスに悪影響を及ぼす可能性があります。
  • 上記の代替方法はそれぞれ長所と短所があります。選択する前に、各オプションを慎重に評価する必要があります。