技術系

Prismaを使ってみる

技術系

こんにちは、なかにしです。

今回はNode.jsのORM、Prismaをいじってみようと思います!
とりあえずCRUDだけ押さえてみます。

Prismaとは

オープンソースの ORMです。
公式は こちら

謳い文句は 次世代のNode.jsおよびTypeScript ORM
TSを使用して記述することができ、型安全にDB操作ができる、といった特徴があります。

対応DB一覧はこちら です。
メジャーなオープンソースDBはすべて押さえているイメージですね。

ORMの導入により、各DBの方言の違いの吸収やメンテナンスの容易さが向上し、
よりスピーディーでハイクオリティな開発ができるようになります。

事前準備

アプリケーションとDBを用意します。
今回の環境は Windows(Windows 11 Home)のWSL2です。

アプリは React×Express で作成します。
(Nodeインストール済の前提)

フロント側

ViteでReactテンプレートを使用します。

npx create vite

バック側

Expressを使用します。

package.jsonを作成して、

npm init

必要なライブラリをインストールします。

npm install typescript ts-node @types/node @types/express --save-dev

package.jsonに、ビルドとサーバー起動、そしてシーダー用のスクリプトを追加します。

  "scripts": {
    "build": "tsc -p tsconfig.prod.json",
    "start": "node dist/index.js",
    "seed": "ts-node prisma/seed.ts"
  },

TS使用の為、tsconfig.jsonを用意します。
(開発用と本番用で2種類)

{
  "compilerOptions": {
    "types": ["node"],
    "target": "es5",
    "module": "commonjs",
    "rootDir": ".",
    "outDir": "./dist",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*", "prisma/seed.ts"]
}
{
  "compilerOptions": {
    "types": ["node"],
    "target": "es5",
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["prisma/seed.ts"]
}

index.tsを用意します。

import express, { Request, Response } from 'express';

const app = express();
const port = 3001;

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
  res.send('Hello~');
})

app.listen(port, () => {
  console.log(`Server is Running: port ${port}`)
})

DB側

DBはDockerでPostgresSQLを立てます。

FROM postgres:latest
ENV POSTGRES_USER=nakanishi-db
ENV POSTGRES_PASSWORD=Nakanishi
ENV POSTGRES_DB=nakanishi-db
version: "3"
services:
  db:
    build: .
    ports:
      - "5432:5432"
    volumes:
      - /mnt/wsl/ubuntu/home/prisma-practice:/var/lib/postgresql/data

dockerでコンテナを立て、準備完了です。

docker compose up -d

Prismaのインストールと初期設定

バック側でPrismaをインストールします。

npm install @prisma/client

初期設定をします。

npx prisma init

Prismaディレクトリと.envが自動生成されました。

DB接続

PostgresSQLと繋いでいきます。

prisma/schema.prismaにDB接続情報を書きます。
PostgresSQLであれば、テンプレートのままでOKです。

generator client {
  provider = "prisma-client-js"
}

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

続いて、.envの環境変数を修正していきます。
文法としては以下のように書きます。

DATABASE_URL="postgresql://ユーザー名:パスワード@ホスト:ポート/データベース名"

上記の文法に則り、今回は以下にしました。

DATABASE_URL="postgresql://nakanishi-db:Nakanishi@127.0.0.1:5432/nakanishi-db"

Model定義

モデルを定義していきます。

まずは定石、Userモデルを定義します。
定義は「schema.prisma」に追記していきます。

idはInt型で、オートインクリメント & プライマリーキー。
nameとemailはString型で、emailのみユニークにします。

// Userモデルを定義
model User{
  id Int @id @default(autoincrement())
  name String
  email String @unique
}

直感的でいいですね。
Prismaの拡張機能を入れると予測変換も出してくれるので、サクサク書けます。

Migration

マイグレーションします。

npx prisma migrate dev --name init

上記コマンドで、「マイグレーションファイルの作成」と「マイグレーションファイルの実行」をしてくれます。

▽ マイグレーションファイルが追加されました。

▽ Userテーブルが作成されました。
中身はもちろん、空です。

Seeder

まだテーブルにデータが入っていないので、Seederファイルを作成します。
今回はprisma/seed.tsとして作成しました。

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

const prisma = new PrismaClient();

async function main() {
  const alice = await prisma.user.create({
    data: {
      name: 'Alice',
      email: 'alice@example.com',
    },
  });

  console.log({ alice });
}

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

実行はpackage.jsonに事前に書いておいた、以下コマンドで実行します。

npm run seed

▽ ちゃんと初期データが入りました。

データ取得

Express側に、データを返すエンドポイントを追記します。

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

app.get('/users', async (req: Request, res: Response) => {
  const users = await prisma.user.findMany();
  res.json(users);
})

サーバーを立てて、Curlを叩いてみます。

npm run start     // Express側のサーバーを立てる
curl http://localhost:3001/users     // Curlを飛ばす

▽ ちゃんとデータが返ってきています。

アプリと結合

いよいよ React側と結合します。

テーブルとボタンを作って、ボタン押下でCRUDを行います。

データ取得

▽ Express(Node)側

import { PrismaClient } from '@prisma/client';
import cors from 'cors';
import express, { Request, Response } from 'express';

const app = express();
const port = 3001;

app.use(cors());
app.use(express.json());
const prisma = new PrismaClient();

// ユーザー取得
app.get('/users', async (req: Request, res: Response) => {
  const users = await prisma.user.findMany();
  res.json(users);
})

app.listen(port, () => {
  console.log(`Server is Running: port ${port}`)
})

▽ React側

import axios from 'axios';
import { useState } from 'react';
import './App.css';

type User = {
  id: number,
  name: string,
  email: string
}

function App() {

  const [users, setUsers] = useState<User[]>([]);

  /** Userデータを取得 */
  const readUserData = () => {
    axios.get("http://localhost:3001/users")
      .then(res => setUsers(res.data))
      .catch(e => console.error(e))
  }

  return (
    <div>
      <h1>Sample Page</h1>
      <button onClick={readUserData} >データ取得</button>

      <div>
        <h2>データ表示</h2>
        <table border={1}>
          <thead>
            <tr>
              <th>ID</th>
              <th>名前</th>
              <th>メール</th>
            </tr>
          </thead>
          <tbody>
            {users && users.map((user) => (
              <tr key={user.id}>
                <td>{user.id}</td>
                <td>{user.name}</td>
                <td>{user.email}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  )
}

export default App

ユーザー作成

▽ Express(Node)側

import { PrismaClient } from '@prisma/client';
import cors from 'cors';
import express, { Request, Response } from 'express';

const app = express();
const port = 3001;

app.use(cors());
app.use(express.json());
const prisma = new PrismaClient();

// ユーザー作成
app.post('/users', async (req: Request, res: Response) => {
  const { name, email } = req.body;
  const data = {
    name: name,
    email: email
  }

  try {
    const newUser = await prisma.user.create({
      data: data
    })
    res.json(newUser);
  } catch (error: any) {
    res.status(400).send(error.message);
  }
})

app.listen(port, () => {
  console.log(`Server is Running: port ${port}`)
})

▽ React側

import axios from 'axios';
import './App.css';

function App() {

  // 登録するデータ
  const data = {
    name: "Tanaka",
    email: "sample@gmail.com",
  }

  /** Userデータを作成 */
  const createUserData = () => {
    axios.post("http://localhost:3001/users", data)
      .then(res => alert("データ作成完了しました!"))
      .catch(e => console.error(e))
  }

  return (
    <div>
      <h1>Sample Page</h1>
      <button onClick={createUserData} >データ作成</button>
    </div>
  )
}

export default App

ユーザー更新

▽ Express(Node)側

import { PrismaClient } from '@prisma/client';
import cors from 'cors';
import express, { Request, Response } from 'express';

const app = express();
const port = 3001;

app.use(cors());
app.use(express.json());
const prisma = new PrismaClient();

// ユーザー更新
app.put('/users/:id', async (req: Request, res: Response) => {
  const { id } = req.params;
  const { name, email } = req.body;

  const data = {
    name: name,
    email: email
  }

  try {
    const updateUser = await prisma.user.update({
      where: { id: parseInt(id) },
      data: data
    })
    res.json(updateUser);
  } catch (error: any) {
    res.status(400).send(error.message);
  }
})

app.listen(port, () => {
  console.log(`Server is Running: port ${port}`)
})

▽ React側

import axios from 'axios';
import './App.css';

function App() {
  // 更新するユーザー
  const userNum = 3;

  /** Userデータを更新 */
  const updateUserData = () => {
    axios.put(`http://localhost:3001/users/${userNum}`, data)
      .then(res => alert("データ更新完了しました!"))
      .catch(e => console.error(e))
  }

  return (
    <div>
      <h1>Sample Page</h1>
      <button onClick={updateUserData} >データ更新</button>
    </div>
  )
}

export default App

ユーザー削除

▽ Express(Node)側

import { PrismaClient } from '@prisma/client';
import cors from 'cors';
import express, { Request, Response } from 'express';

const app = express();
const port = 3001;

app.use(cors());
app.use(express.json());
const prisma = new PrismaClient();

// ユーザー削除
app.delete('/users/:id', async (req: Request, res: Response) => {
  const { id } = req.params;
  try {
    const deleteUser = await prisma.user.delete({
      where: { id: parseInt(id) }
    })
    res.json(deleteUser);
  } catch (error: any) {
    res.status(400).send(error.message);
  }
})

app.listen(port, () => {
  console.log(`Server is Running: port ${port}`)
})

▽ React側

import axios from 'axios';
import './App.css';

function App() {
  /** Userデータを削除 */
  const deleteUserData = () => {
    axios.delete(`http://localhost:3001/users/${userNum}`)
      .then(res => alert("データ削除完了しました!"))
      .catch(e => console.error(e))
  }

  return (
    <div>
      <h1>Sample Page</h1>
      <button onClick={deleteUserData} >データ削除</button>
    </div>
  )
}

export default App

GUIツール起動

Prismaには、dbの確認や更新ができるGUIツールが付属しています。
以下コマンドで起動できます。

npx prisma studio  // ポートは5555で起動

シンプルでいいですね。最近っぽい。

▽ データの挿入や削除もできます。

さいごに

今回は Prisma をいじってみました!

直感的に書ける & TS対応しているという点が人気の理由なのかな?という感想です。

個人的には DB操作ができるGUIが付属しているのが好きです。
わざわざ別ツールでアクセスしたり、コンテナの中に入らなくてもいいのはとても楽です。

ちなみに、今回は簡略化の為にトランザクションを書いていないですが、
業務使用の際は必ず書くようにしてくださいね。

トランザクションの参考は こちら

今回はここまで!
Enjoy Hacking!