はじめに
複数のアプリケーションで同一のデータ基盤を再利用したい——この要求は、多くのプロジェクトで生じる典型的なニーズです。
静岡大学情報学部 ITソリューション室では、室員の情報を一元管理する「室員データベース」を様々なWebアプリケーションから手軽に活用するため、shizuoka-its/core
という共通基盤パッケージを開発してきました。
初期は、レイヤードアーキテクチャやDIパターンを「なんとなく」適用し、Prismaが本来提供しているデータアクセス(リポジトリ的機能)をわざわざ再実装したり、柔軟なリレーション取得を支えるためにGraphQL的な抽象化を再発明しそうになりました。結果として、過度な抽象化がかえって開発・保守を複雑にし、「車輪の再発明」に陥ったのです。
本記事では、
- なぜPrismaが既に「リポジトリ」的役割を果たすのに再実装してしまったのか
- なぜ柔軟なユースケース対応を追求した結果、GraphQL的な型定義・クエリモデルを再構築しそうになったのか
- v0(サービスレイヤー過剰) → v1(スキーマのみ) → v2(再度サービス導入)の変遷を経て、どのように「本当に必要なレイヤー」を見極めるようになったのか
これらを振り返り、Prisma・GraphQL・サービスレイヤーの正しい役割分担と、過剰な抽象化を避けるための指針を紹介します。
背景:共通基盤開発の理想と現実
ITソリューション室では、Discord連携を通じ全室員データが一元化されており、展示作品管理ツールやイベント運用システムなど、様々なWebアプリでこのデータを利用します。
「npm install一発でユーザーデータに型安全にアクセスできる共通基盤があれば、開発効率が上がる」という発想からshizuoka-its/core
が誕生しました。
v0期:レイヤーパターンの雰囲気適用による過剰抽象化
最初の実装(v0)では、なんとなく「きれいそう」という理由でリポジトリ層・サービス層を導入しました。しかしここで問題が発生します。
Prismaが既に「リポジトリ的機能」を提供している
PrismaはDBアクセスに対して、
- 型安全なクエリインターフェース
- 柔軟なリレーション取得(`include`句による深いネスト対応)
- CRUD操作を標準化
といった機能を提供します。これらは典型的な「リポジトリパターン」の要件(ドメインモデルへのアクセス抽象化)を多くカバーしています。
にもかかわらず私たちは、Prismaの上にさらに独自のリポジトリクラスやインターフェースを重ね、既存の機能を再抽象化してしまいました。結果として、単純なデータ取得すら回りくどくなり、N+1問題の誘発やパフォーマンス低下、メンテナンスコスト増大といった副作用が生まれました。
GraphQL的抽象化の再発明
また、室員データにはDiscordアカウント、イベント参加、展示作品など複数のリレーションが存在します。多彩なユースケースに柔軟対応しようとした結果、「このメソッドは特定のリレーションをinclude」「あのメソッドは別の関連をfetch」といったパターンが乱立。型定義やメソッド設計が複雑化する中、「まるでGraphQLスキーマを自前で作り、特定クエリで必要なフィールドを返す構造を定義するような作業」に陥りました。
GraphQLは、API境界で柔軟な型定義とクエリ選択を可能にする仕組みです。Prismaと独自レイヤーによる柔軟化は、GraphQLが得意とする「必要なデータのみを取得」モデルを、ORM層で無理に再構築しているような状況でした。
v1期:抽象化撤廃によるシンプル化
この気づきを経て、v1では思い切って抽象化を取り除きました。shizuoka-its/core
はPrismaのschema.prisma
と自動生成される型定義(Prisma Client)のみを提供し、アプリ側がPrismaを直接利用してデータ取得を行う方針へ転換しました。
// v1: Prisma本来の力を活用
const member = await prisma.member.findUnique({
where: { id: "member-1" },
include: {
discordAccount: true,
events: {
include: {
event: true,
exhibits: { include: { members: true } }
}
}
}
});
こうすることで、柔軟なクエリ設計はPrismaが標準で提供する機能で完結し、煩雑なレイヤーは不要に。GraphQL的な抽象化の再実装も回避でき、N+1問題やパフォーマンス低下といった課題も軽減されました。
v2期:改めてサービスレイヤー導入を考える理由
「シンプル化」にしたv1を経た現在、v2では再度サービスレイヤーの導入を検討しています。「なぜまたサービス?」と思うかもしれません。
ここでのポイントは、v0でのサービス導入目的とv2でのサービス導入目的が違うことです。
• v0:何となくの設計美学でサービスやリポジトリを積み、Prismaが既に解決しているデータアクセス問題を再発明してしまった。
• v2:Prismaはあくまでデータアクセスをシンプルにするツールであり、業務ロジックや複雑なユースケース(例えば、イベント参加時の複合的なバリデーションや整合性維持、複数ドメインオブジェクト間の整合チェック)をまとめる場としてサービスレイヤーを使う。
つまり、サービスレイヤーはビジネスルールのカプセル化やトランザクション管理、複数エンティティ操作の一元化といった、本来のドメインロジック集約のために活用します。Prismaが「リポジトリ的機能」を提供する一方、ドメインサービスは「ビジネスロジックを定義する」役割を果たし、両者は明確な責務分離が可能です。
得られた教訓と最適なバランス
この変遷から得た教訓は以下のとおりです。
1. Prismaの標準機能を最大限活用する
Prismaは高水準のデータアクセス抽象化を提供します。これを無視して独自リポジトリを作ると二重抽象化となり、複雑性が増すだけです。
2. GraphQL的問題はGraphQLで解決する
柔軟なフィールド取得や型定義は、GraphQLという別のレイヤーで解決されるべき課題です。ORM層で無理に同様の抽象化を再発明する必要はありません。
3. サービスレイヤーは本来の目的で使う
ドメインルールや複雑なロジックが必要な場合にのみサービス層を導入することで、責務分離を明確にできます。データ取得そのものはPrismaに任せ、サービスはビジネスロジック専用とすることで、過剰な再発明を避けられます。
4. 段階的な進化が有効
最初から完璧な抽象化を求めず、v0→v1→v2と段階的に設計を見直すことで、本当に必要なものが見えてきます。ツール(Prisma、GraphQL)の特性とドメイン要件を理解した上で、必要な抽象化レベルを適宜再考しましょう。
まとめ
• v0では「なんとなく」の設計でPrismaの機能を再発明し、複雑化。
• v1で抽象化を剥がし、Prisma本来の力を活かしてシンプル化。
• v2では、この学びを踏まえ、本当に必要なビジネスロジック抽象化(サービスレイヤー)を再導入。
本記事で紹介した経験は、多くの開発現場で起こりうる「過剰抽象化」と「車輪の再発明」を回避するヒントになるはずです。
PrismaやGraphQLといった強力なツールを活用し、本来の役割を見極めながら、最小限かつ必要十分な抽象化を行うことで、健全で拡張しやすいアーキテクチャを実現できるでしょう。