Martin Fowlerが『PoEAA』で紹介したIdentity Mapパターンは、同一トランザクション内で同じレコードを複数回読み込んだとき、同一のオブジェクトを返すことを保証します。しかしPrismaにはこの仕組みがありません。これは問題なのでしょうか。
Identity Mapとは何か
Fowlerの定義を確認しましょう。
An Identity Map keeps a record of all objects that have been read from the database in a single business transaction. Whenever you want an object, you check the Identity Map first to see if you already have it.
Identity Mapは、1回のビジネストランザクション内でデータベースから読み込んだすべてのオブジェクトを記録します。オブジェクトが必要になったとき、まずIdentity Mapを確認し、すでに持っているかどうかをチェックします。
このパターンには2つの目的があります。
- パフォーマンス向上 - 同じレコードを複数回フェッチしない
- 整合性の保証 - 同じレコードへの参照が同一オブジェクトを指す
ActiveRecordはIdentity Mapを持っているか
Ruby on RailsのActiveRecordは、実は完全なIdentity Mapを持っていません。ただし、部分的に似た挙動があります。
# Railsでの挙動
user1 = User.find(1)
user2 = User.find(1)
user1.object_id == user2.object_id # => false(別オブジェクト)
# しかし、associationのキャッシュはある
post = Post.includes(:user).find(1)
post.user.object_id == post.user.object_id # => true(同一オブジェクト)
Railsはリクエストスコープでの完全なIdentity Mapを実装していません。一度は実装されましたが、複雑さとの兼ね合いで削除された経緯があります。
Prismaの挙動
Prismaは明確にIdentity Mapを持っていません。
const user1 = await prisma.user.findUnique({ where: { id: 1 } });
const user2 = await prisma.user.findUnique({ where: { id: 1 } });
user1 === user2; // => false(別オブジェクト)
同じIDで2回クエリを発行すると、2回データベースにアクセスし、別々のオブジェクトが返されます。
これは問題なのか
Identity Mapがないことで生じる問題を考えてみましょう。
問題1. パフォーマンス
同じレコードを何度もフェッチするとパフォーマンスが劣化します。
// N+1的な問題
for (const order of orders) {
const user = await prisma.user.findUnique({
where: { id: order.userId }
});
// 同じuserIdが複数回出現すると、同じクエリが複数回走る
}
ただし、これはIdentity Mapがなくても解決可能です。
// 事前にまとめてフェッチする
const userIds = [...new Set(orders.map(o => o.userId))];
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});
const userMap = new Map(users.map(u => [u.id, u]));
for (const order of orders) {
const user = userMap.get(order.userId);
}
問題2. オブジェクトの同一性
より深刻なのは、同じレコードが別オブジェクトとして存在することによる不整合です。
async function processOrder(orderId: string) {
const order = await prisma.order.findUnique({ where: { id: orderId } });
// 別の場所で同じorderを取得
const sameOrder = await prisma.order.findUnique({ where: { id: orderId } });
order.status = 'PROCESSING';
// sameOrderのstatusは変わらない
await prisma.order.update({
where: { id: orderId },
data: { status: order.status }
});
// この時点でsameOrderは古いデータを持っている
console.log(sameOrder.status); // まだ古い値
}
Prismaの設計思想
PrismaがIdentity Mapを実装しない理由は、設計思想の違いにあると考えられます。
ステートレスなクエリビルダー
Prismaは「ステートレスなクエリビルダー」として設計されています。各クエリは独立しており、前のクエリの結果を覚えていません。これにより、動作が予測しやすくなります。
イミュータブルなデータ
Prismaが返すオブジェクトは、事実上イミュータブルとして扱うことが想定されています。取得したデータを変更するのではなく、updateやcreateで新しい状態をデータベースに書き込むスタイルです。
// Prismaの想定する使い方
const user = await prisma.user.findUnique({ where: { id: 1 } });
// オブジェクトを変更するのではなく、updateで新しい状態を書き込む
await prisma.user.update({
where: { id: 1 },
data: { name: 'New Name' }
});
Identity Mapが必要なケースへの対処
それでもIdentity Mapが欲しい場面はあります。その場合、自前で実装するか、設計で回避するかの選択になります。
自前でIdentity Mapを実装する
class IdentityMap<T extends { id: string }> {
private map = new Map<string, T>();
get(id: string): T | undefined {
return this.map.get(id);
}
set(entity: T): void {
this.map.set(entity.id, entity);
}
getOrFetch(id: string, fetcher: () => Promise<T | null>): Promise<T | null> {
const cached = this.get(id);
if (cached) return Promise.resolve(cached);
return fetcher().then(entity => {
if (entity) this.set(entity);
return entity;
});
}
}
// 使用例
const orderMap = new IdentityMap<Order>();
const order1 = await orderMap.getOrFetch(orderId, () =>
orderRepository.findById(orderId)
);
const order2 = await orderMap.getOrFetch(orderId, () =>
orderRepository.findById(orderId)
);
order1 === order2; // => true
設計で回避する
多くの場合、Identity Mapがなくても問題にならないように設計できます。
- 1つのUseCaseで同じエンティティを複数回取得しない - 最初に取得して使い回す
- 取得したオブジェクトを変更しない - 変更は常にDBへの書き込みで行う
- 短いトランザクションを保つ - 長時間オブジェクトを保持しない
// 良い例:一度だけ取得して使い回す
async function approveOrder(orderId: string, approverId: string) {
const order = await orderRepository.findById(orderId);
const approver = await userRepository.findById(approverId);
// orderとapproverを使って処理
const result = order.approve(approver);
await orderRepository.save(order);
return result;
}
まとめ
FowlerがIdentity Mapで解決しようとした問題は今でも存在します。しかし、現代のORMはそれぞれ異なるアプローチを取っています。
Prismaの場合、Identity Mapがないことは設計上の欠陥ではなく、ステートレスでシンプルなモデルを選択した結果です。この制約を理解した上で、必要なら自前で実装するか、設計で回避するかを選べばよいでしょう。
重要なのは、使っているツールがどのような設計思想を持っているかを理解し、その制約の中で適切な設計を選ぶことです。
参考文献
- Martin Fowler『Patterns of Enterprise Application Architecture』(2002)
- Disable identity map by default