Prismaのテーブル作成

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

選ばれたのはPrismaでした。

## Prismaとは？
### 特徴
[直感的なデータ モデル、自動移行、型安全性、自動補完を備えた Node.jsおよびTypeScript ORM です。](https://www.prisma.io/docs/orm)

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

#### ORMを使用しない場合（生のSQLを書く）
以下はRailsでORM（[ActiveRecord](https://railsguides.jp/active_record_basics.html)）を使用しない場合です。

（例）25歳以上のユーザーを取得し、名前順にソートする
```ruby
sql = "SELECT * FROM users WHERE age >= 25 ORDER BY name"
users = ActiveRecord::Base.connection.execute(sql)
```

#### ORMを使用する場合
生のSQLを書くよりもシンプルに記述できます。
私はこちらの方が馴染みあります。それだけORMの恩恵を受けているということですね。
```ruby
users = User.where("age >= ?", 25).order(:name)
```

### Prismaのメリット
- **型安全性とTypeScript連携**
  データベースクエリやモデルがすべて型安全に扱えるのが最大のメリットです。
  IDEによる補完機能や静的解析によるエラー防止が可能になります
:::note warn
Ruby（Active Record）やPHP（Eloquent）などのORMは動的型付け言語のため、型安全性は保証されません。
:::

- **シンプルで直感的なAPI**
SQLの知識が少ない人でも、[Prisma Client](https://www.prisma.io/docs/orm/prisma-client)を通じてオブジェクト指向のインターフェースでデータ操作ができるため、学習コストが低く、直感的に扱えます。

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

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

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

## prismaの導入
[Prismaの公式ドキュメント](https://www.prisma.io/docs/orm/tools/prisma-cli#installation)に沿ってインストールしていきます。

:::note warn
 開発依存関係として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のwiki](https://docs.github.com/ja/communities/documenting-your-project-with-wikis/adding-or-editing-wiki-pages)で[Mermeid記法](https://zenn.dev/kento_mm_ninw/articles/8b10afdbef306a)を使って簡単に作りました。

![スクリーンショット 2024-09-08 7.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677000/e90e2262-0e71-abb5-2ecb-c556c2d01a7f.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"`](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/generating-prisma-client): Prismaクライアントが生成され、アプリケーションとデータベースが連携できます
- [`datasource`](https://www.prisma.io/docs/orm/prisma-schema/overview/data-sources): データベースに接続する方法を定義します
- [`@default(autoincrement())`](https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-an-id-field): 数値の主キーを自動インクリメントしています
- [`@map`](https://www.prisma.io/docs/orm/prisma-schema/data-model/database-mapping#map-field--column-names): Prismaのモデルとデータベースのカラム名とのマッピングを行います。スキーマ内でのフィールド名と、実際のデータベースのカラム名を異なる名前にすることができます
- [`assignedAt`](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#explicit-many-to-many-relations): ユーザーがセミナーに割り当てられた日時
- [`assignedBy`](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#explicit-many-to-many-relations): ユーザーをセミナーに割り当てた担当者を表す文字列
- [`@@id([userId, seminarId])`](https://www.prisma.io/docs/orm/reference/prisma-schema-reference#id-1): 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](https://www.npmjs.com/package/bcryptjs)というライブラリを導入しています。
DBをseedする方法も[公式ドキュメント](https://www.prisma.io/docs/orm/prisma-migrate/workflows/seeding#how-to-seed-your-database-in-prisma-orm)に記載されています。

```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`](https://www.prisma.io/docs/orm/reference/prisma-client-reference#upsert): 条件を満たすレコードが存在する場合は更新し、存在しない場合はレコードを作成します
- `where`: 指定された条件に一致するユーザーを検索します
- [`prisma.$disconnect`](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections/connection-management#disconnect): クエリのプロセスを終了し、DB接続を切断します
- [`process.exit(1);`](https://nodejs.org/api/process.html#processexitcode): エラーステータスコード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](https://typestrong.org/ts-node/docs/)とは、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"]
}
```
### 補足
- [`"esm": true`](https://typestrong.org/ts-node/docs/imports/#native-ecmascript-modules): [ECMAScriptモジュール（ESM）](https://jsprimer.net/basic/module/)を有効にします。この設定により、importやexport構文を使用してモジュールを扱うことができます
- [`"experimentalSpecifierResolution": "node"`](https://typestrong.org/ts-node/docs/options/#experimentalspecifierresolution): Node.jsのモジュール解決ルールに従って、ファイル拡張子やパスの解決方法を制御します。`.js`や`.ts`などのファイル拡張子を省略することが可能になります

## シード実行結果
```
% 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](http://localhost:5555/)を開く
1. [seed.tsファイルに定義されたユーザーデータ](#シードデータの作成)のレコードが作成されていることを確認する

![スクリーンショット 2024-09-01 9.57.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677000/bc2810ab-667b-c87f-3206-df6b961bf227.png)

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

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

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


## 参考記事
- [prisma.io（ORM）](https://www.prisma.io/docs/orm)
- [TypeScriptとエコシステム](https://typescriptbook.jp/overview/ecosystem)
- [ts-node（公式）](https://typestrong.org/ts-node/docs/)