ORMで使用されるデザインパターンについてnpmパッケージを元に考える
概要
パターンについて
大抵のORMは、これらのうち複数のパターンを実装しています DB上のテーブルへのアクセスを抽象化する
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();
Model(ここではリレーションにマッピングされるオブジェクトの意味)が永続化に関する知識を持つ
code:sequelize.js
const user = await User.findByPk(1234); // 永続化に関する知識を持つ
user.changeName("foobar"); // ドメインロジック
await user.save(); // 永続化に関する知識を持つ
Model(リレーションにマッピングされるオブジェクト)はドメインロジックを持つものの、永続化に関する知識は持たないのがActive Recordとの違い ORMの目的であるインピーダンスミスマッチを解消するという点においては、ここで紹介した各パターンの中において、最も理想的なパターンではないかと思います ただし、ドメインと永続化を完全に分離することは難しくて、大抵のORMではある程度妥協されている場合がほとんどだと思います このうちデコレータを使ってマッピングを定義した場合、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は永続化に関する知識を持たない
各ORMが実装しているパターンについて
結局どのパターンがいいの?
ビジネスロジックをどのように実装するかにもよる
特にPrismaはAPIもシンプルで学習コストも他のORMと比較しても低めだと思うため、生産性もかなり意識して作られていると思うため、とにかく動くものを素早く作りたいケースにおいては向いているはず きちんとテストが書かれて自動化されていれば、結局どちらのパターンを使用したとしてもメンテナンスは十分可能だと思います。
なので、この2つのパターンについては、対象ドメインの複雑度によってどちらを使うかを決めるとよいのではないかと思います
また、チーム内にRailsのスキルを持ったメンバーが多く在籍しているような場合は、Railsでactiverecordを使って実装した方が、生産性やメンテナンス性の面では実は有利かもしれません (最近、何かと批判されがちな印象は感じるものの、Railsは十分な実績や使用率もあり、とても優れたフレームワークだと自分は思っています) 最終的にはどのパターンを採用するにせよ、保守性の観点からすると、結局はテストの自動化やCIとかの仕組みとかが重要なのではないかと思います
参考
関連ページ