はじめに
普段バックエンドの言語は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 | ○ | ||
メールアドレス | string | ○ | |||
パスワード | password | string | ○ | ||
役割 | role | boolean | ○ | ○ | 参加者か主催者かをenumで定義する(デフォルト値は参加者) |
作成日時 | created_at | datetime | ○ | ○ | |
作成日時 | updated_at | datetime | ○ | ○ |
インデックス情報
インデックス情報 | カラム | 主キー | ユニーク |
---|---|---|---|
PRIMARY KEY | id | ○ | ○ |
UNIQUE | ○ |
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でMermeid記法を使って簡単に作りました。
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
生成されたマイグレーションファイル
-- 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する方法も公式ドキュメントに記載されています。
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のプロセスを終了します
パッケージ
{
"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の設定ファイル
{
"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
: ECMAScriptモジュール(ESM)を有効にします。この設定により、importやexport構文を使用してモジュールを扱うことができます -
"experimentalSpecifierResolution": "node"
: 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.
動作確認
-
npx prisma studio
をターミナルで実行し、ブラウザでlocalhost:5555を開く - seed.tsファイルに定義されたユーザーデータのレコードが作成されていることを確認する
まとめ
Prismaは、データベースのスキーマ定義、マイグレーション、データベース操作をNode.jsおよびTypeScriptのORMとして行います。
型安全なクエリを作成することができるのが魅力ですが、Prisma独自の概念(schema.prismaファイルなど)に慣れるのに時間がかかりそうです...
TypeScriptまたはJavaScriptに特化して開発するプロジェクトでは向いてそうです。
プロジェクトの要件や開発環境に応じて、最適なORMを選択するのが大切だと思いました。