はじめてのPrisma ORM
話題の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
設定はこんなとこ。きっと接続できると思う。テーブルが何もないから現状ではこんな状態。
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つの観点から指定している。
- アプリケーションレベルの型定義
- データベースレベルの型定義
String
は、アプリケーションレベルの型定義であり、TypeScriptからこのフィールドを扱う際の型を指定している。
@db.VarChar(20)
はデータベースレベルの型定義であり、実際のデータベース内でこのフィールドがどのように保存されるかを指定する。
二重定義の目的としては…
- アプリケーションコードでの型安全性の確保
- データベース固有の型や制約の指定
- 異なるデータベース間での移植性の向上
といったことらしい。アプリケーション型とデータベース型が大きくことなるケースもあったりする。そういうときに便利。
// 日付と時刻
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ショップを想定したサンプルデータベースがある。これを利用して色々練習してみるといいかも!と思い立った次第。
Discussion