技術・業務

【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
Prisma ORM supports PostgreSQL, MySQL, SQLite, MongoDB, SQL Server, CockroachDB, and serverless databases'

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
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をコピーしました