こんにちは、なかにしです。
今回は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-dbversion: "3"
services:
  db:
    build: .
    ports:
      - "5432:5432"
    volumes:
      - /mnt/wsl/ubuntu/home/prisma-practice:/var/lib/postgresql/datadockerでコンテナを立て、準備完了です。
docker compose up -dPrismaのインストールと初期設定
バック側でPrismaをインストールします。
npm install @prisma/client初期設定をします。
npx prisma initPrismaディレクトリと.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 AppGUIツール起動
Prismaには、dbの確認や更新ができるGUIツールが付属しています。
以下コマンドで起動できます。
npx prisma studio  // ポートは5555で起動

シンプルでいいですね。最近っぽい。
▽ データの挿入や削除もできます。

さいごに
今回は Prisma をいじってみました!
直感的に書ける & TS対応しているという点が人気の理由なのかな?という感想です。
個人的には DB操作ができるGUIが付属しているのが好きです。
わざわざ別ツールでアクセスしたり、コンテナの中に入らなくてもいいのはとても楽です。
ちなみに、今回は簡略化の為にトランザクションを書いていないですが、
業務使用の際は必ず書くようにしてくださいね。
トランザクションの参考は こちら
今回はここまで!
Enjoy Hacking!



