blog.euxn.me

TypeORM と比べながら Prisma を触る

2020-12-23 Wed.

私は以前 TypeORM という node.js の ORM を推していましたが、 Decorator の仕様が未だ安定しないこと、 TypeScript の legacy な実装に依存している点が難点でした。 そんな中、 Next.js ベースのフルスタックフレームワークである blitz が Prisma2 を採用したこともあり注目を集めているため、 TypeORM と比較して紹介します。

TypeORM

まず TypeORM について見ていきます。 公式ドキュメントの Getting Started は以下です。 https://typeorm.io/#/ 以下ではコード片の紹介に留めているため、実際に動かす際は上記に沿って環境を構築してください。

スキーマ定義

1import { Entity, Column, Index, PrimaryGeneratedColumn } from 'typeorm';
2
3@Entity()
4export class User {
5
6 @PrimaryGeneratedColumn()
7 id: number;
8
9 @Column()
10 @Index({ unique: true })
11 email: string;
12
13 @Column()
14 name: string;
15
16 @OneToMany(type => Post, post => post.user)
17 posts Post[];
18}
19
20@Entity()
21export class Post {
22 @PrimaryGeneratedColumn()
23 id: number;
24
25 @Column()
26 content: string;
27
28 @Column()
29 user: User;
30
31 @Column()
32 userId: number;
33}

TypeScript の class ベースに Decorator で様々な情報や制約を付与していく形式です。 スキーマの定義とエンティティが同一の class で扱えるので、型的にも安全に使えます。

接続情報定義

1{
2 "type": "sqlite",
3 "database": "db.sqlite3",
4 "autoSchemaSync": true,
5 "entities": ["dist/models/*.js"],
6 "migrations": ["dist/migrations/*.js"],
7 "cli": {
8 "entitiesDir": "dist/models",
9 "migrationsDir": "dist/migrations"
10 }
11}

マイグレーション

1$ typeorm migration:run

npm パッケージの cli からマイグレーションするには一度 TypeScript から JavaScript にビルドしなくてはなりません。 TS のまま扱う場合は、 ts-node 経由で typeorm/cli.js を実行する等が必要です。

読み書き

1const user = await User.find({ id });
2
3const post = new Post();
4post.content = "hello world";
5post.user = user;
6post.userId = user.id;
7
8await post.save();

上記はアクティブレコードパターンですが、リポジトリパターンでのアクセスもできます。

1const userRepository = connection.getRepository(User);
2const user = await userRepository.find({ id });
3
4const postRepository = connection.getRwpository(Post);
5const post = new Post();
6post.content = "hello world";
7post.user = user;
8post.userId = user.id;
9await postRepository.save(post);

状況や開発のフェーズに応じてアクティブレコードパターンとリポジトリパターンを使い分けできるのも魅力の 1 つです。 TypeORM については、以下に過去のサンプルリポジトリがあります。

https://github.com/euxn23/typeorm-demo

また、こちらの potato4d さんの資料でユースケースについて紹介されています。

https://speakerdeck.com/potato4d/introduce-typeorm

Prisma

次に、 Prisma について見ていきます。 公式ドキュメントの QuickStart は以下です。

https://www.prisma.io/docs/getting-started/quickstart-typescript

以下ではコード片の紹介に留めているため、実際に動かす際は上記に沿って環境を構築してください。 また、 migration については experimental な機能のため、執筆時点(2020/12/23)の情報となります。

スキーマ・接続情報定義

1datasource db {
2 provider = "sqlite"
3 url = "file:./dev.db"
4}
5
6generator client {
7 provider = "prisma-client-js"
8}
9
10model Post {
11 id Int @id @default(autoincrement())
12 content String?
13 user User @relation(fields: [userId], references: [id])
14 userId Int
15}
16
17model User {
18 id Int @id @default(autoincrement())
19 email String @unique
20 name String?
21 posts Post[]
22}

GraphQL に似た DSL での定義となりますが、 Prisma 独自の Language Server が存在し、各種 IDE の extention ではこちらが使用されているようです。

https://github.com/prisma/language-tools

Language Server が動作しているため、エラー詳細まで表示されます。

型定義生成・マイグレーション

1$ prisma generate
2$ prisma migrate dev --preview-feature --name init

node_modules/.prisma 以下に Schema の DSL をもとに TypeScript 向けの型定義ファイルを生成します。

読み書き

1const user = await prisma.user.findUnique({ where: { id: 1 } });
2
3prisma.post.create({
4 data: { content: "hello world", user: { connect: user! } },
5});

上記の型定義生成により、 prisma にも prisma.user にも型が付いています。 Post を create する際の user の関連付けは、 connect というキーで渡すと、 User 型から UserCreateOneWithoutPostsInput という書き込み用の型として解釈・変換します。

総括

TypeORM はスキーマ定義とエンティティを同一のクラスで使い回すため、 Decorator による定義をしていますが、こちらは定義が複雑になった場合に煩雑になってしまう可能性がある、また DB の型は Decorator に依存しているため、 class 定義の型と異なる可能性がある、という点で、完全に安全とは言えませんでした。 一方、 Prisma は DSL により 1 つの定義で DB とエンティティのインタフェースの両方を定義できるため、型の齟齬が起こりにくくなっています。 DSL も Language Server によるサポートがあることも強みです。 なお、 Prisma 公式ドキュメントには TypeORM からのマイグレーションガイドが存在します。Prisma としても TypeORM の課題点を克服しようとしているように見えます。

https://www.prisma.io/docs/guides/migrate-to-prisma/migrate-from-typeorm

また、両ライブラリとも、生の SQL を安全に実行するインタフェースは存在していますが、そちらの型はユーザが宣言したとおりにしかならない、というのは仕方のないところです。 TypeORM では ORM でうまく表現できないケースは存在しましたが、 Prisma にもおそらくあるでしょう。 そういった点については確実に安全な方法がないのはもどかしいところです。 (とはいえ、通常の CRUD であれば双方とも十分に賄えているように見えます。)

終わりに

以上、 Node.js アドベントカレンダー 2020 23 日目、かつ Japan Digital Design アドベントカレンダー 2020 の 23 日目でした。 明日の Node.js アドベントカレンダーは watilde さんの Node.js v14.5.0 に入った BroadcastChannel について です。(なんと本記事執筆時点で公開されている速さ!!!)