0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Prisma 初学者の手引書 (dockerでAPI作成)

Last updated at Posted at 2024-09-02

はじめに

新たに 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

docker-compose.yml
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 コンテナに接続できるようにする

.env
DATABASE_URL="mysql://root:password@db-container:3306/sample_db"

schema.prisma 編集

DB 設計をする

schema.prisma
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 の基礎部分

app.js
'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 を実行すると、「そんなファイルないよ」エラーになる

docker-compose.yml
api:
  command: >
    sh -c 'npm install &&
    node app'

db 接続する endpoint の作成

CRUD の C

app.js
// ユーザーの登録(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.js
// ユーザー一覧の取得
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.js
// 投稿の更新
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 の使用手順

  1. 通信method(GET, POST, PUT, DELESE)を選択。
  2. リクエストの下部にある「Body」タブをクリックします。
  3. 「raw」を選択し、右側のドロップダウンから「JSON」を選択します。
  4. JSON形式でリクエストボディにデータを入力します。

テストデータ

ベースURL: http://localhost:3000

機能 endpoint method sample data
学生一覧の取得

/students

GET -
学生と成績一覧の取得

/students/score

GET -
ユーザーの登録(1件)

/students

POST
{
  "name": "John Doe",
  "email": "john.doe@example.com"
}
投稿の登録(1件)

/test_results

POST
{
  "title": "My First Post",
  "content": "My first content.",
  "author_id": 1
}
投稿の更新

/test_results

PUT
{
  "id": 1,
  "title": "Second Post",
}
投稿の削除

/test_results

DELETE
{
  "id": 6
}

※「投稿の登録(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 追記

docker-compose.yml
ports:
  - 5555:5555          # prisma studio 用

起動: api サーバー内で以下のコマンドを実行

npx prisma studio

ブラウザで確認
http://localhost:5555

docker-compose.yml でマイグレーションとSeeding

Seeding: マイグレーションの実行時にテーブルに初期データを挿入できる

docker-compose.yml 追記

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 作成

(長いので折り畳み)
/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 追記

/prisma/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 記事です

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?