🙌

はじめてのPrisma ORM

2024/10/21に公開

話題のTypeScriptフレンドリーなORマッパー「Prisma ORM」を触ってみた。
RemixやNext.jsを実務として利用するうえでデータベースの利用は避けられない。とはいえSQLを生で書くわけにもいかないので最近ホットなPrisma ORMをチョイス。Drizzle ORMも興味があったが、情報量が少ないのと、Xの公式アカウントの投稿がアレだったので見送り。

今回のソースコードはこちら👉
ANTON072/trial-prisma-mysql

ちなみに筆者はRailsのActiveRecordと生SQLは多少の経験があるが基本はフロントエンドエンジニア。その前提で読んでいただきたい。間違っていたら是非コメントでツッコミをお願いしたい👍

1. 開発環境をDocker Composeでつくる

サクッと開発環境を作りたいので慣れているDocker Composeを利用する。データベースは手慣れたMySQLを利用する。docker-compose.ymlはこんなかんじ。開発環境用なので動けばいいやつだ。

services:
  db:
    image: mysql:latest
    container_name: mysql_local
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: mydb
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:
  • サービス名はdb。
  • データベース名はmydb。
  • rootユーザーでログインする。ユーザー名は root でパスワードは password
  • volumesを指定して永続化。

このファイルを用意したら docker compose up -d を実行して、データベースを起動する。docker compose logs -f でログがチェックできるのでエラーが出ていないか見てみる。エラーが無かったら無事にデータベースが起動したってことだ。簡単。MAMPとか利用していたのが古代のように思える。

2. SQLクライアントで疎通してみる

ちゃんとデータベースに接続できるか目視でチェックしたい派。SQLクライアントを利用する。自分はTablePlusっていう有料のアプリを利用している。MySQL専用のフリーで有名なのはSequel Aceとかだと思う。何でもいい。大体どれも一緒。

  • Name: prisma-mysql ※なんでもいい。識別子。
  • Host/IP: 127.0.0.1
  • Port: 3306
  • User: root
  • Password: password
  • Database: mydb

設定はこんなとこ。きっと接続できると思う。テーブルが何もないから現状ではこんな状態。

TablePlus

3. TypeScriptとPrismaのセットアップ

データベースができたので、MySQLにログインしてCREATE TABLEして…というフローをTypeScriptとPrismaを利用してやってみる。その為には事前準備が必要。docker-compose.yml と同じディレクトリにターミナルから進んで以下を実行する。

まずはTypeScriptのセットアップから。

npm init -y
npm install typescript ts-node @types/node --save-dev

ts-nodeはTypeScriptのコードをコンパイルせずに直接実行できるコマンドラインツール。今回のデモはスクリプトファイルをts-nodeから実行してPrisma ORMの挙動を確かめるために必要。

せっかくTypeScriptで書けるのでコードフォーマッタとリンターも入れておこう。Biomeを使う。
VSCodeにも拡張を入れておく。VSCode拡張機能 | Biome

npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init

プロジェクトルートの .vscode/setting.json に以下を記述。

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "biomejs.biome"
}

prismaのモデルファイルは .prisma といった独自のファイル形式を使う。これもVSCodeの拡張を入れておこう。
Prisma - Visual Studio Marketplace

そして .vscode/settings.json に以下の一文を入れておく。prismaファイルがフォーマットされるので便利だ。

"[prisma]": { "editor.defaultFormatter": "Prisma.prisma"},

TypeScriptを初期化。

npx tsc --init

Prisma CLI もインストールする。

npm install prisma --save-dev

最後にPrisma ORMを prisma CLI の init コマンドでセットアップする。

npx prisma init --datasource-provider mysql
✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run npx prisma db pull to turn your database schema into a Prisma schema.
3. Run npx prisma generate to generate the Prisma Client. You can then start querying your database.
4. Tip: Explore how you can extend the ORM with scalable connection pooling, global caching, and real-time database events. Read: https://pris.ly/cli/beyond-orm

More information in our documentation:
https://pris.ly/d/getting-started

.env ファイルにデータベースへの接続情報を記載する。これは練習用なので公開しているが、本来は絶対公開してはならない情報なので注意を。

DATABASE_URL="mysql://root:password@127.0.0.1:3306/mydb"

4. テーブルをつくる

準備は完了した。PrismaからCREATE TABLEをやってみる。
今回はpersonテーブルとfavorite_foodテーブルの2つを作る。

こういったモデル構造。

personテーブル

有効な値
person_id smallint(unsigned)
first_name varchar(20)
last_name varchar(20)
eye_color char(2) BL、BR、GR
birth_date date
street varchar(30)
city varchar(20)
state varchar(20)
country varchar(20)
postal_code varchar(20)

favorite_foodテーブル

person_id smallint(unsigned)
food varchar(20)

これをPrismaのモデルとして表現する。
prisma/schema.prisma

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

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

model person {
  person_id     Int             @id @default(autoincrement()) @db.UnsignedSmallInt
  fname         String          @db.VarChar(20)
  lname         String          @db.VarChar(20)
  eye_color     eye_color
  birth_date    DateTime        @db.Date
  street        String?         @db.VarChar(30)
  city          String?         @db.VarChar(20)
  state         String?         @db.VarChar(20)
  country       String?         @db.VarChar(20)
  postal_code   String?         @db.VarChar(20)
  favorite_food favorite_food[]
}

enum eye_color {
  BR
  BL
  GR
}

model favorite_food {
  person_id Int    @db.UnsignedSmallInt
  food      String @db.VarChar(20)
  person    person @relation(fields: [person_id], references: [person_id])

  @@id([person_id, food])
}

DSLなので結構むずかしい。スキーマについてはこのドキュメントに詳しくかいてあった。リレーションについてのあれこれも書いてある。読んでおこう。

Relations (Reference) | Prisma Documentation

fname を例にとると String@db.VarChar(20) の二重定義をしているように見えた。何故こうなっているのだろう。
Prismaのモデル定義では、フィールドの型を2つの観点から指定している。

  1. アプリケーションレベルの型定義
  2. データベースレベルの型定義
    String は、アプリケーションレベルの型定義であり、TypeScriptからこのフィールドを扱う際の型を指定している。
    @db.VarChar(20) はデータベースレベルの型定義であり、実際のデータベース内でこのフィールドがどのように保存されるかを指定する。

二重定義の目的としては…

  1. アプリケーションコードでの型安全性の確保
  2. データベース固有の型や制約の指定
  3. 異なるデータベース間での移植性の向上

といったことらしい。アプリケーション型とデータベース型が大きくことなるケースもあったりする。そういうときに便利。

// 日付と時刻
createdAt DateTime @db.Timestamp
// JSONデータ
metadata Json @db.Text
// Enum
enum UserRole {
  ADMIN
  USER
  GUEST
}

model User {
  role UserRole @db.VarChar(255)
}

例えばTypeScriptのコードからJSONでDBに保存したら、Prismaがテキストに変換してDBに保存してくれるってことらしい。融通が利いていいかもね❗️

モデル定義が完了したとする。
このモデル定義をもとにマイグレーションファイルを作る。

npx prisma migrate dev --name init

この dev というのはローカルのDBに対して実行するよ、ということらしい。

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "mydb" at "127.0.0.1:3306"

Applying migration `20241021132254_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20241021132254_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

✔ Generated Prisma Client (v5.21.1) to ./node_modules/@prisma/client in 80ms

TablePlusでチェックしたらできてた❗️

5. データを挿入する

データを投入してみる。ここからTypeScriptのコード。

scripts/insert_person.ts

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  const person = await prisma.person.create({
    data: {
      fname: "William",
      lname: "Turner",
      eye_color: "BL",
      birth_date: new Date("2023-10-21"),
    },
  });
  console.log(person);
}

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

実行する。

npx ts-node scripts/insert_person.ts
{
  person_id: 1,
  fname: 'William',
  lname: 'Turner',
  eye_color: 'BL',
  birth_date: 2023-10-21T00:00:00.000Z,
  street: null,
  city: null,
  state: null,
  country: null,
  postal_code: null
}

入ってる❗

6. データを参照する

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  const persons = await prisma.person.findMany();
  console.log(persons);
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });
npx ts-node scripts/read_person.ts
[
  {
    person_id: 1,
    fname: 'William',
    lname: 'Turner',
    eye_color: 'BL',
    birth_date: 2023-10-21T00:00:00.000Z,
    street: null,
    city: null,
    state: null,
    country: null,
    postal_code: null
  }
]

7. まとめ

TypeScriptのパワーで型がバンバンでて書けるのは非常に書き味がいい。
ドキュメントもかなりのボリューム量があるのでマスターするのはなかなか難しそう。
SQLの書籍などをPrismaで書き進めて慣れていくのがいいのかも。

手元にこの本があるので、Prismaで書いて試している最中。
O'Reilly Japan - 初めてのSQL 第3版

MySQLには「sakilaデータベース」と呼ばれるレンタルDVDショップを想定したサンプルデータベースがある。これを利用して色々練習してみるといいかも!と思い立った次第。

MySQL :: Sakila Sample Database

株式会社トゥーアール

Discussion