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);
}
% 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,
},
});
await prisma.user.create({
data: {},
});
await prisma.user.create({
data: {
name: 'Alice',
bar: 'buz',
},
});
}
実は、% yarn run prisma migrate dev --name init
を実行した際にnode_modules
内に型定義ファイルが自動生成されており、それを使って型チェックが行われている。
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
を渡してしまうと、プライマリキーとしての制約が守られていないので、実行時エラーになる。
async function main() {
try {
await prisma.user.create({
data: {
id: 2,
name: "Carol",
},
});
} catch (e) {
console.log(e);
}
}
一度に複数のレコードを作成するときは、createMany
を使う。
async function main() {
const users = await prisma.user.createMany({
data: [
{
name: "Carol",
},
{
name: "Dave",
},
],
});
console.log(users);
}
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);
}
フィールドを指定するには、select
を使う。
async function main() {
const users = await prisma.user.findMany({
select: {
name: true,
},
});
console.log(users);
}
レコードの絞り込みにはwhere
を使う。
以下はcontains
を使って、「name
フィールドにo
が含まれているレコード」を取得している。
async function main() {
const users = await prisma.user.findMany({
where: {
name: {
contains: "o",
},
},
});
console.log(users);
}
該当するレコードが存在しないときは空の配列を返す。
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
を使うこともできる。
async function main() {
const users = await prisma.user.findMany({
where: {
OR: [
{
name: {
startsWith: "Al",
},
},
{
id: {
gte: 3,
},
},
],
},
});
console.log(users);
}
async function main() {
const users = await prisma.user.findMany({
where: {
AND: [
{
name: {
contains: "o",
},
},
{
id: {
gte: 3,
},
},
],
},
});
console.log(users);
}
orderBy
を使って並べ替えを行うこともできる。
async function main() {
const allUsers = await prisma.user.findMany({
orderBy: {
id: "desc",
},
});
console.log(allUsers);
}
findUnique
を使うと、ユニークな識別子を使って、該当するレコードを取得できる。
該当するレコードが存在しない場合はnull
が返ってくる。
async function main() {
let user = await prisma.user.findUnique({
where: {
id: 1,
},
});
console.log(user);
user = await prisma.user.findUnique({
where: {
id: 99,
},
});
console.log(user);
}
ユニークではないフィールドを使おうとすると型エラーになる。
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);
}
async function main() {
const user = await prisma.user.findFirst({
where: {
name: {
contains: "o",
},
},
orderBy: {
id: "desc",
},
});
console.log(user);
}
レコードのアップデート
レコードのアップデートには、update
メソッドを使う。
async function main() {
const user = await prisma.user.update({
where: {
id: 4,
},
data: {
name: "David",
},
});
console.log(user);
}
存在しないレコードをアップデートしようとすると、実行時エラーになってしまう。
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);
}
存在しないレコードを削除しようとすると、実行時エラーになってしまう。
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
の型定義も更新されているので、引き続き型の恩恵を受けながら開発することができる。
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 クライアントでデータベース操作を行う。
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}]}
今回は対応しなかったが、アップデートや削除も同じ要領で実装すればよい。