技術・業務

【Next.js】Prismaを使ってみる

初めまして。システムデザイン開発の高木です。
Prisma は、T3 Stackの技術の1つに採用されている今流行りのORMです。
今まで私はバックエンド依存のORM(GormやActiveRecord等)を使用していたので、
フロントエンドベースでDB操作を行えることがとても魅力的に感じました。
今回はNext.js(TypeScript) × Prisma × Ant Design の構成で、
簡単なメモアプリを作ってみようと思います。

Prismaとは

Node.js環境で動作するTypeScriptおよびJavaScript向けのORMです。

Prismaは次の3つの要素から構成されています。
・Prisma Client
 データベースとのやり取りを行うためのライブラリ
・Prisma Migrate
 データベースのスキーマ管理を行うためのCLIツール
・Prisma Studio
 Web上でデータベース管理を行うためのGUIツール

また、PostgreSQL、MySQL、SQL Server等、主要なRDBMSに対応しています。
対応状況については以下を参照してください。

Databases supported by Prisma ORM | Prisma Documentation
This page lists all the databases and their versions that are supported by Prisma ORM.

GraphQLやREST APIのようなデータアクセスの方法にも対応しているため、
Webアプリケーション開発において幅広く使用されています。

プロジェクトの作成

まずはNext.jsプロジェクトを作成します。

npx create-next-app@latest prisma_tutorial_app --ts

質問に対しては以下のように返しています。
今回はAnt Designを使用するため、Tailwind CSSは無効にしておきます。

Would you like to use ESLint with this project? ... No / Yes
Would you like to use Tailwind CSS with this project? ... No / Yes
Would you like to use `src/` directory with this project? ... No / Yes
Would you like to use experimental `app/` directory with this project? ... No / Yes
What import alias would you like configured? ... @/*

プロジェクト作成完了後、正常にデフォルトページが表示されるかどうかを確認してみましょう。

npm run dev

Prisma CLIの導入

prismaパッケージをインストールし、起動します。

npm install prisma --save-dev
npx prisma

次にPrisma の初期設定を行います。
今回はDBにSQLiteを使用するため、datasource-providerにsqliteを指定しています。

npx prisma init --datasource-provider sqlite

このコマンドを実行することで、prisma/schema.prismaとenvファイルが生成されます。

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

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
DATABASE_URL="file:./dev.db"

Prisma Migrateを使用してmigrateを実行する

Prisma Migrate はschema.prismaのデータモデルを元にmigrateを行うため、
まずはデータモデルを定義します。

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

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

model notes {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())
}

アノテーションを使用することで、スキーマに関する情報を定義することができます。
今回は最低限のものしか使用していないため、詳しく知りたい方は以下を参照してください。

Prisma Schema API | Prisma Documentation
API reference documentation for the Prisma Schema Language (PSL).

migrateを実行します。

npx prisma migrate dev --name init

migrationファイルの生成 + スキーマの変更が実行されます。

CREATE TABLE "notes" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "content" TEXT NOT NULL,
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Prisma Studioを使用してDBの管理画面を表示する

以下のコマンドでデータベースの管理画面を表示できます。

npx prisma studio

Tableを選択する画面です。先ほど作成した「notes」をクリックします。

Tableの管理画面が表示されました。
今回は触れませんが、レコードの追加/変更/削除等、Tableに対して様々な操作が行えます。

Prisma Clientを使用してメモアプリを作成する

@prisma/clientパッケージをインストールします。

npm install @prisma/client

まずはエンドポイントを作成します。
PrismaClientを使用して、検索/登録/削除機能を実装しています。

import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function GET() {
  const notes = await getAllNotes();
  return NextResponse.json(notes);
}

export async function POST(request: NextRequest) {
  const { content } = await request.json();

  await prisma.notes.create({
    data: {
      content: content,
    },
  });

  const notes = await getAllNotes();
  return NextResponse.json(notes);
}

export async function DELETE(request: NextRequest) {
  const id = parseInt(request.nextUrl.searchParams.get('id')!);

  await prisma.notes.delete({
    where: {
      id: id,
    },
  });

  const notes = await getAllNotes();
  return NextResponse.json(notes);
}

async function getAllNotes() {
  const notes = await prisma.notes.findMany();
  return notes;
}

次に画面を作成します。
前述した通りAnt Designを使用しています。

'use client';

import { useState, useEffect, ChangeEvent } from 'react';
import { Card, Row, Col, Input, Button, Table } from 'antd';
import { ColumnsType } from 'antd/es/table';

interface DataType {
  key: string;
  id: number;
  content: string;
  createdAt: string;
}

export default function Home() {
  const [content, setContent] = useState('');
  const [dataSource, setDataSource] = useState<DataType[]>([]);

  useEffect(() => {
    const fetchNotes = async () => {
      const response = await fetch('/api/notes');
      const notes = await response.json();
      setDataSource(notes);
    };
    fetchNotes();
  }, []);

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    setContent(event.target.value);
  };

  const handleSaveClick = async () => {
    const response = await fetch('/api/notes', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ content }),
    });

    const notes = await response.json();
    setDataSource(notes);

    setContent('');
  };

  const handleDeleteClick = async (id: number) => {
    const response = await fetch(`/api/notes?id=${id}`, {
      method: 'DELETE',
    });

    const notes = await response.json();
    setDataSource(notes);
  };

  const columns: ColumnsType<DataType> = [
    {
      title: 'created_at',
      dataIndex: 'createdAt',
      width: '20%',
      render: (date: Date) => new Date(date).toLocaleDateString(),
      sorter: (a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt),
    },
    {
      title: 'content',
      dataIndex: 'content',
      width: '75%',
    },
    {
      width: '5%',
      render: (record: DataType) => (
        <Button danger onClick={() => handleDeleteClick(record.id)}>
          Delete
        </Button>
      ),
    },
  ];

  const centeredStyle: React.CSSProperties = {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    height: '100vh',
  };

  return (
    <div style={centeredStyle}>
      <Card title='Note' style={{ width: 800 }}>
        <Row>
          <Col span={16}>
            <Input
              placeholder='content'
              value={content}
              onChange={handleInputChange}
            />
          </Col>
          <Col span={7} offset={1}>
            <Button type='primary' onClick={handleSaveClick}>
              Save
            </Button>
          </Col>
        </Row>
        <Table
          dataSource={dataSource}
          columns={columns}
          rowKey={(record) => record.id}
          pagination={{
            pageSize: 5,
          }}
          style={{ marginTop: 20 }}
        />
      </Card>
    </div>
  );
}

完成形がこちらになります。

まとめ

今回はPrismaを使用してみましたが、他のフレームワークに比べてmigrateが簡単に実行できたり、型の安全性が保証されるため、正確なDB操作が行えるな、と感じました。
フロントエンドにTypeScriptを使用している方や、T3 Stackを使用してType Safeな
フルスタックアプリケーションを作成したい方は、是非一度使用してみてはいかがでしょうか。

次回は、同じくT3 Stackの技術の1つであるtRPCについての記事を書こうと思っています。


システムデザイン開発は、北海道の地で35年以上の歴史があります。企業向けのシステム設計~開発・構築~保守運用までワンストップサービスを提供するシステム開発会社です。豊富な開発実績と高い技術力を強みとして、北海道から全国へ幅広い分野・業種へトータルにサポートいたします。

システムの導入やご検討、お困りごとがありましたら、お気軽にご相談・お問合せください

SDDの受託システムとは?

お問い合わせはこちら

タイトルとURLをコピーしました