概要
https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#type-safety
Node.js + TypeScript を使用する前提で最適な ORM について調査を行った。 ただし ORM 一つ一つの情報量が多く、すべてを網羅的に見ることは非現実的であるため、 あらかじめ github のスター数でフィルタリングをした。
また比較する軸としては
- パフォーマンス
- TypeScript との互換性
- 保守性 とした。
ORM のメリデメ
ORM のメリット・デメリットについて整理を行う。
メリット
- SQL を直接書かずにデータベースへアクセス
- データベースへの問い合わせ結果をオブジェクトへマッピング
- マイグレーションファイルの自動生成
- コードを量を削減できる
デメリット
- SQL の生成ロジックがブラックボックス
- ORM の学習コスト
候補
Github のスター数でフィルタリングを行った。
検索キーワードとしては node.js orm
node orm
を使用し、上位 3 件に表示された
を対象とした。
TypeORM は初めて TypeScript 制のアプリケーションに特化した ORM であるため人気が高く、 Sequelize は node.js との組み合わせで最も歴史のある(9 年前にリリース)ORM で人気を誇る、 Prisma は比較的この中では新しいものの TypeORM など既存の ORM で内包していた問題を解決する。
比較
パフォーマンス
生成される SQL の比較については調査難易度が高いため、 調査可能な下記について
- よくある SQL 問題に対応しているか
- 生の SQL を実行することができるか(自由度の高い SQL を実行できるか)
- 吐き出されるSQLをロギングできるか
- Indexを登録できるか
について調査を行う。
よくある SQL 問題に対応しているか
TypeORM
n+1 問題対応
https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations
bulk 対応
https://github.com/typeorm/typeorm/blob/master/docs/insert-query-builder.md
select文の対応
https://stackoverflow.com/questions/64401212/how-to-select-specific-columns-in-typeorm-querybuilder
Sequelize
n+1 問題対応 https://sequelize.org/master/manual/eager-loading.html bulk 対応 https://sequelize.org/v5/manual/instances.html#working-in-bulk--creating--updating-and-destroying-multiple-rows-at-once- select文の対応 https://sequelize.org/master/manual/model-querying-basics.html
Prisma
n+1 問題に対応 https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance bulk 対応 https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance#using-bulk-queries select文の対応 https://www.prisma.io/docs/concepts/components/prisma-client/select-fields
生の SQL を実行することができるか
TypeORM
可能。 https://typeorm.io/#/entity-manager-api
Sequelize
可能。 https://sequelize.org/master/manual/raw-queries.html
サブクエリも対応 https://sequelize.org/master/manual/sub-queries.html
Prisma
可能。 https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access
吐き出されるSQLをロギングできるか
TypeORM
設定ファイルで logging: true
と指定することでロギングを有効化することができる。
{ name: "mysql", type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", ... logging: true }
また情報のタイプによってロギングを切り分けることもできる。
{ host: "localhost", ... logging: ["query", "error"] }
また独自のロガーを設定することも可能。
import {Logger} from "typeorm"; export class MyCustomLogger implements Logger { // implement all methods from logger class }
import {createConnection} from "typeorm"; import {MyCustomLogger} from "./logger/MyCustomLogger"; createConnection({ name: "mysql", type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", logger: new MyCustomLogger() });
Sequelize
Sequelizeではデフォルトで吐き出されるSQLが標準出力される。
Sequelizeをインスタンス化するタイミングで optionsのloggingでこの処理をカスタマイズすることができる。
const sequelize = new Sequelize('sqlite::memory:', { // Choose one of the logging options logging: console.log, // Default, displays the first parameter of the log function call logging: (...msg) => console.log(msg), // Displays all log function call parameters logging: false, // Disables logging logging: msg => logger.debug(msg), // Use custom logger (e.g. Winston or Bunyan), displays the first parameter logging: logger.debug.bind(logger) // Alternative way to use custom logger, displays all messages });
Prisma
Prismaではロギングに関して2つの方法を提供している。
- Log to stdout
対象となるイベント名を引数の log
に指定することで標準出力することができる。
const prisma = new PrismaClient({ log: ['query', 'info', `warn`, `error`], })
- Event-based logging
イベントを購読することでロギングすることができる。
prisma.$on('query', e => { console.log("Query: " + e.query) console.log("Duration: " + e.duration + "ms") })
Indexを登録できるか
DBで直接Indexを貼る手間はそこまで発生しないと思うが、 ORMでIndexを貼れるか調査を行った。
TypeORM
登録可能。 https://github.com/typeorm/typeorm/blob/master/docs/indices.md
- 単体指定
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Index() @Column() firstName: string; @Column() @Index() lastName: string; }
- 複数指定
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm"; @Entity() @Index(["firstName", "lastName"]) @Index(["firstName", "middleName", "lastName"], { unique: true }) export class User { @PrimaryGeneratedColumn() id: number; @Column() firstName: string; @Column() middleName: string; @Column() lastName: string; }
- unique指定
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Index({ unique: true }) @Column() firstName: string; @Column() @Index({ unique: true }) lastName: string; }
Sequelize
登録可能。 https://sequelize.org/master/manual/indexes.html インデックスの種類などを指定することも可能。
- 単体指定
const User = sequelize.define('User', { /* attributes */ }, { indexes: [ { fields: ['email'] }, ] });
- 複数指定
const User = sequelize.define('User', { /* attributes */ }, { indexes: [ // By default index name will be [table]_[fields] // Creates a multi column partial index { name: 'public_by_author', fields: ['author', 'status'], where: { status: 'public' } }, ] });
- unique指定
const User = sequelize.define('User', { /* attributes */ }, { indexes: [ // Create a unique index on email { unique: true, fields: ['email'] }, ] });
Prisma
登録可能。 https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#index インデックスの名前、対象のカラムの指定が可能。
@@index(fields: [title, author]) @@index([title, author])
uniqueにしたい場合は下記のように書ける。
model User { firstname Int lastname Int id Int? @@unique([firstname, lastname, id]) }
サブクエリに対応しているか
予めフィルタリングしたサブクエリにJOINまたはWHEREで結合を行うことでパフォーマンスの向上が期待できる。 上記のようなケースを対処するため、サブクエリを明示的に書けるかの調査を行った。 また前提としてすべてのORMで生のSQLが記述できるため、対応していなくても実行することは可能。
TypeORM
queryをstringへ変換することで対応可能。 https://stackoverflow.com/questions/53553523/typeorm-subqueries https://dev.to/yoshi_yoshi/typeorm-query-builder-with-subquery-490c
const subquery = await getManager() .createQueryBuilder(table4, 't4') .select('"t4".f') .addSelect('"t4".g') .addSelect('"t5".e') .addSelect('"t6".h') .innerJoin(table5, 't5', '"t4".g = "t5".g') .innerJoin(table6, 't6', '"t6".g = "t4".g') .where('"t4".k = 4 AND ("t6".i = 2 OR ("t6".i = 1 AND "t6".j = 1))'); model = await getManager() .createQueryBuilder(table1, 't1') .select('"t1".a') .addSelect("TO_CHAR (MAX (jointable.f), 'MON YYYY')", 'f') .addSelect('"t3".c') .addSelect('"t3".d') .addSelect('"t1".e') .leftJoin('table2', 't2', '"t2".e = "t1".e') .innerJoin(table3, 't3', '"t3".d = "t2".d') .innerJoin('('+subquery.getQuery()+')', 'jointable', '"t1".e = jointable.e') .where('jointable.h = :h AND (:d = 3 OR "t3".d = :d)', { h: h, d: d }) .groupBy('"t1".a, "t3".c, "t3".d, "t1".e') .orderBy('"t1".a', 'ASC') .getRawMany();
Sequelize
一部生のSQLを記述することで対応可能。 https://sequelize.org/master/manual/sub-queries.html
SELECT *, ( SELECT COUNT(*) FROM reactions AS reaction WHERE reaction.postId = post.id AND reaction.type = "Laugh" ) AS laughReactionsCount FROM posts AS post
Post.findAll({ attributes: { include: [ [ // Note the wrapping parentheses in the call below! sequelize.literal(`( SELECT COUNT(*) FROM reactions AS reaction WHERE reaction.postId = post.id AND reaction.type = "Laugh" )`), 'laughReactionsCount' ] ] } });
Prisma
生のSQLを書く必要がある。
https://github.com/prisma/prisma/discussions/2836
TypeScript との互換性
セットアップ
TypeORM
特になし。
Sequelize
Sequelize で TypeScript 化は可能であるが、型を自作する必要があり実装上複雑化が懸念される。 https://sequelize.org/master/manual/typescript.html
sequelize-typescript を使用することで上記よりも簡略化はできるが学習コスト増加が懸念される。 https://github.com/RobinBuschmann/sequelize-typescript#readme
Prisma
特になし
型付けの性能
TypeORM
型付けをサポートしているが Prisma より弱い。 例えば select などのクエリで指定したカラム以外の要素へアクセスしようとしたときに、アクセスできてしまうため、 実行時にエラーが発生する。 https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#type-safety
Sequelize
Prisma
TypeORM と比較してより強力な型付けを提供する。 Prisma ではクエリに沿った型を返すため、select などでカラムを指定しても、その値以外アクセスすることができない。 https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#type-safety
保守性
外部ドキュメント
外部ドキュメントがどれだけ存在するかを調査した。 stack overflow, medium, dev.to なども考慮に入れたかったが、記事数が表示されないため除外した。 対象媒体としては google search, qiita とする。 また google search で prisma と検索した場合, orm 以外の prisma と関連付けられるため、対象キーワードに orm を付けて検索を行った。
TypeORM
- google search: 418,000 件
- qiita: 218 件
Sequelize
- google search: 414,000 件
- qiita: 248 件
Prisma
- google search: 339,000 件
- qiita: 148 件
公式ドキュメント
公式ドキュメントの読みやすさ、また初心者に対する情報量の多さを比較した。 定性的ではあるが圧倒的に Prisma の公式ドキュメントが読みやすく、初心者向けの ガイド情報が豊富である。時点で TypeORM といったところか。
TypeORM
Sequelize
Prisma
モデルの定義
TypeORM
TypeORM ではデータベースのテーブルをクラスへマッピングする。 このクラスをもとに migration ファイルを生成したり、インスタンスに CRUD のメソッドを提供する。 Active Record または Data Mapper パターンを採用している。
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, } from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column({ nullable: true }) name: string; @Column({ unique: true }) email: string; @OneToMany((type) => Post, (post) => post.author) posts: Post[]; }
Sequelize
Active Record パターン 返す値はインスタンス。
module.exports = function (sequelize, DataTypes) { const Project = sequelize.define("project", { title: DataTypes.STRING, description: DataTypes.TEXT, }); return Project; };
sequelize-typescript を使用した場合
import { Table, Column, Model, HasMany } from "sequelize-typescript"; @Table class Person extends Model { @Column name: string; @Column birthday: Date; @HasMany(() => Hobby) hobbies: Hobby[]; }
Prisma
Prisma は上記 ORM と異なり、定義は独自の Prisma Scheme で行う。 クラスではなく Prisma Scheme を使用することで、インスタンスの生成コストの軽減・CRUD ロジックの分離を行うことができる。 Prisma Scheme ではモデルの定義を宣言的に行うことができ、了解性の向上が期待できる。 またクエリの返り値はインスタンスではなくオブジェクトである。
model User { id Int @id @default(autoincrement()) name String? email String @unique posts Post[] }
Filter
TypeORM
検索クエリが SQL ライクな書き方ができる。ただし弱い型付けである。
const posts = await postRepository.find({ where: { title: ILike("Hello World"), }, });
const posts = await postRepository.find({ where: { title: ILike("%Hello World%"), }, });
const posts = await postRepository.find({ where: { title: ILike("Hello World%"), }, });
const posts = await postRepository.find({ where: { title: ILike("%Hello World"), }, });
Sequelize
検索クエリが SQL ライクな書き方ができる。ただし弱い型付けである。
const post = await Post.findAll({ raw: true, where: { title: { [Op.like]: 'Hello', }, }, })
const post = await Post.findAll({ raw: true, where: { title: { [Op.like]: '%Hello%', }, }, })
const post = await Post.findAll({ raw: true, where: { title: { [Op.like]: '%Hello', }, }, })
const post = await Post.findAll({ raw: true, where: { title: { [Op.like]: 'Hello%', }, }, })
Prisma
検索クエリが JavaScript ライクな書き方ができる。強い型付けである。
const posts = await postRepository.find({ where: { title: "Hello World", }, });
const posts = await postRepository.find({ where: { title: { contains: "Hello World" }, }, });
const posts = await postRepository.find({ where: { title: { startsWith: "Hello World" }, }, });
const posts = await postRepository.find({ where: { title: { endsWith: "Hello World" }, }, });
マイグレーション
それぞれの ORM がどのようなマイグレーション機能を提供しているのか調査した。
TypeORM
typeorm migration:create -n PostRefactoring
マイグレーションファイルを作成。
typeorm migration:generate -n PostRefactoring
定義した entity をベースにマイグレーションファイルを作成。
typeorm migration:run
マイグレート。
typeorm migration:revert
ロールバック。
作成されるマイグレーションファイルは TypeScript で書かれており、up
down
メソッドが用意されており、
ロールバックに対応している。
また migration:generate
コマンドを使用すれば、SQL を手動で書かなくても自動で生成してくれる。
書き出されるコードには生の SQL があり、どのような SQL が実行されるか把握しやすくなっている。
import {MigrationInterface, QueryRunner} from "typeorm"; export class PostRefactoringTIMESTAMP implements MigrationInterface { async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "post" RENAME COLUMN "title" TO "name"`); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "post" RENAME COLUMN "name" TO "title"`); // reverts things made in "up" method } }
Sequelize
npx sequelize-cli db:migrate
でマイグレーションファイルを作成。
npx sequelize-cli db:migrate:undo
でロールバック。
作成されたファイルは JavaScript で書かれており、また up
, down
メソッドを用意しており、
ロールバックに対応している。
ただし、実行内容も JavaScript で書かれているため吐き出される SQL はブラックボックスである。
また Sequelize では差分を見て migration ファイルを生成してくれなさそうなので、手動で書くコストが発生する。
https://github.com/Scimonster/sequelize-auto-migrations というプラグインも存在しているが、スター数も低く更新も止まっている。
‘use strict’; module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.createTable(‘Users’, { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, firstName: { type: Sequelize.STRING }, lastName: { type: Sequelize.STRING }, email: { type: Sequelize.STRING }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: async (queryInterface, Sequelize) => { await queryInterface.dropTable(‘Users’); } };
Prisma
npx prisma migrate dev
でマイグレーションファイル作成&実行(開発環境)
npx prisma migrate reset
でデータベースをリセット(開発環境)
npx prisma migrate deploy
でマイグレート(本番環境)
Prisma では作成した prisma scheme をもとにマイグレーションファイルを作成する。 prisma scheme に変更が加わったとき、その差分を見てくれる。
また他 ORM と違いロールバック機能を備えていない。
その理由としては、ロールバック機能は多くの開発者が必要としないと考えたためらしい。 https://github.com/prisma/prisma/discussions/4617
トランザクションに対応しているか
すべてのORMでトランザクションは対応していることがわかった。 しかしPrismaではトランザクション分離レベルの変更を行うことができないため、 使用するDBで直接設定を行う必要がある。
TypeORM
TypeORMでは複数のトランザクションパターンを提供している。
- 従来のトランザクション
getManager
または getConnection
を使用しトランザクションを行う。
import {getManager} from "typeorm"; await getManager().transaction(async transactionalEntityManager => { await transactionalEntityManager.save(users); await transactionalEntityManager.save(photos); // ... });
- デコレーターを用いたトランザクション
TypeScriptのデコレーターを使用してトランザクションを行うことができる。
@Transaction({ isolation: "SERIALIZABLE" }) save(@TransactionManager() manager: EntityManager, user: User) { return manager.save(user); }
トランザクション分離レベル
設定可能。下記レベルを対応している。
READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
import {getManager} from "typeorm"; await getManager().transaction("SERIALIZABLE", transactionalEntityManager => { });
@Transaction({ isolation: "SERIALIZABLE" }) save(@TransactionManager() manager: EntityManager, user: User) { return manager.save(user); }
Sequelize
Sequelizeでは複数のトランザクションパターンを提供している。
- 手動トランザクション コミットまたはロールバックは手動でおこなう。
// First, we start a transaction and save it into a variable const t = await sequelize.transaction(); try { // Then, we do some calls passing this transaction as an option: const user = await User.create({ firstName: 'Bart', lastName: 'Simpson' }, { transaction: t }); await user.addSibling({ firstName: 'Lisa', lastName: 'Simpson' }, { transaction: t }); // If the execution reaches this line, no errors were thrown. // We commit the transaction. await t.commit(); } catch (error) { // If the execution reaches this line, an error was thrown. // We rollback the transaction. await t.rollback(); }
- 自動トランザクション エラーが発生した場合自動でロールバックを行う、成功した場合は自動でコミットされる。
try { const result = await sequelize.transaction(async (t) => { const user = await User.create({ firstName: 'Abraham', lastName: 'Lincoln' }, { transaction: t }); await user.setShooter({ firstName: 'John', lastName: 'Boothe' }, { transaction: t }); return user; }); // If the execution reaches this line, the transaction has been committed successfully // `result` is whatever was returned from the transaction callback (the `user`, in this case) } catch (error) { // If the execution reaches this line, an error occurred. // The transaction has already been rolled back automatically by Sequelize! }
トランザクション分離レベル
設定可能。
const { Transaction } = require('sequelize'); // The following are valid isolation levels: Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED // "READ UNCOMMITTED" Transaction.ISOLATION_LEVELS.READ_COMMITTED // "READ COMMITTED" Transaction.ISOLATION_LEVELS.REPEATABLE_READ // "REPEATABLE READ" Transaction.ISOLATION_LEVELS.SERIALIZABLE // "SERIALIZABLE"
const { Sequelize, Transaction } = require('sequelize'); const sequelize = new Sequelize('sqlite::memory:', { isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE });
Prisma
Prismaでは複数のトランザクションパターンを提供している。
- 関連したモデルの操作 複数の処理をネストし、対象のオブジェクトのCRUDメソッドに渡すことでトランザクション処理をすることができる。
const nestedWrite = await prisma.user.create({ data: { email: 'imani@prisma.io', posts: { create: [ { title: 'My first day at Prisma' }, { title: 'How to configure a unique constraint in PostgreSQL' }, ], }, }, })
- 独立したモデルの操作
const id = 9 // User to be deleted const deletePosts = prisma.post.deleteMany({ where: { userId: id, }, }) const deleteMessages = prisma.privateMessage.deleteMany({ where: { userId: id, }, }) const deleteUser = prisma.user.delete({ where: { id: id, }, }) await prisma.$transaction([deletePosts, deleteMessages, deleteUser]) // Operations succeed or fail together
トランザクション分離レベル
現在 Prismaでは対応していない。
PostgreSQLで行うには下記参照。 https://www.postgresql.org/docs/9.3/runtime-config-client.html#guc-default-transaction-isolation
ログのマスキングが可能か
すべてのORMで強引ではあるがログのマスキングは可能である。
ORMはログ出力のロジックをカスタマイズできるように設計されており、 ログをマスキングするロジックを持たせたロガーを設定することで上記を実現することができる。
TypeORM
TypeORMではデフォルトでAdvancedConsoleLoggerというロガーが使用され、 プリペアドステートメント形式のクエリとパラメーターを吐き出す。
SELECT "Post"."id" AS "Post_id", "Post"."title" AS "Post_title", "Post"."text" AS "Post_text" FROM "post" "Post" WHERE "Post"."text" = $1 -- PARAMETERS: ["password"]
このとき PARAMETERS に機密情報が含まれている可能性がある。 このような対策として下記のような独自のロガーを設定し、対象のパラメーターに対してマスキングを行う。
export class MaskingLogger extends AdvancedConsoleLogger { constructor(private keywords: string[] = []) { super(true); } logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { if (!parameters) super.logQuery(query, parameters, queryRunner); for (let keyword of this.keywords) { const re = new RegExp(`\"${keyword}\" = \\$(\\d*)`); const result = query.match(re); if (!result) continue; for (let index = 1; index < result.length; index += 1) { parameters[Number(result[index]) - 1] = "******"; } } super.logQuery(query, parameters, queryRunner); } } getConnectionOptions().then((connectionOptions) => { return createConnection( Object.assign(connectionOptions, { logger: new MaskingLogger(["text"]), }) ); });
SELECT "Post"."id" AS "Post_id", "Post"."title" AS "Post_title", "Post"."text" AS "Post_text" FROM "post" "Post" WHERE "Post"."text" = $1 -- PARAMETERS: ["******"]
また下記のようにPARAMETERS自体を吐き出さないようにすることも可能。
SELECT "Post"."id" AS "Post_id", "Post"."title" AS "Post_title", "Post"."text" AS "Post_text" FROM "post" "Post" WHERE "Post"."text" = $1
Sequelize
Sequelizeはクエリーとパラメーターが結合された状態でログに出力される。
SELECT "id", "firstName", "createdAt", "updatedAt" FROM "Users" AS "User" WHERE "User"."firstName" = 'credential'
下記のように独自のロガーを設定してマスキングを行う必要がある。
const logging = (query) => { // 正規表現などでqueryを書き換える console.log(query); } const sequelize = new Sequelize("sequelize", "tsurutan", "", { host: 'localhost', dialect: 'postgres', logging });
Prisma
PrismaではTypeORMと同様にクエリとパラメーターが分離している。
デフォルトではパラメーターは表示されず、プリペアドステートメント形式のクエリが出力される。
prisma:query SELECT “public”.“User”.“id”, “public”.“User”.“email”, “public”.“User”.“name” FROM “public”.“User” WHERE “public”.“User”.“email” = $1 OFFSET $2
マスキングを行ったパラメーターを合わせて表示させたい場合は下記のようなロガーを設定する。
prisma.$on("query", (query) => { // 正規表現をしようしてパラメーターのマスキング処理 const params = query.params; console.log(`${query}-${params}`); });
まとめ
基本的な機能についてはどれも対応しているが、TypeScript との互換性、保守性を考えた場合 Prisma を選択したほうが良いのではないかと考えた。 また TypeORM や Sequelize は従来の ORM でありクラスベースのマッピングを行う。 そのため返り値はインスタンスであるが、Prisma はこれら ORM と異なり Plain オブジェクトを返す。 この違いはアーキテクチャへ影響を与えるため重要であると感じられた。
また TypeScript を前提に作成された ORM であっても型の強さで違いがあり、Prisma に軍配が上がる。
枯れたサービスを使用したいのであれば Sequelize が良い。 Active Record パターンなどのクラスベースの設計を行いたいのであれば TypeORM。
歴史 Sequelize > TypeORM > Prisma
モデル定義 Prisma > TypeORM > Sequelize
TypeScript 互換性 Prisma > TypeORM > Sequelize
参考: