これは、なにをしたくて書いたもの?
Node.js+TypeScript環境でのORMはどれを使ったらいいのかな?ということで。
このあたりみたいです。
- Sequelize | Sequelize ORM
- v5以降、TypeScriptをサポート
- TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.
- Prisma - Next-generation Node.js and TypeScript ORM for Databases
- Knex.js - A SQL Query Builder for Javascript
情報を見ていると、Node.js環境ではSequelizeが有名だと思いますが、TypeScriptで使う場合はTypeORMかPrismaを選ぶようです。
今回は、Prismaを使ってみたいと思います。
Prisma
Prismaは、次世代のNode.jsとTypeScriptのORMと謳っています。
Prisma - Next-generation Node.js and TypeScript ORM for Databases
サポートしているデータベースは、以下です。
- MySQL
- PostgreSQL
- Microsoft SQL Server
- SQLite
- MongoDB(Preview)
Prismaとはなにか?というのは、こちらに書かれています。
What is Prisma? (Overview) | Prisma Docs
主に以下の3つで構成されているみたいです。
- タイプセーフなクライアント(Prisma Client)
- マイグレーション(Prisma Migrate)
- データベースブラウザ(Prisma Studio)
また、プレビューですがPrisma Data Platformというものもあり、こちらは文字通りプラットフォームサービスの
ようで、上記とはまた性格が異なりそうです。
Prismaのいずれのツールも、Prisma schemaというものが起点になるようです。
Prisma schema (Reference) | Prisma Docs
Prisma schemaには、以下が定義されます。
- データソース(データベース接続設定)
- ジェネレーター(Prisma Clientの生成)
- データモデル(アプリケーションモデル)の定義
このPrisma schemaの存在が、他のORMと大きく異なるものみたいです。たとえば、TypeORMではデータモデルの定義にTypeScriptの
デコレーターを使用します。
Prisma自身のドキュメントからですが、なぜPrismaが良いのか?というのはこちらを見るとよいみたいです。
Why Prisma? Comparison with SQL query builders & ORMs | Prisma Docs
Should you use Prisma as a Node.js/TypeScript ORM? | Prisma Docs
Prisma自身による、他のORMとの比較はこちらに記載されています。
Comparing Prisma to other ORMs and ODMs. | Prisma Docs
Best 11 ORMs for Node.js, Query Builders & Database Libraries in 2021
今回は、まずはPrisma Clientを中心に見ていこうかなと思います。
Prisma Client
Prisma Clientは、自動生成された、型安全なクエリービルダーです。
Prisma Client - Auto-generated query builder for your data
Go向けのものもありますが、もうメンテナンスされないようですね。
機能は?というと、ドキュメントの項目を見た方が早そうですが。
Prisma Client (Reference) | Prisma Docs
まずは、こちらのスクラッチで始めるGetting Startedで試していきたいと思います。
Start from scratch with relational databases (15 min) | Prisma Docs
なおQuick Startは、GitHubリポジトリをベースに進めていくようです。
Quickstart: Getting started with TypeScript & SQLite | Prisma Docs
GitHub - prisma/quickstart: 🏁 Starter templates for the 5min Quickstart in the Prisma docs.
環境
今回の環境は、こちらです。
$ node --version v16.13.2 $ npm --version 8.1.2
データベースはMySQLを使用します。
バージョンは8.0.27で、172.17.0.2で動作しているものとします。
ちなみに、依存関係については最終的にはこうなりました。
"devDependencies": { "@types/jest": "^27.4.0", "@types/node": "^16.11.20", "esbuild": "^0.14.11", "esbuild-jest": "^0.5.0", "jest": "^27.4.7", "prettier": "2.5.1", "prisma": "^3.8.1", "ts-node": "^10.4.0", "typescript": "^4.5.4" }, "dependencies": { "@prisma/client": "^3.8.1" }
セットアップ
まずは、こちらの手順に従ってプロジェクトをセットアップしていきましょう。
Start from scratch with relational databases (15 min) | Prisma Docs
$ npm init -y $ npm i -D prisma typescript ts-node @types/node@v16
ここでインストールしたprisma
というパッケージは、Prisma CLIですね。
$ npx prisma --version prisma : 3.8.1 @prisma/client : Not found Current platform : debian-openssl-1.1.x Query Engine (Node-API) : libquery-engine 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node) Migration Engine : migration-engine-cli 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x) Introspection Engine : introspection-core 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/introspection-engine-debian-openssl-1.1.x) Format Binary : prisma-fmt 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/prisma-fmt-debian-openssl-1.1.x) Default Engines Hash : 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f Studio : 0.452.0
tsconfig.json
は、このように設定。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "lib": ["esnext"], "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true }, "include": [ "src" ] }
prisma init
を実行してみます。
$ npx prisma init
prisma/schema.prisma
というファイルができたようです。
✔ Your Prisma schema was created at prisma/schema.prisma You can now open it in your favorite editor. 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. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview). 3. Run prisma db pull to turn your database schema into a Prisma schema. 4. Run prisma generate to generate the Prisma Client. You can then start querying your database. More information in our documentation: https://pris.ly/d/getting-started
このようなファイルが生成されます。
prisma/schema.prisma
// 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") }
PostgreSQL向けの設定が出力されています。
schema.prismaを修正する
ドキュメントに沿って、生成されたschema.prisma
を修正していきます。
Connect your database | Prisma Docs
まずはprovider
を修正。
datasource db { provider = "mysql" url = env("DATABASE_URL") }
env
というのは環境変数を参照する仕組みのようです。
Environment variables | Prisma Docs
OS側の環境変数を参照しますが、.env
というファイルからもルックアップできます。このファイルは、実はprisma init
の時点でプロジェクトの
ルートディレクトリに生成されています。
.env
# Environment variables declared in this file are automatically made available to Prisma. # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview). # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
.env
ファイルは、いくつか探索するルールがあります。
Environment variables / Using .env files
ただ、.env
ファイルはバージョン管理システムにコミットしてはならないとされていて、以下の内容の.gitignore
ファイルも同時に生成
されています。
.gitignore
node_modules # Keep environment variables out of version control .env
では、用意したMySQLに接続できるようにDATABASE_URL
を修正します。
.env
# Environment variables declared in this file are automatically made available to Prisma. # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview). # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="mysql://appuser:password@172.17.0.2:3306/example"
Connection URLの記述については、こちらを参照。
Connection URLs (Reference) | Prisma Docs
次に、こちらを参照しながらデータモデルを書いていきます。
Prisma schema (Reference) | Prisma Docs
Data model (Reference) | Prisma Docs
Relations (Reference) | Prisma Docs
Names in the underlying database | Prisma Docs
インデックスの定義については、プレビュー機能のようです。
最終的にできあがったファイルは、こちら。
prisma/schema.prisma
// 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 = "mysql" url = env("DATABASE_URL") } model Post { id Int @id @default(autoincrement()) title String @db.VarChar(255) url String @unique user User @relation(fields: [userId], references: [id]) userId Int @map("user_id") @@map("post") } model User { id Int @id @default(autoincrement()) name String @db.VarChar(30) age Int posts Post[] @@map("user") }
@
の部分は属性で、だいたい意味は予想がつく気はしますが、詳しくはリファレンスに書かれています。
Prisma schema API (Reference) | Prisma Docs
これでPrisma Migrateを使ってマイグレーションします。
$ npx prisma migrate dev --name init
Prisma Migrateのドキュメントは、こちら。
Prisma Migrate | Database, Schema, SQL Migration Tool | Prisma Docs
dev
というのは開発コマンドのことで、本番環境での利用は想定していません。--name
の後には任意の名前を指定し、マイグレーションの名前に
反映されるようです。--name
の値はユニークである必要はなさそうです。
※本番環境向けにはdeploy
というコマンドを使うようです
ちなみに、prisma migrate dev
の時点でデータベースに接続するようなのですが、いわゆる管理者権限のないユーザーだとエラーに
なりました。グローバルにデータベースを操作できる必要がありそうです。
Environment variables loaded from .env Prisma schema loaded from prisma/schema.prisma Datasource "db": MySQL database "example" at "172.17.0.2:3306" Error: P3014 Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases. Read more about the shadow database (and workarounds) at https://pris.ly/d/migrate-shadow Original error: Error code: P1010 User `appuser` was denied access on the database `example`
権限設定をしなおして、もう1度実行。
$ npx prisma migrate dev --name init Environment variables loaded from .env Prisma schema loaded from prisma/schema.prisma Datasource "db": MySQL database "example" at "172.17.0.2:3306" MySQL database example created at 172.17.0.2:3306 Applying migration `20220118133834_init` The following migration(s) have been created and applied from new schema changes: migrations/ └─ 20220118133834_init/ └─ migration.sql Your database is now in sync with your schema. ✔ Generated Prisma Client (3.8.1 | library) to ./node_modules/@prisma/client in 615ms
対象のデータベースが存在しない場合は、作成するようです。
今度はうまくいき、以下のようなファイルが生成されました。
$ tree prisma prisma ├── migrations │ ├── 20220118133834_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma 2 directories, 3 files
生成されたSQLファイル。
prisma/migrations/20220118133834_init/migration.sql
-- CreateTable CREATE TABLE `post` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `url` VARCHAR(191) NOT NULL, `user_id` INTEGER NOT NULL, UNIQUE INDEX `post_url_key`(`url`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- CreateTable CREATE TABLE `user` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `name` VARCHAR(30) NOT NULL, `age` INTEGER NOT NULL, PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- AddForeignKey ALTER TABLE `post` ADD CONSTRAINT `post_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
Collationがutf8mb4_unicode_ci
固定になっているのが気になるのですが(使っているサーバーの設定は別のCollation)、これはPrismaでは
変更できなさそうです。
内部で使っているMariaDBのドライバーのデフォルトが、utf8mb4_unicode_ci
であることに起因してそうですね。
もうひとつのファイルには、Providerの情報が書かれていました。
prisma/migrations/migration_lock.toml
# Please do not edit this file manually # It should be added in your version-control system (i.e. Git) provider = "mysql"
また、node_modules/.prisma
ディレクトリには、型宣言などが生成され、こちらをプログラムの作成に利用できます。
$ tree node_modules/.prisma node_modules/.prisma └── client ├── index-browser.js ├── index.d.ts ├── index.js ├── libquery_engine-debian-openssl-1.1.x.so.node ├── package.json └── schema.prisma 1 directory, 6 files
そして、テーブルもこの時点で作成されています。
mysql> use example; Database changed mysql> show tables; +--------------------+ | Tables_in_example | +--------------------+ | _prisma_migrations | | post | | user | +--------------------+ 3 rows in set (0.00 sec)
_prisma_migrations
というテーブルは、マイグレーションの履歴のようです。
mysql> select * from _prisma_migrations; +--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+ | id | checksum | finished_at | migration_name | logs | rolled_back_at | started_at | applied_steps_count | +--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+ | f9c4ddb8-cc8e-4353-a550-3684530697b0 | 11b11399bfa8bbd1c555a1548b87ea473d8eac9aa23aaee3c12cb6e14fcb3e83 | 2022-01-18 13:38:35.440 | 20220118133834_init | NULL | NULL | 2022-01-18 13:38:34.679 | 1 | +--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+ 1 row in set (0.00 sec)
こう書くとPrismaを使うと、新規にデータベースを作る場合でないと使えないのかな?とも思うのですが、既存のプロジェクトに対しても
適用できるようです。そのパターンは、こちらを参照。
Add Prisma to an existing project that uses a relational database (15 min) | Prisma Docs
Prisma Clientを使う
では、生成された型宣言を使ってプログラムを書いていきましょう。ドキュメントは、こちらを足がかりに。
Install Prisma Client | Prisma Docs
Prisma Clientをインストールします。
$ npm i @prisma/client
確認は、テストコードで行うことにします。Jestをインストール。あと、Prettierも。
$ npm i -D jest @types/jest esbuild-jest esbuild $ npm i -D -E prettier
設定はこんな感じです。
jest.config.js
module.exports = { testEnvironment: 'node', transform: { "^.+\\.tsx?$": "esbuild-jest" } };
.prettierrc.json
{ "singleQuote": true }
あとは、テスト用のディレクトリにテストを作成していきます。
$ mkdir test
まずは、宣言部分だけ。
test/prisma.test.ts
import { Prisma, PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // ここに、テストを書く!
あとはGetting Startedと
Querying the database | Prisma Docs
Prisma Clientのドキュメントを見ながらテストコードを書いていきます。
Prisma Client (Reference) | Prisma Docs
データの登録。
CRUD (Reference) | Prisma Docs
test('insert data', async () => { const katsuo = await prisma.user.create({ data: { name: '磯野 カツオ', age: 11, posts: { create: [ { title: 'はじめてのTypeScript(+TypeScript ESLint、Prettier、Emacs lsp-mode)', url: 'https://kazuhira-r.hatenablog.com/entry/2021/10/30/222106', }, { title: 'JestでTypeScriptのテストを書く', url: 'https://kazuhira-r.hatenablog.com/entry/2021/10/31/173852', }, { title: 'TypeScriptでExpress', url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345', }, ], }, }, }); expect(katsuo.name).toBe('磯野 カツオ'); expect(katsuo.age).toBe(11); const wakame = await prisma.user.create({ data: { name: '磯野 ワカメ', age: 9, }, }); expect(wakame.name).toBe('磯野 ワカメ'); expect(wakame.age).toBe(9); const createdPosts = await prisma.post.createMany({ data: [ { title: 'Node.jsの管理ツール、nvmをインストールする', url: 'https://kazuhira-r.hatenablog.com/entry/2021/03/22/223042', userId: wakame.id, }, { title: 'Node.jsアプリケーションのログ出力に、winstonを使ってみる', url: 'https://kazuhira-r.hatenablog.com/entry/2019/05/21/235843', userId: wakame.id, }, ], }); expect(createdPosts.count).toBe(2); }); test('search single data', async () => { const katsuo = await prisma.user.findFirst({ where: { name: '磯野 カツオ' }, }); expect(katsuo?.age).toBe(11); const katsuo2 = await prisma.user.findUnique({ where: { id: katsuo?.id } }); expect(katsuo2).not.toBeNull(); });
使うとちょっと驚くのですが、findUnique
は補完候補にユニークなもの(主キーなど)しか現れないようになっています。
すごいですね。
リレーションのあるレコードを後から追加するパターンも行っていますが、新規データを登録する際にconnect
すれば関連付けもできるようです。
※今回は意図的にpost
を先に作ってしまったのでconnect
は行っていませんが、user
から作成すればOKです
Relation queries (Concepts) | Prisma Docs
1件検索。
test('search single data', async () => { const katsuo = await prisma.user.findFirst({ where: { name: '磯野 カツオ' }, }); expect(katsuo?.age).toBe(11); const katsuo2 = await prisma.user.findUnique({ where: { id: katsuo?.id } }); expect(katsuo2).not.toBeNull(); });
リレーションのある検索。ソートも入れています。
Relation queries (Concepts) | Prisma Docs
Filtering and sorting (Concepts) | Prisma Docs
test('search related data', async () => { const katsuo = await prisma.user.findFirst({ where: { name: '磯野 カツオ' }, include: { posts: true }, }); expect(katsuo?.age).toBe(11); const katsuoIncludePosts = await prisma.user.findUnique({ where: { id: katsuo?.id }, include: { posts: { orderBy: { url: 'desc' } } }, }); expect(katsuoIncludePosts?.posts).toHaveLength(3); expect(katsuoIncludePosts?.posts[0].title).toBe('TypeScriptでExpress'); expect(katsuoIncludePosts?.posts[0].url).toBe( 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345' ); });
トランザクションは、xxxMany
がバッチ更新的な扱いになるようですが、$transaction
を使うことでPromiseをトランザクションとして
まとめることもできるようです。
Transactions and batch queries (Reference) | Prisma Docs
$transaction
を使う場合は、失敗したPromiseがあるとロールバックするようです。
ロールバックするパターン。片方のPromiseは、ユニークキーを重複させるレコードを登録して失敗させます。
test('transaction rollback', async () => { const katsuo = await prisma.user.findFirst({ where: { name: '磯野 カツオ' }, }); if (katsuo) { const currentCount = await prisma.post.count(); expect(currentCount).toBe(5); try { const [post1, post2] = await prisma.$transaction([ prisma.post.create({ data: { title: 'TypeScript+Node.jsで、Echo Server/Clientを書いてみる', url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/222927', userId: katsuo.id, }, }), prisma.post.create({ data: { title: 'TypeScriptでExpress', url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345', userId: katsuo.id, }, }), ]); const rollbackedCount = await prisma.post.count(); expect(rollbackedCount).toBe(5); } catch (e) { expect((e as Error).message).toContain( 'Unique constraint failed on the constraint: `post_url_key`' ); } } else { throw new Error('katsuo is null'); } });
コミットするパターン。
test('transaction commit', async () => { const katsuo = await prisma.user.findFirst({ where: { name: '磯野 カツオ' }, }); if (katsuo) { const currentCount = await prisma.post.count(); expect(currentCount).toBe(5); const [post1, post2] = await prisma.$transaction([ prisma.post.create({ data: { title: 'TypeScript+Node.jsで、Echo Server/Clientを書いてみる', url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/222927', userId: katsuo.id, }, }), prisma.post.create({ data: { title: 'TypeScript+Node.jsプロジェクトを自動ビルドする(--watch、nodemon+ts-node)', url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/28/221448', userId: katsuo.id, }, }), ]); const committedCount = await prisma.post.count(); expect(committedCount).toBe(7); } else { throw new Error('katsuo is null'); } });
最後に、データを削除。
test('delete data', async () => { const deletedPosts = await prisma.post.deleteMany(); const deletedUsers = await prisma.user.deleteMany(); expect(deletedPosts.count).toBe(7); expect(deletedUsers.count).toBe(2); });
とりあえず、こんなところでしょうか。
まとめ
TypeScriptのORMである、Prismaを試してみました。
補完がかなり強力で、型宣言をschema.prisma
から生成しているからか、クエリーのパラメータに指定するオブジェクトの内容をほぼ補完できます。
これには驚きました。
一方で、schema.prisma
がとっつきにくいというか、ネイティブなSQLからはできなくなっていることもあったりするので、ちょっと微妙な
感じもします…。
ちょっと独自の世界観も強いような印象なのですが、どうなのでしょう。使うのがPrismaだけなら、問題ないのかなぁとも。
最近はPrismaが人気らしいので、Prismaを扱ったら終わりにしようかと思っていたのですが、1度TypeORMも試して感覚を掴んでみようかな?
という気分になりました。