30
11

Prismaのテーブル作成

Posted at

はじめに

普段バックエンドの言語はRuby(Rails)を使用しているのですが、そちらを使わずにテーブルを作成する方法ってあるのか?
わたし、気になります!

選ばれたのはPrismaでした。

Prismaとは?

特徴

直感的なデータ モデル、自動移行、型安全性、自動補完を備えた Node.jsおよびTypeScript ORM です。

ORMとは?

オブジェクト関係マッピング(Object-Relational Mapping)の略で、データベースとオブジェクト指向プログラミング言語を結びつけるための技術です。
ORMを使用すると、SQLを直接記述せずにプログラミング言語でデータベースを操作するコードを記述することができます。

ORMを使用しない場合(生のSQLを書く)

以下はRailsでORM(ActiveRecord)を使用しない場合です。

(例)25歳以上のユーザーを取得し、名前順にソートする

sql = "SELECT * FROM users WHERE age >= 25 ORDER BY name"
users = ActiveRecord::Base.connection.execute(sql)

ORMを使用する場合

生のSQLを書くよりもシンプルに記述できます。
私はこちらの方が馴染みあります。それだけORMの恩恵を受けているということですね。

users = User.where("age >= ?", 25).order(:name)

Prismaのメリット

  • 型安全性とTypeScript連携
    データベースクエリやモデルがすべて型安全に扱えるのが最大のメリットです。
    IDEによる補完機能や静的解析によるエラー防止が可能になります

Ruby(Active Record)やPHP(Eloquent)などのORMは動的型付け言語のため、型安全性は保証されません。

  • シンプルで直感的なAPI
    SQLの知識が少ない人でも、Prisma Clientを通じてオブジェクト指向のインターフェースでデータ操作ができるため、学習コストが低く、直感的に扱えます。

  • データベース移行機能(マイグレーション)
    Prismaの設定ファイル(schema.prisma)にスキーマを定義し、マイグレーションを行うことで、データベースにテーブルを作成できます。

  • 複数のデータベースに対応
    MySQL、PostgreSQL、SQLite、SQL Server、MongoDBといった複数のデータベースに対応しています。

Prismaのデメリット

  • Node.jsに特化している
    JavaScript/TypeScriptのエコシステムに依存しているため、RubyやPHPなどのバックエンド言語に慣れている方は学習コストが発生します。
  • 厳密な型付けへの違和感
    RubyやPHPなどが型に関する厳密な制約はなく、柔軟性があることに対して、Prismaの型定義や型安全性が煩雑に感じる方もいるかもしれません。
  • トランザクション管理が複雑
    Prismaの$transaction()メソッドは、複数のデータベース操作を1つのトランザクションでまとめて実行するために使用されます。
await prisma.$transaction(async (prisma) => {
  await prisma.user.create({ data: { name: "Mike" } });
  await prisma.post.create({ data: { title: "First post", userId: 1 } });
});

これが数十や数百のクエリになるとトランザクションの全体管理が難しくなり、コードが冗長化する可能性もあるかもしれないですね。

prismaの導入

Prismaの公式ドキュメントに沿ってインストールしていきます。

開発依存関係としてprismaをインストールしたら、コマンドの前にパッケージランナーを付ける必要があります(例: npx prisma

prismaのインストール
Prisma CLIを開発依存関係(devDependencies)としてプロジェクトに追加します。

% npm install prisma --save-dev

added 6 packages in 2s

prismaのバージョン確認
以下は例ですが、prisma@prisma/clientに表示されれば成功です。

% npx prisma --version
Environment variables loaded from .env
prisma                  : 5.18.0
@prisma/client          : 5.18.0
Computed binaryTarget   : darwin-arm64
Operating System        : darwin
Architecture            : arm64
Node.js                 : v17.0.0
...(以下省略)

Prismaプロジェクトの初期化
schema.prismaファイルが作成され、Prismaの設定がプロジェクトに追加されます。

% prisma init          

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

開発環境

言語・FWなど バージョン
prisma 5.18.0
@prisma/client 5.18.0
bcryptjs 2.4.3
@types/bcryptjs 5.18.0

テーブル定義

テーブル一覧

論理テーブル名 物理テーブル名 備考
ユーザー情報 users
セミナー情報 seminars
ユーザーの参加セミナー user_seminars 中間テーブル

usersテーブルとseminarsテーブルは多対多にしています。
つまり、1人のユーザーが複数のセミナーに参加でき、1つのセミナーにも複数のユーザーが参加できる構造になります。

usersテーブル

カラム情報

論理名 物理名 データ型 Not Null デフォルト値 備考
ID id int
名前 name string
メールアドレス email string
パスワード password string
役割 role boolean 参加者か主催者かをenumで定義する(デフォルト値は参加者)
作成日時 created_at datetime
作成日時 updated_at datetime

インデックス情報

インデックス情報 カラム 主キー ユニーク
PRIMARY KEY id
UNIQUE email

seminarsテーブル

カラム情報

論理名 物理名 データ型 Not Null デフォルト値 備考
ID id int
テーマ theme string
開催日 seminar_day datetime
作成日時 created_at datetime
作成日時 updated_at datetime

インデックス情報

インデックス情報 カラム 主キー ユニーク
PRIMARY KEY id

user_seminarsテーブル

カラム情報

論理名 物理名 データ型 Not Null デフォルト値 備考
ユーザーID user_id int
セミナーID seminar_id int

インデックス情報

インデックス情報 カラム 主キー ユニーク
PRIMARY KEY user_id
PRIMARY KEY seminar_id

ER図

githubのwikiMermeid記法を使って簡単に作りました。

スクリーンショット 2024-09-08 7.48.26.png

Prismaスキーマ

スキーマ定義

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id          Int      @id @default(autoincrement())
  name        String?
  email       String   @unique
  password    String
  role        Role     @default(PARTICIPANT)
  createdAt   DateTime @default(now()) @map(name: "created_at")
  updatedAt   DateTime @updatedAt @map(name: "updated_at")
  seminars    SeminarsOnUsers[]
}

model Seminar {
  id          Int      @id @default(autoincrement())
  theme       String
  seminar_day DateTime
  createdAt   DateTime @default(now()) @map(name: "created_at")
  updatedAt   DateTime @updatedAt @map(name: "updated_at")
  users SeminarsOnUsers[]
}

model SeminarsOnUsers {
  user       User    @relation(fields: [userId], references: [id])
  userId     Int
  seminar   Seminar @relation(fields: [seminarId], references: [id])
  seminarId Int
  assignedAt DateTime @default(now())
  assignedBy String

  @@id([userId, seminarId])
}

enum Role {
  PARTICIPANT
  ORGANIZER
}

補足

  • provider = "prisma-client-js": Prismaクライアントが生成され、アプリケーションとデータベースが連携できます
  • datasource: データベースに接続する方法を定義します
  • @default(autoincrement()): 数値の主キーを自動インクリメントしています
  • @map: Prismaのモデルとデータベースのカラム名とのマッピングを行います。スキーマ内でのフィールド名と、実際のデータベースのカラム名を異なる名前にすることができます
  • assignedAt: ユーザーがセミナーに割り当てられた日時
  • assignedBy: ユーザーをセミナーに割り当てた担当者を表す文字列
  • @@id([userId, seminarId]): userId と seminarId を組み合わせた複合主キーです。これにより、同じユーザーとセミナーのペアが重複しないようにしています

マイグレーション

  • prisma migrate dev: マイグレーションを実行します。devを付与して開発環境でのスキーマの変更をDBに適用します
  • --nameオプションでマイグレーションの名前を指定します。今回はinitという名前をつけます
% prisma migrate dev --name init                   
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "localhost:5432"

PostgreSQL database mydb created at localhost:5432

Applying migration `20240824224137_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20240824224137_init/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (v5.18.0) to ./node_modules/@prisma/client in 85ms

生成されたマイグレーションファイル

prisma/migrations/20240824224137_init/migration.sql
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('PARTICIPANT', 'ORGANIZER');

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "name" TEXT,
    "email" TEXT NOT NULL,
    "password" TEXT NOT NULL,
    "role" "Role" NOT NULL DEFAULT 'PARTICIPANT',
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Seminar" (
    "id" SERIAL NOT NULL,
    "theme" TEXT NOT NULL,
    "seminar_day" TIMESTAMP(3) NOT NULL,
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Seminar_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "SeminarsOnUsers" (
    "userId" INTEGER NOT NULL,
    "seminarId" INTEGER NOT NULL,
    "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "assignedBy" TEXT NOT NULL,

    CONSTRAINT "SeminarsOnUsers_pkey" PRIMARY KEY ("userId","seminarId")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- AddForeignKey
ALTER TABLE "SeminarsOnUsers" ADD CONSTRAINT "SeminarsOnUsers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SeminarsOnUsers" ADD CONSTRAINT "SeminarsOnUsers_seminarId_fkey" FOREIGN KEY ("seminarId") REFERENCES "Seminar"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

シードデータの作成

今回はseedを実行してテーブルを作成したいと思います。
なお、パスワードをハッシュ化するためにbcryptというライブラリを導入しています。
DBをseedする方法も公式ドキュメントに記載されています。

prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import bcrypt from "bcryptjs";

const prisma = new PrismaClient();
const password = "password";
const saltRounds = 10;
const salt = bcrypt.genSaltSync(saltRounds);
const hashedPassword = bcrypt.hashSync(password, salt);
async function main() {
  await prisma.user.upsert({
    where: { email: 'test@example.com' },
    update: {},
    create: {
      name: 'test',
      email: 'test@example.com',
      password: hashedPassword,
      role: 'PARTICIPANT'
    }
  })
}

main()
.then(async () => {
  await prisma.$disconnect();
})
.catch(async (e) => {
  console.error(e);
  await prisma.$disconnect();
  process.exit(1);
});

補足

  • prisma.user.upsert: 条件を満たすレコードが存在する場合は更新し、存在しない場合はレコードを作成します
  • where: 指定された条件に一致するユーザーを検索します
  • prisma.$disconnect: クエリのプロセスを終了し、DB接続を切断します
  • process.exit(1);: エラーステータスコード1でNode.jsのプロセスを終了します

パッケージ

package.json
{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@prisma/client": "^5.18.0",
    "@types/bcryptjs": "^2.4.6",
    "bcryptjs": "^2.4.3",
  },
  "devDependencies": {
    "@types/node": "^20.16.2",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "prisma": "^5.18.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.5.4"
  },
  "type": "module",
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}

補足

  • "prisma": { "seed": "ts-node prisma/seed.ts" }: ts-nodeを使用して、Prismaのシードスクリプトを実行するための設定です
  • ts-nodeとは、TypeScriptファイルを直接実行できるツールです。従来のようにコンパイルしなくてもTypeScriptコードを実行できます

TypeScriptの設定ファイル

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    // その他設定
  },
  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

補足

シード実行結果

% npx prisma db seed  
Environment variables loaded from .env
Running seed command `ts-node prisma/seed.ts` ...

🌱  The seed command has been executed.

動作確認

  1. npx prisma studioをターミナルで実行し、ブラウザでlocalhost:5555を開く
  2. seed.tsファイルに定義されたユーザーデータのレコードが作成されていることを確認する

スクリーンショット 2024-09-01 9.57.50.png

まとめ

Prismaは、データベースのスキーマ定義、マイグレーション、データベース操作をNode.jsおよびTypeScriptのORMとして行います。

型安全なクエリを作成することができるのが魅力ですが、Prisma独自の概念(schema.prismaファイルなど)に慣れるのに時間がかかりそうです...

TypeScriptまたはJavaScriptに特化して開発するプロジェクトでは向いてそうです。
プロジェクトの要件や開発環境に応じて、最適なORMを選択するのが大切だと思いました。

参考記事

30
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
11