ECサイトのSQLスキーマをPrismaに変換する

商品、注文、明細、カテゴリを含むマルチテーブルのECサイトSQLスキーマを、リレーションと制約を備えた完全なPrismaスキーマに変換する方法を学びます。

Real-World Schemas

詳細な説明

ECサイトスキーマの変換

ECサイトのデータベースには、商品、カテゴリ、顧客、注文、注文明細がさまざまなリレーションシップ型で含まれます。この例では、完全なマルチテーブルSQLスキーマをPrismaに変換することを示します。

SQLの例

CREATE TABLE categories (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL UNIQUE,
  slug VARCHAR(100) NOT NULL UNIQUE,
  parent_id INTEGER,
  FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
);

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(255) NOT NULL UNIQUE,
  description TEXT,
  price DECIMAL(10, 2) NOT NULL,
  compare_at_price DECIMAL(10, 2),
  sku VARCHAR(50) UNIQUE,
  stock_quantity INTEGER NOT NULL DEFAULT 0,
  is_active BOOLEAN NOT NULL DEFAULT true,
  metadata JSONB NOT NULL DEFAULT '{}',
  category_id INTEGER,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
  CONSTRAINT chk_price CHECK (price >= 0),
  CONSTRAINT chk_stock CHECK (stock_quantity >= 0)
);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  order_number VARCHAR(20) NOT NULL UNIQUE,
  customer_email VARCHAR(320) NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'pending',
  subtotal DECIMAL(10, 2) NOT NULL,
  tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
  shipping DECIMAL(10, 2) NOT NULL DEFAULT 0,
  total DECIMAL(10, 2) NOT NULL,
  notes TEXT,
  ordered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT chk_status CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled'))
);

CREATE TABLE order_items (
  id SERIAL PRIMARY KEY,
  order_id INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  product_name VARCHAR(255) NOT NULL,
  quantity INTEGER NOT NULL DEFAULT 1,
  unit_price DECIMAL(10, 2) NOT NULL,
  FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
  FOREIGN KEY (product_id) REFERENCES products(id),
  CONSTRAINT chk_quantity CHECK (quantity > 0)
);

CREATE INDEX idx_products_category_id ON products (category_id);
CREATE INDEX idx_products_is_active ON products (is_active);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_orders_ordered_at ON orders (ordered_at);
CREATE INDEX idx_order_items_order_id ON order_items (order_id);
CREATE INDEX idx_order_items_product_id ON order_items (product_id);

生成されるPrismaスキーマ

enum OrderStatus {
  pending
  processing
  shipped
  delivered
  cancelled
}

model Category {
  id       Int        @id @default(autoincrement())
  name     String     @unique @db.VarChar(100)
  slug     String     @unique @db.VarChar(100)
  parentId Int?       @map("parent_id")
  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
  children Category[] @relation("CategoryTree")
  products Product[]

  @@map("categories")
}

model Product {
  id             Int       @id @default(autoincrement())
  name           String
  slug           String    @unique
  description    String?   @db.Text
  price          Decimal   @db.Decimal(10, 2)
  compareAtPrice Decimal?  @map("compare_at_price") @db.Decimal(10, 2)
  sku            String?   @unique @db.VarChar(50)
  stockQuantity  Int       @default(0) @map("stock_quantity")
  isActive       Boolean   @default(true) @map("is_active")
  metadata       Json      @default("{}")
  categoryId     Int?      @map("category_id")
  category       Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
  createdAt      DateTime  @default(now()) @map("created_at") @db.Timestamptz()
  updatedAt      DateTime  @updatedAt @map("updated_at") @db.Timestamptz()
  orderItems     OrderItem[]

  @@index([categoryId], map: "idx_products_category_id")
  @@index([isActive], map: "idx_products_is_active")
  @@map("products")
}

model Order {
  id            Int         @id @default(autoincrement())
  orderNumber   String      @unique @map("order_number") @db.VarChar(20)
  customerEmail String      @map("customer_email") @db.VarChar(320)
  status        OrderStatus @default(pending)
  subtotal      Decimal     @db.Decimal(10, 2)
  tax           Decimal     @default(0) @db.Decimal(10, 2)
  shipping      Decimal     @default(0) @db.Decimal(10, 2)
  total         Decimal     @db.Decimal(10, 2)
  notes         String?     @db.Text
  orderedAt     DateTime    @default(now()) @map("ordered_at") @db.Timestamptz()
  items         OrderItem[]

  @@index([status], map: "idx_orders_status")
  @@index([orderedAt], map: "idx_orders_ordered_at")
  @@map("orders")
}

model OrderItem {
  id          Int     @id @default(autoincrement())
  orderId     Int     @map("order_id")
  productId   Int     @map("product_id")
  productName String  @map("product_name")
  quantity    Int     @default(1)
  unitPrice   Decimal @map("unit_price") @db.Decimal(10, 2)
  order       Order   @relation(fields: [orderId], references: [id], onDelete: Cascade)
  product     Product @relation(fields: [productId], references: [id])

  @@index([orderId], map: "idx_order_items_order_id")
  @@index([productId], map: "idx_order_items_product_id")
  @@map("order_items")
}

スキーマ設計のポイント

自己参照カテゴリCategory modelは階層的なカテゴリツリー(例:電子機器 > ノートパソコン > ゲーミングノートパソコン)のためにparentIdで自分自身を参照しています。

CHECKからEnumへstatusのCHECK制約はPrismaのenum OrderStatusに昇格し、アプリケーション全体でTypeScriptの型安全性を提供します。

非正規化されたproduct_nameOrderItemは購入時の商品名を保存しています。商品名は後で変更される可能性がありますが、注文履歴は正確に維持される必要があるためです。

金額にはDecimal — すべての金額フィールドは浮動小数点の丸め誤差を避けるため、明示的な精度を持つDecimalを使用しています。

JSONメタデータProduct.metadataフィールドはPrismaのJson型を使用し、色、寸法、仕様などの柔軟なスキーマレス属性を格納します。

クエリの例

// 注文と明細・商品詳細を取得
const order = await prisma.order.findUnique({
  where: { orderNumber: "ORD-2024-5001" },
  include: {
    items: {
      include: { product: true }
    }
  }
});

// カテゴリ内のアクティブな商品一覧
const products = await prisma.product.findMany({
  where: { categoryId: 5, isActive: true },
  orderBy: { createdAt: "desc" }
});

ユースケース

ECプラットフォームを生SQLからPrisma ORMに移行する場合に、コンバーターが商品カタログ、注文システム、カテゴリ階層を、すべてのリレーション、enum、インデックス、ビジネス制約を持つ完全に型付けされたPrismaスキーマに変換します。

試してみる — SQL to Prisma Schema

フルツールを開く