TypeORM vs Sequelize vs Prisma

Node.js + TypeScriptでどのORMを選択すればよいか調査を行いました。

概要

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つの方法を提供している。

  1. Log to stdout

対象となるイベント名を引数の log に指定することで標準出力することができる。

const prisma = new PrismaClient({ log: ['query', 'info', `warn`, `error`], })
  1. 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

  1. 単体指定
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Index() @Column() firstName: string; @Column() @Index() lastName: string; }
  1. 複数指定
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; }
  1. 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 インデックスの種類などを指定することも可能。

  1. 単体指定
const User = sequelize.define('User', { /* attributes */ }, { indexes: [ { fields: ['email'] }, ] });
  1. 複数指定
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' } }, ] });
  1. 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

https://typeorm.io/#/

Sequelize

https://sequelize.org/master/

Prisma

https://www.prisma.io/

モデルの定義

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では複数のトランザクションパターンを提供している。

  1. 従来のトランザクション

getManager または getConnection を使用しトランザクションを行う。

import {getManager} from "typeorm"; await getManager().transaction(async transactionalEntityManager => { await transactionalEntityManager.save(users); await transactionalEntityManager.save(photos); // ... });
  1. デコレーターを用いたトランザクション

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では複数のトランザクションパターンを提供している。

  1. 手動トランザクション コミットまたはロールバックは手動でおこなう。
// 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(); }
  1. 自動トランザクション エラーが発生した場合自動でロールバックを行う、成功した場合は自動でコミットされる。
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では複数のトランザクションパターンを提供している。

  1. 関連したモデルの操作 複数の処理をネストし、対象のオブジェクトの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' }, ], }, }, })
  1. 独立したモデルの操作
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

参考:

©Tsurutan. All Rights Reserved.