ORMで使用されるデザインパターンについてnpmパッケージを元に考える
#ORM #Node.js
概要
エンタープライズ アプリケーションアーキテクチャパターンを読み直してみたので、整理も兼ねて情報をまとめてみました
パターンについて
大抵のORMは、これらのうち複数のパターンを実装しています
テーブルデータゲートウェイ
DB上のテーブルへのアクセスを抽象化する
大抵のORMでこのパターンは使用されている
DBへのアクセスを抽象化することで、例えば、SQLを書かずにクエリが実行できたり、RDBMS間の差異を吸収することなどができます
code:prisma.ts
const user = await prisma.user.findUnique({
where: { id: 1234 },
});
await prisma.user.update({
where: { id: 1234 },
data: { name: "foobar" },
});
行データゲートウェイ
DBのレコードと1:1に紐づき、永続化ロジックを提供するオブジェクトを実装するパターン
code:javascript
const user = await User.find(1234);
user.name = "foobar";
await user.save();
Active Record
※少しややこしいですが、railsのactiverecordはデザインパターンの名前がそのままライブラリ名として採用されています。
Model(ここではリレーションにマッピングされるオブジェクトの意味)が永続化に関する知識を持つ
行データゲートウェイとの違いはドメインロジックの有無で、Active Recordは行データゲートウェイが備える永続化の役割に加えて、ドメインロジックも提供するのが特徴 (ドメインモデルと行データゲートウェイの両方のパターンを実装したものがActive Recordのイメージ)
code:sequelize.js
const user = await User.findByPk(1234); // 永続化に関する知識を持つ
user.changeName("foobar"); // ドメインロジック
await user.save(); // 永続化に関する知識を持つ
Data Mapper
Model(リレーションにマッピングされるオブジェクト)はドメインロジックを持つものの、永続化に関する知識は持たないのがActive Recordとの違い
ORMの目的であるインピーダンスミスマッチを解消するという点においては、ここで紹介した各パターンの中において、最も理想的なパターンではないかと思います
ただし、ドメインと永続化を完全に分離することは難しくて、大抵のORMではある程度妥協されている場合がほとんどだと思います
例えば、TypeORMでは、マッピング情報をデコレータまたはEntity Schemaのいずれかの方法で定義できます
このうちデコレータを使ってマッピングを定義した場合、Modelが永続化に関する知識(TypeORMに対する依存)を持つことになるため、完全にドメインと永続化のレイヤーを分離できているわけではなくなります
MikroORMの場合、*:N関係を表現するのに独自のCollection型を使用する必要があり、ドメインモデルからORMへの依存ができる
永続化に関する責務は、ORMにもよりますが、EntityManagerやRepositoryのような名前のクラスで提供されることが多い印象です (これらのクラスは、ほとんどの場合、テーブルデータゲートウェイパターンを実装しているはずです)
code:typeorm.ts
const userRepository = dataSource.getRepository(User);
const user = await userRepository.findOneBy({ id: 1234 });
user.changeName("foobar");
await userRepository.save(user); // Modelは永続化に関する知識を持たない
Unit of Work
MikroORMが実装しているパターン
各ORMが実装しているパターンについて
TypeORM - Data Mapper/Active Record/テーブルデータゲートウェイ/行データゲートウェイ
MikroORM - Data Mapper/Unit of Work/テーブルデータゲートウェイ
Sequelize - Active Record/テーブルデータゲートウェイ/行データゲートウェイ
Prisma - テーブルデータゲートウェイ (※Prisma Client extensionsを活用すれば、Active Recordパターンなども実装可能)
Drizzle ORM - テーブルデータゲートウェイ
結局どのパターンがいいの?
ビジネスロジックをどのように実装するかにもよる
トランザクションスクリプトによって実装する場合は、テーブルデータゲートウェイまたは行データゲートウェイが手軽
特にPrismaはAPIもシンプルで学習コストも他のORMと比較しても低めだと思うため、生産性もかなり意識して作られていると思うため、とにかく動くものを素早く作りたいケースにおいては向いているはず
ドメインモデルパターンによって実装する場合は、Data MapperかActive Recordが相性が良いでしょう
Active RecordとData Mapperを比較した場合、関心の分離の観点からすると、Active RecordよりもData Mapperの方が理想的です
ただ、Rails製の巨大なサービスも世の中には数多くあるように、Active Recordパターンを採用したからといって、メンテナンス不可能になるわけではないです
きちんとテストが書かれて自動化されていれば、結局どちらのパターンを使用したとしてもメンテナンスは十分可能だと思います。
なので、この2つのパターンについては、対象ドメインの複雑度によってどちらを使うかを決めるとよいのではないかと思います
また、チーム内にRailsのスキルを持ったメンバーが多く在籍しているような場合は、Railsでactiverecordを使って実装した方が、生産性やメンテナンス性の面では実は有利かもしれません (最近、何かと批判されがちな印象は感じるものの、Railsは十分な実績や使用率もあり、とても優れたフレームワークだと自分は思っています)
最終的にはどのパターンを採用するにせよ、保守性の観点からすると、結局はテストの自動化やCIとかの仕組みとかが重要なのではないかと思います
参考
エンタープライズ アプリケーションアーキテクチャパターン
What's the difference between Active Record and Data Mapper?
関連ページ
ORMって一体なんなの?
Node.jsのORMについて
DenoのORMについて