Sae/note

Next.jsにPrismaを導入してみた

目次

    Learn Next.jsで実装したアプリケーションのデータ操作をPrismaで書き換えてみた。

    Prismaとは

    What is Prisma ORM?

    • Prisma ORM is an open-source next-generation ORM. It consists of the following parts:
    • Prisma Client: Auto-generated and type-safe query builder for Node.js & TypeScript
    • Prisma Migrate: Migration system
    • Prisma Studio: GUI to view and edit data in your database.

    https://www.prisma.io/docs/orm/overview/introduction/what-is-prisma#what-is-prisma-orm

    ORMとは

    Object-Relational Mapping

    データベースオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。

    https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E9%96%A2%E4%BF%82%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0

    業務先ではRailsを使用していて、ORMにはActive Recordを使用している。
    テーブルとモデルを関連づけることで、SQLを書かずともデータを操作できるもの。そうすることで再利用可能なコードを書くことができ、保守性にもつながるメリットを持つ。とざっくり理解している。

    インストール

    npm install prisma --save-dev
    

    初期設定

    npx prisma init
    

    prismaディレクトリとschema.prismaファイル爆誕!

    既存のDBに接続

    今回はVercelのStorage機能を使って作成したDB(Postgres)に繋ぐ

    datasourceに環境変数で管理している、DBの接続先URLを指定する。

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

    モデルの生成

    Introspection

    Prismaスキーマに現在のDBスキーマを反映したモデルを生成する。

    <figure>

    <figcaption>

    ref: What does introspection do?

    </figcaption> </figure>
    npx prisma db pull
    

    datasourceに指定したDBに接続し、DBスキーマを読み取りモデルに変換する。

    prisma/scheme.prismaにmodelが生成された!

    model customers {
      id        String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
      name      String @db.VarChar(255)
      email     String @db.VarChar(255)
      image_url String @db.VarChar(255)
    }
    
    model invoices {
      id          String   @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
      customer_id String   @db.Uuid
      amount      Int
      status      String   @db.VarChar(255)
      date        DateTime @db.Date
    }
    
    model revenue {
      month   String @unique @db.VarChar(4)
      revenue Int
    }
    
    model users {
      id       String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
      name     String @db.VarChar(255)
      email    String @unique
      password String
    }
    

    リレーションの追加

    モデル間のリレーションをスキーマファイルに追加していく。今回の例でいうと、customersinvoicesは1対多の関係性となる。
    relation fieldsは実際のDBには存在しないがPrisma Clientで使用するために定義する必要がある。

    model customers {
      id        String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
      name      String @db.VarChar(255)
      email     String @db.VarChar(255)
      image_url String @db.VarChar(255)
      invoices invoices[]
    }
    
    model invoices {
      id          String   @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
      customers customers @relation(fields: [customer_id], references: [id])
      customer_id String
      amount      Int
      status      String   @db.VarChar(255)
      date        DateTime @db.Date
    }
    

    ref: relations

    Prisma Client

    npm install @prisma/client
    

    installした@prisma/clientモジュールはnode_modules/.prisma/clientフォルダを参照している。/.prisma/clientには生成されたPrisma Clientが格納されている。

    <figure>

    <figcaption>

    ref: Install and generate Prisma Client

    </figcaption> </figure>
    prisma generate
    

    スキーマを更新する度にprisma generateを実行する必要がある。実行することでPrisma Clientが更新される。

    インスタンスの生成

    Best practices for using Prisma Client with Next.jsに従い、lib/db.tsファイルを作成。
    globalオブジェクトにPrismaClientのインスタンスを保存した。

    import { PrismaClient } from '@prisma/client';
    
    const prismaClientSingleton = () => {
      return new PrismaClient()
    };
    
    declare global {
      var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
    }
    
    const prisma = globalThis.prisma ?? prismaClientSingleton();
    
    export default prisma;
    
    if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;
    

    Best practices for using Prisma Client with Next.jsには下記のように書いてあった。
    余分なインスタンスの作成を防ぎ、コネクションプールが起きないようにするためと理解した。

    In development, the command next dev clears Node.js cache on run. This in turn initializes a new PrismaClient instance each time due to hot reloading that creates a connection to the database. This can quickly exhaust the database connections as each PrismaClient instance holds its own connection pool.
    開発では、next devコマンドは実行時にNode.jsキャッシュをクリアします。これは、データベースへの接続を作成するホットリロードにより、毎回新しいPrismaClientインスタンスを初期化します。これは、各PrismaClientインスタンスが独自の接続プールを保持するため、データベース接続をすぐに使い果たす可能性があります。

    https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices#problem

    レコードを取得してみる

    元々のSQLクエリで取得していたデータをPrisma Clientを使用して置き換えていく。
    invoicesテーブルからamount、idcustomersテーブルからnameimage_urlemailをdateの降順で最大5件、取得している。
    上のschemaで定義した通り、customer_idが外部キーとなってcustomersテーブルのidと一致するcustomersのレコードの情報を持ってきている。

    SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
          FROM invoices
          JOIN customers ON invoices.customer_id = customers.id
          ORDER BY invoices.date DESC
          LIMIT 5
    

    findManyを使用して指定した条件に合うレコードのリストを取得する。
    selectはどのプロパティを含めるかを指定することができる。

      const latestInvoices = await prisma.invoices.findMany({
        select: {
          amount: true,
          customers: {
            select: {
              name: true,
              image_url: true,
              email: true,
            },
          },
          id: true,
        },
        orderBy: {
          date: 'desc',
        },
        take: 5,
      });
    

    エラー記録

    PrismaClientInitializationError

    PrismaClientInitializationError: Prisma has detected that this project was built on Vercel, which caches dependencies. This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered. To fix this, make sure to run the `prisma generate` command during the build process.

    ビルド時に出たエラー。
    エラー文言通りにprisma generate を追加した。

      "scripts": {
        "build": "prisma generate && next build",
    ...
      },
    

    感想

    ORM自体、触るのが初めてだったがドキュメントが丁寧で理解を助けてくれた。
    業務先ではRailsのActiveRecordを使用していて、時々modelsファイルを見たりしていたのでとっかかりやすかった。あと、SQLクエリからPrismaで提供されているクエリに書き換える際に、SQLクエリを読むことに苦戦した。。