はじめに
新たに TypeORM を学ぼうかと思ったが、 Prisma の理解を深めることを選択したのである。
docker で環境構築をして Prisma の基礎的な動作を hands on(実践する)。DB は MySQL を利用する
前提条件
- DB がなにかを知っている
- docker engine をダウンロードできる
- Postman をダウンロードできる
導入メリット
Prisma は TypeScript と JavaScript のための強力な型安全なデータベースクライアントを提供します。
これにより、開発者はコンパイル時にエラーを検出でき、デバッグ時間を節約し、ランタイムエラーを減らすことができます。
vs typeORM
TypeORMではリストやレコードをフィルタリングするために主にSQL演算子を使用している
また、TypeORMでは多くのケースでフィルタクエリの型安全性を失っている。
一方で、Prismaは直感的に使用できる演算子(contains, startsWith, endsWithなど)を提供している
docker 作成
ディレクトリ構成
prisma_test
├── docker-compose.yml
└── api-server
└── app
db_data
はコンテナ起動時に生成させる
Dockerネットワーク
docker network create prisma-test
docker network list
docker inspect prisma-test
docker-compose
services:
db:
image: mysql:9
container_name: prisma-test-db-server
volumes:
- ./db_data:/var/lib/mysql
environment: # 環境変数の設定
- MYSQL_ROOT_PASSWORD=password
networks:
prisma-test:
aliases:
- db-container # db-serverのnetwork内での別名
stdin_open: true
tty: true # コンテナを自動停止させない
api:
image: node:alpine3.19
volumes:
- ./api-server/app:/usr/src/app # bind mount
container_name: prisma-test-api-server
networks:
- prisma-test # Dockerネットワークに所属させる
stdin_open: true
tty: true
working_dir: /usr/src/app
depends_on: # 依存関係
- db
ports:
- 3000:3000
networks:
prisma-test:
external: true # 外部で事前に定義されているネットワークを利用する
volumes:
db_data:
Docker で コンポーネント を起動
docker compose up -d
DBコンテナ内初期設定
DB の作成
コンテナ初回起動時に自動的に実行される /docker-entrypoint-initdb.d/init.sql
を利用すべきだが、CREATE DATABASE
だけなので直接実行する。
docker exec -it prisma-test-db-server bash
mysql -u root -p
password
show databases;
CREATE DATABASE sample_db;
show databases;
exit # mysql から脱出
exit # コンテナから脱出
※後述の prisma db push
コマンドだとDATABASEは自動的に作成される
MySQL database sample_db created at db-container:3306
APIコンテナ内初期設定
prisma をインストール
npm install express
npm install prisma --save-dev
npx prisma init
.env
編集
prisma が DB コンテナに接続できるようにする
DATABASE_URL="mysql://root:password@db-container:3306/sample_db"
schema.prisma
編集
DB 設計をする
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ユーザーテーブル
model Users {
id Int @id @default(autoincrement())
name String
email String @unique
posts Posts[] // 一対多リレーションを定義
}
// 投稿テーブル
model Posts {
id Int @id @default(autoincrement())
title String
content String
author_id Int
author Users @relation(fields: [author_id], references: [id]) // リレーションを定義
// Timestamps
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
マイグレーション
npx prisma migrate dev --name my_migrate_init
API の作成
/prisma_test/api-server/app
または /usr/src/app
(マウントしているので同じ)に app.js
を作成
今回は1つのファイルにすべて記述する
API の基礎部分
'use strict';
const express = require('express');
const app = express();
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// JSON ボディのパース
app.use(express.json());
// index
app.get('/', function(req, res){
res.send('Hello Express!');
});
// ここに後述の endpoint を追記
// NotFound
app.use(function(req,res){
res.send("NOT FOUND !")
});
// サーバーの起動
app.listen(3000);
console.log(`サーバが起動しました: http://localhost:3000`);
API の起動
node app
コンポーネント起動時に API も起動するように docker-compose.yml
に追記
app.js
を作成する前に node app
を実行すると、「そんなファイルないよ」エラーになる
api:
command: >
sh -c 'npm install &&
node app'
db 接続する endpoint の作成
CRUD の C
// ユーザーの登録(1件)
app.post('/users', async(req,res)=>{
// body 取得
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
// INSERT 処理
try {
const newUser = await prisma.users.create({
data: { name, email, },
});
return res.status(201).json(newUser);
} catch (error) {
console.error('Error adding user:', error);
// Prisma がクライアントに返すエラー
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// ユニーク制約違反
if (error.code === 'P2002') {
console.log('There is a unique constraint violation, a new user cannot be created with this email');
return res.status(400).json({ error: 'A user with this email already exists' });
}
}
return res.status(500).json({ error: 'Internal server error' });
}
});
// 投稿の登録(1件)
app.post('/posts', async(req,res)=>{
// body 取得
const { title, content, author_id } = req.body;
if (!title || !content || !author_id) {
return res.status(400).json({ error: 'Title, content, and author_id are required' });
}
// INSERT 処理
try {
const newPost = await prisma.posts.create({
data: { title, content, author_id, },
});
return res.status(201).json(newPost);
} catch (error) {
console.error('Error adding post:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
CRUD の R
// ユーザー一覧の取得
app.get('/users', async (req, res) => {
try {
const users = await prisma.users.findMany();
return res.json(users);
} catch (error) {
console.error('Error fetching users:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
// ユーザーと投稿一覧の取得
app.get('/users/posts', async(req,res)=>{
try {
const users = await prisma.users.findMany({
include: {
posts: true, // 関連する posts を含める
},
});
return res.json(users);
} catch (error) {
console.error('Error fetching users with posts:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
CRUD の U と D
// 投稿の更新
app.put('/posts', async(req,res)=>{
const { id, title, content } = req.body;
if (id === undefined || (title === undefined && content === undefined)) {
// `!title` を使用すると、falsy な値(undefined、null、0、空文字列 "" など)の場合に true になります。
return res.status(400).json({ error: 'ID, and either title or content are required' });
}
try {
// データの更新
const updatedPost = await prisma.posts.update({
where: { id }, // 更新するレコードを特定する条件
data: { title, content }, // 更新するデータ
});
// 更新結果をクライアントに返す
return res.status(200).json(updatedPost);
} catch (error) {
console.error('Error updating post:', error);
if (error.code === 'P2025') {
// Prisma のエラーコード P2025 は "Record to update not found" を示す
return res.status(404).json({ error: 'Post not found' });
}
return res.status(500).json({ error: 'Internal server error' });
}
});
// 投稿の削除
app.delete('/posts', async(req,res)=>{
const { id } = req.body;
// id が指定されていない場合のエラーハンドリング
if (id === undefined) {
return res.status(400).json({ error: 'ID is required' });
}
try {
// レコードの削除
const deletedPost = await prisma.posts.delete({
where: { id }, // 削除するレコードを特定する条件
});
// 成功レスポンスを返す
return res.status(200).json(deletedPost);
} catch (error) {
console.error('Error deleting post:', error);
if (error.code === 'P2025') {
// Prisma のエラーコード P2025 は "Record to delete not found" を示す
return res.status(404).json({ error: 'Post not found' });
}
return res.status(500).json({ error: 'Internal server error' });
}
});
postman で確認
postman の使用手順
- 通信method(GET, POST, PUT, DELESE)を選択。
- リクエストの下部にある「Body」タブをクリックします。
- 「raw」を選択し、右側のドロップダウンから「JSON」を選択します。
- JSON形式でリクエストボディにデータを入力します。
テストデータ
ベースURL: http://localhost:3000
機能 | endpoint | method | sample data |
---|---|---|---|
学生一覧の取得 |
|
GET | - |
学生と成績一覧の取得 |
|
GET | - |
ユーザーの登録(1件) |
|
POST |
|
投稿の登録(1件) |
|
POST |
|
投稿の更新 |
|
PUT |
|
投稿の削除 |
|
DELETE |
|
※「投稿の登録(1件)」で関連する author_id
がないとき、Internal server error
になるが、 DB の autoincrement は増加している
DateTime 型を使用する場合の罠
Prisma の DateTime 型は ISO 8601 形式の文字列を期待する
基本形式: "YYYY-MM-DDTHH:MM:SSZ"
例: "2012-05-11T14:42:00Z"
タイムゾーン指定: タイムゾーンを指定する場合は、次のようになります:
オフセット形式: "YYYY-MM-DDTHH:MM:SS+HH:MM"
例: "2012-05-11T14:42:00+09:00"
Prisma Studio を利用する
docker-compose.yml
追記
ports:
- 5555:5555 # prisma studio 用
起動: api サーバー内で以下のコマンドを実行
npx prisma studio
ブラウザで確認
http://localhost:5555
docker-compose.yml でマイグレーションとSeeding
Seeding: マイグレーションの実行時にテーブルに初期データを挿入できる
docker-compose.yml
追記
db:
healthcheck: # 自身のヘルスチェックを行うコマンド
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
interval: 10s
timeout: 5s
retries: 3
api:
depends_on:
db:
condition: service_healthy # 依存先コンテナのヘルスチェックが完了するまで待機
command: >
sh -c 'npm install &&
npx prisma db push &&
node prisma/seed.js &&
node app'
prisma_test/api-server/app/prisma/seed.js
作成
(長いので折り畳み)
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
await prisma.users.createMany({
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
{ name: "Charlie", email: "charlie@example.com" },
{ name: "Diana", email: "diana@example.com" },
{ name: "Eve", email: "eve@example.com" },
{ name: "Frank", email: "frank@example.com" },
{ name: "Grace", email: "grace@example.com" },
{ name: "Hank", email: "hank@example.com" },
{ name: "Ivy", email: "ivy@example.com" },
{ name: "Jack", email: "jack@example.com" },
{ name: "Kara", email: "kara@example.com" },
{ name: "Leo", email: "leo@example.com" },
{ name: "Mona", email: "mona@example.com" },
{ name: "Nina", email: "nina@example.com" },
{ name: "Owen", email: "owen@example.com" },
{ name: "Paul", email: "paul@example.com" },
{ name: "Quinn", email: "quinn@example.com" },
{ name: "Rita", email: "rita@example.com" },
{ name: "Sam", email: "sam@example.com" },
{ name: "Tina", email: "tina@example.com" },
{ name: "Uma", email: "uma@example.com" },
{ name: "Vera", email: "vera@example.com" },
{ name: "Will", email: "will@example.com" },
{ name: "Xena", email: "xena@example.com" },
{ name: "Yara", email: "yara@example.com" },
{ name: "Zane", email: "zane@example.com" },
],
skipDuplicates: true, // 重複をスキップするオプション。主キーが重複した場合にエラーを防ぐ
});
await prisma.posts.createMany({
data: [
{
"title": "Exploring Prisma",
"content": "Detailed content about exploring Prisma features.",
"author_id": 2
},
{
"title": "Introduction to GraphQL",
"content": "An introduction to GraphQL and its benefits.",
"author_id": 2
},
{
"title": "Understanding JWT",
"content": "A deep dive into JWT and its usage.",
"author_id": 3
},
{
"title": "REST vs GraphQL",
"content": "Comparing REST APIs with GraphQL.",
"author_id": 3
},
{
"title": "The Rise of TypeScript",
"content": "Why TypeScript is becoming popular in modern development.",
"author_id": 1
},
{
"title": "Testing in Node.js",
"content": "Best practices for testing Node.js applications.",
"author_id": 2
},
{
"title": "Performance Optimization",
"content": "Techniques to optimize the performance of your applications.",
"author_id": 3
},
{
"title": "Async/Await in JavaScript",
"content": "Understanding async/await syntax and how to use it effectively.",
"author_id": 1
}
],
});
console.log('Seeding completed');
}
main()
.catch(e => {
throw e;
})
.finally(async () => {
await prisma.$disconnect();
});
※Docker container 再起動時に毎回データが追加されるので、 データベースに既存のデータがあるかどうかを確認してからシーディングを行う。seed.js
を空にしたり db_data
を初期化したりで、対応するべきです。
seed.js
追記
async function main() {
// ユーザーが存在するか確認
const userCount = await prisma.users.count();
if (userCount === 0) {
// ユーザーが存在しない場合のみデータを追加
await prisma.users.createMany({ ... });
console.log('Seeding completed');
} else {
console.log('Seed data already present');
}
}
reference
おわりに
他の Prisma 記事です