Prisma は、Node.js の ORM。
この記事では、導入方法、基本的な使い方について説明したのち、Prisma を使って簡単な API サーバを作ってみる。
Node.js のバージョンは16.13.2
、MySQLのバージョンは8.0.28
という環境で、動作確認している。
使用している npm ライブラリのバージョンは以下の通り。
- @prisma/client@3.11.0
- @types/node@16.11.26
- prisma@3.11.0
- ts-node-dev@1.1.8
- typescript@4.6.2
MySQL のインストール
ORM である Prisma を試すためには、まずデータベースをセットアップしないといけない。今回は MySQL を使うことにする。
この記事では Homebrew を使ってインストールしているが、他の方法でももちろん問題ない。
% brew install mysql
バージョンを確認できればインストール成功。
% mysql --version mysql Ver 8.0.28 for macos11.6 on x86_64 (Homebrew)
brew services start mysql
で起動、brew services stop mysql
で終了。
以降、MySQL が起動しているという前提で話を進めていく。
以下のコマンドを打つといくつか質問されるので、root ユーザーのパスワード設定以外は全て No にする。Enter を押していけば No になるはず。
% mysql_secure_installation
以下のコマンドを打つと root ユーザーのパスワードを求められるので、先程設定したパスワードを入力すると、ログインできる。
% mysql --user=root --password
データベースの準備
MySQL にログインしたら、Prisma で操作するデータベースを用意する。
今回はprisma_db
という名前にしたが、特に制限はない。
mysql> CREATE DATABASE prisma_db;
SHOW DATABASES;
で確認してprisma_db
が存在すれば成功。
mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | prisma_db | | sys | +--------------------+ 5 rows in set (0.00 sec)
USE prisma_db;
とすると、以降はprisma_db
に対して操作できるようになる。
SHOW TABLES;
を実行すると、prisma_db
にはまだテーブルがないことを確認できる。
mysql> SHOW TABLES; Empty set (0.00 sec)
最後に、このデータベースのポート番号を確認する。Prisma の設定時に必要になる。
mysql> show variables like 'port'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | port | 3306 | +---------------+-------+ 1 row in set (0.00 sec)
Prisma プロジェクトを作成する
まずは必要なライブラリをインストールする。
% yarn init -y % yarn add -D typescript ts-node-dev prisma
TypeScript で開発していくので、以下のtsconfig.json
を作る。
{ "compilerOptions": { "sourceMap": true, "outDir": "dist", "strict": true, "lib": ["esnext", "DOM"], "esModuleInterop": true } }
以下のコマンドを実行すると、いくつかのファイルが生成される。
% yarn run prisma init
まず.env
。
このファイルでDATABASE_URL
という環境変数が定義されているので、これを自身の環境に合わせて書き換える。
この記事の内容に沿って環境を構築した場合、以下のようになるはず。YOURPASSWORD
の部分だけ、先程設定した root ユーザーのパスワードに書き換えればよい。
DATABASE_URL="mysql://root:YOURPASSWORD@localhost:3306/prisma_db"
そして、prisma/schema.prisma
という名前のスキーマファイルも生成される。
以下の内容になっているはずなので、postgresql
の部分をmysql
に書き換える。
// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") }
モデルを定義する
環境構築が終わったのでここから実際に開発を進めていくが、まずはモデルを定義する。
先程編集したスキーマファイルに、モデルの定義を書き足していく。
スキーマファイルはPrisma Schema Language(PSL)
という構文で書いていく。
また、.prisma
ファイルは、% yarn run prisma format
を実行することでフォーマットできる。
早速、User
というモデルを定義してみる。
model User { id Int @id @default(autoincrement()) name String }
そして、以下のコマンドを実行する。
% yarn run prisma migrate dev --name init
そうすると様々な処理が行われるが、まず、@prisma/client
がまだインストールされていなかった場合はインストールされる。package.json
に追記されていることを確認できる。
それから、prisma/migrations
ディレクトリに、マイグレーションファイルが作成される。
そしてそのマイグレーションファイルをデータベースに対して実行する。
prisma_db
の中身を見てみると、User
テーブルが作られているのが分かる。
mysql> SHOW TABLES; +---------------------+ | Tables_in_prisma_db | +---------------------+ | _prisma_migrations | | User | +---------------------+
このように、Prisma のモデルはデータベースのテーブルに対応している。
DESC
で調べてみると、モデルの定義通りに作成されている。
mysql> DESC User; +-------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(191) | NO | | NULL | | +-------+--------------+------+-----+---------+----------------+ 2 rows in set (0.00 sec)
この時点ではレコードはまだ存在しない。
mysql> SELECT * FROM User; Empty set (0.00 sec)
Prisma クライアントを使った CRUD 操作
Prisma クライアントを使ってコードを書いていくことで、データベースに対する操作を行うことができる。
ここでは、User テーブルに対して簡単な CRUD 操作を行っていく。
最初に、動作確認用の雛形となるコードを用意する。
src/index.ts
という名前で以下のファイルを作成する。
import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); async function main() { // ここにクエリを書いていく } main() .catch((e) => { throw e; }) .finally(async () => { // データベースとのコネクションを切る await prisma.$disconnect(); });
あとはmain
関数のなかに具体的な処理を書き、% yarn run ts-node-dev src/index.ts
で実行すればいい。
レコードの作成
レコードを作成するにはcreate
メソッドを使う。create
の返り値は、作成されたUser
のオブジェクト。
async function main() { const user = await prisma.user.create({ data: { name: "Alice", }, }); console.log(user); // { id: 1, name: 'Alice' } }
% yarn run ts-node-dev src/index.ts
を実行したあとにデータベースで検索を行うと、レコードが作られていることがわかる。
mysql> SELECT * FROM User; +----+-------+ | id | name | +----+-------+ | 1 | Alice | +----+-------+ 1 row in set (0.00 sec)
data
に渡すフィールドがモデルの定義と一致していない場合、型エラーとなる。
// 以下は全て型エラー async function main() { await prisma.user.create({ data: { name: 1, // name の型が正しくない }, }); await prisma.user.create({ data: {}, // name が渡されていない }); await prisma.user.create({ data: { name: 'Alice', bar: 'buz', // 不要なフィールド bar を渡している }, }); }
実は、% yarn run prisma migrate dev --name init
を実行した際にnode_modules
内に型定義ファイルが自動生成されており、それを使って型チェックが行われている。
// node_modules/.prisma/client/index.d.ts から抜粋 /** * Model User * */ export type User = { id: number name: string }
id
は auto_increment なので明示的に渡さなくてもよいのだが、もちろん渡してもよい。
async function main() { const user = await prisma.user.create({ data: { id: 2, name: "Bob", }, }); console.log(user); // { id: 2, name: 'Bob' } }
但し、既に存在しているid
を渡してしまうと、プライマリキーとしての制約が守られていないので、実行時エラーになる。
async function main() { try { await prisma.user.create({ data: { id: 2, name: "Carol", }, }); } catch (e) { // PrismaClientKnownRequestError // Unique constraint failed on the constraint: `PRIMARY` console.log(e); } }
一度に複数のレコードを作成するときは、createMany
を使う。
async function main() { const users = await prisma.user.createMany({ data: [ { name: "Carol", }, { name: "Dave", }, ], }); console.log(users); // { count: 2 } }
mysql> SELECT * FROM User; +----+-------+ | id | name | +----+-------+ | 1 | Alice | | 2 | Bob | | 3 | Carol | | 4 | Dave | +----+-------+ 4 rows in set (0.00 sec)
レコードの取得
レコードの取得を行うためのメソッドとしてここでは、findMany
、findUnique
、findFirst
を紹介する。
まずfindMany
。これは、複数のレコードを取得するメソッド。
条件を何も指定しなかった場合、全てのフィールドを持った全レコードが返ってくる。
async function main() { const allUsers = await prisma.user.findMany(); console.log(allUsers); // [ // { id: 1, name: 'Alice' }, // { id: 2, name: 'Bob' }, // { id: 3, name: 'Carol' }, // { id: 4, name: 'Dave' } // ] }
フィールドを指定するには、select
を使う。
async function main() { const users = await prisma.user.findMany({ select: { name: true, }, }); console.log(users); // [ // { name: 'Alice' }, // { name: 'Bob' }, // { name: 'Carol' }, // { name: 'Dave' } // ] }
レコードの絞り込みにはwhere
を使う。
以下はcontains
を使って、「name
フィールドにo
が含まれているレコード」を取得している。
async function main() { const users = await prisma.user.findMany({ where: { name: { contains: "o", }, }, }); console.log(users); // [ { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' } ] }
該当するレコードが存在しないときは空の配列を返す。
async function main() { const users = await prisma.user.findMany({ where: { name: { contains: "z", }, }, }); console.log(users); // [] }
contains
以外にも様々な検索条件がある。詳しくは公式ドキュメントを参照。
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#filter-conditions-and-operators
OR
やAND
を使うこともできる。
// 「name が Al から始まる」 or 「id が 3 以上」 async function main() { const users = await prisma.user.findMany({ where: { OR: [ { name: { startsWith: "Al", }, }, { id: { gte: 3, }, }, ], }, }); console.log(users); // [ // { id: 1, name: 'Alice' }, // { id: 3, name: 'Carol' }, // { id: 4, name: 'Dave' } // ] }
// 「name に o を含む」 and 「id が 3 以上」 async function main() { const users = await prisma.user.findMany({ where: { AND: [ { name: { contains: "o", }, }, { id: { gte: 3, }, }, ], }, }); console.log(users); // [ { id: 3, name: 'Carol' } ] }
orderBy
を使って並べ替えを行うこともできる。
async function main() { const allUsers = await prisma.user.findMany({ orderBy: { id: "desc", }, }); console.log(allUsers); // [ // { id: 4, name: 'Dave' }, // { id: 3, name: 'Carol' }, // { id: 2, name: 'Bob' }, // { id: 1, name: 'Alice' } // ] }
findUnique
を使うと、ユニークな識別子を使って、該当するレコードを取得できる。
該当するレコードが存在しない場合はnull
が返ってくる。
async function main() { let user = await prisma.user.findUnique({ where: { id: 1, }, }); console.log(user); // { id: 1, name: 'Alice' } user = await prisma.user.findUnique({ where: { id: 99, }, }); console.log(user); // null }
ユニークではないフィールドを使おうとすると型エラーになる。
async function main() { const user = await prisma.user.findUnique({ where: { name: "Alice", // 型エラー }, }); }
findFirst
は、最初に見つかったレコードを返す。
async function main() { const user = await prisma.user.findFirst({ where: { name: { contains: "o", }, }, }); console.log(user); // { id: 2, name: 'Bob' } }
// orderBy で降順にしているので、{ id: 3, name: 'Carol' } が返ってくる async function main() { const user = await prisma.user.findFirst({ where: { name: { contains: "o", }, }, orderBy: { id: "desc", }, }); console.log(user); // { id: 3, name: 'Carol' } }
レコードのアップデート
レコードのアップデートには、update
メソッドを使う。
async function main() { const user = await prisma.user.update({ where: { id: 4, }, data: { name: "David", }, }); console.log(user); // { id: 4, name: 'David' } }
存在しないレコードをアップデートしようとすると、実行時エラーになってしまう。
// id が 99 のレコードの name をアップデートしようとしているが、該当するレコードは存在しないため、実行時エラーになる async function main() { const user = await prisma.user.update({ // 実行時エラー where: { id: 99, }, data: { name: "David", }, }); console.log(user); }
レコードの削除
レコードの削除にはdelete
メソッドを使う。
async function main() { const user = await prisma.user.delete({ where: { id: 4, }, }); console.log(user); // { id: 4, name: 'David' } }
存在しないレコードを削除しようとすると、実行時エラーになってしまう。
// id が 99 のレコードを削除しようとしているが、該当するレコードは存在しないため、実行時エラーになる async function main() { const user = await prisma.user.delete({ // 実行時エラー where: { id: 99, }, }); console.log(user); }
マイグレーション
モデルの追加・変更を行う場合は、スキーマファイルを更新して、再びprisma migrate
を行えばいい。
今回はUser
モデルにisAdmin
フィールドを足すことにする。
model User { id Int @id @default(autoincrement()) name String isAdmin Boolean }
しかしこの状態で% yarn run prisma migrate dev --name add-is-admin
を実行すると、エラーになってしまう
Error: ⚠️ We found changes that cannot be executed: • Step 0 Added the required column `isAdmin` to the `User` table without a default value. There are 3 rows in this table, it is not possible to execute this step.
User
にはisAdmin
が必須になるように変更を行おうとしたが、既にUser
テーブルにはレコードが 3 つあり、それらは当然isAdmin
を持っていないため、エラーになってしまう。
対応方法はいくつかあるが、今回はデフォルト値を設定して解決することにする。
スキーマファイルを編集し、isAdmin
にデフォルト値を設定する。
model User { id Int @id @default(autoincrement()) name String isAdmin Boolean @default(false) }
再度% yarn run prisma migrate dev --name add-is-admin
を実行すると、今度は上手くいく。
データベースを調べてみると、新しいモデルの定義が反映されていることを確認できる。
mysql> DESC User; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(191) | NO | | NULL | | | isAdmin | tinyint(1) | NO | | 0 | | +---------+--------------+------+-----+---------+----------------+ 3 rows in set (0.00 sec) mysql> SELECT * FROM User; +----+-------+---------+ | id | name | isAdmin | +----+-------+---------+ | 1 | Alice | 0 | | 2 | Bob | 0 | | 3 | Carol | 0 | +----+-------+---------+ 3 rows in set (0.00 sec)
node_modules/.prisma/client/index.d.ts
の型定義も更新されているので、引き続き型の恩恵を受けながら開発することができる。
/** * Model User * */ export type User = { id: number name: string isAdmin: boolean }
API サーバの構築
最後に実践として、Prisma を使った簡単な API サーバを構築してみる。
仕様は以下のようにする。
/user
にGET
メソッドでリクエストすると、全てのUser
が返ってくる/user/{ID}
にGET
メソッドでリクエストすると、id
フィールドがID
のUser
が返ってくる/user
にPOST
メソッドでリクエストすると、User
が新規作成され、そのUser
が返ってくる
まず、Node.js の型定義ファイルをインストールする。
% yarn add -D @types/node@16
次に、src/db.ts
という名前のファイルを作成する。このファイルでは、Prisma クライアントでデータベース操作を行う。
// src/db.ts import { PrismaClient, User } from "@prisma/client"; const prisma = new PrismaClient(); export async function getUser(id: number): Promise<User | null> { return await prisma.user.findUnique({ where: { id, }, }); } export async function getAllUsers(): Promise<User[]> { return await prisma.user.findMany(); } export async function createUser({ name }: { name: string }): Promise<User> { return await prisma.user.create({ data: { name, }, }); }
次に、src/index.ts
でサーバについて記述していくのだが、まずは「全てのUser
の取得」にのみ、対応させる。
import http from "http"; import { getAllUsers } from "./db"; function resNotFound(res: http.ServerResponse) { res.writeHead(404, { "Content-Type": "application/json", }); res.write(JSON.stringify({ message: "Not Found" }) + "\n"); res.end(); } function resData(res: http.ServerResponse, data: unknown) { res.writeHead(200, { "Content-Type": "application/json", }); res.write(JSON.stringify(data) + "\n"); res.end(); } http .createServer(async (req, res) => { if (req.url === undefined || !req.url.startsWith("/user")) { resNotFound(res); return; } switch (req.method) { case "GET": { switch (true) { case /^\/user$/.test(req.url): { const allUsers = await getAllUsers(); resData(res, { data: allUsers }); break; } default: { resNotFound(res); break; } } break; } default: { res.writeHead(405, { "Content-Type": "application/json", }); res.write(JSON.stringify({ message: "Method Not Allowed" }) + "\n"); res.end; break; } } }) .listen(8080);
% yarn run ts-node-dev src/index.ts
でサーバが起動するので、早速リクエストしてみる。
% curl http://localhost:8080/user {"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false}]}
問題なく全てのUser
を取得できた。
次に、id
を指定して特定のUser
を取得できるようにする。
以下の変更を加えてサーバを起動し直す。
@@ -1,6 +1,6 @@ import http from "http"; -import { getAllUsers } from "./db"; +import { getAllUsers, getUser } from "./db"; function resNotFound(res: http.ServerResponse) { res.writeHead(404, { @@ -33,6 +33,12 @@ resData(res, { data: allUsers }); break; } + case /^\/user\/[0-9]+$/.test(req.url): { + const id = Number(req.url.slice(6)); + const user = await getUser(id); + resData(res, { data: user }); + break; + } default: { resNotFound(res); break;
そうすると、個別のUser
を取得できるようになる。
% curl http://localhost:8080/user/1 {"data":{"id":1,"name":"Alice","isAdmin":false}} % curl http://localhost:8080/user/2 {"data":{"id":2,"name":"Bob","isAdmin":false}} % curl http://localhost:8080/user/9 {"data":null}
最後に、User
を作成できるようにする。
以下の変更を加えてサーバを起動し直せばよい。
@@ -1,6 +1,7 @@ import http from "http"; +import { URLSearchParams } from "url"; -import { getAllUsers, getUser } from "./db"; +import { getAllUsers, getUser, createUser } from "./db"; function resNotFound(res: http.ServerResponse) { res.writeHead(404, { @@ -10,6 +11,14 @@ res.end(); } +function resBadRequest(res: http.ServerResponse) { + res.writeHead(400, { + "Content-Type": "application/json", + }); + res.write(JSON.stringify({ message: "Bad Request" }) + "\n"); + res.end(); +} + function resData(res: http.ServerResponse, data: unknown) { res.writeHead(200, { "Content-Type": "application/json", @@ -47,6 +56,33 @@ break; } + case "POST": { + switch (true) { + case /^\/user$/.test(req.url): { + let data = ""; + req.on("data", (chunk) => { + data += chunk; + }); + req.on("end", async () => { + const params = new URLSearchParams(data); + const name = params.get("name"); + if (name === null) { + resBadRequest(res); + return; + } + const user = await createUser({ name }); + resData(res, { data: user }); + }); + break; + } + default: { + resNotFound(res); + break; + } + } + break; + } + default: { res.writeHead(405, { "Content-Type": "application/json",
POST
メソッドでUser
を作成できるようになっている。
% curl -d name="Dave" http://localhost:8080/user {"data":{"id":5,"name":"Dave","isAdmin":false}} % curl http://localhost:8080/user {"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false},{"id":5,"name":"Dave","isAdmin":false}]}
今回は対応しなかったが、アップデートや削除も同じ要領で実装すればよい。