5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【失敗備忘録】あいまいなレイヤー設計によるPrisma機能の二重抽象化でGraphQL的抽象化を再発明してしまった話

Last updated at Posted at 2024-12-01

はじめに

複数のアプリケーションで同一のデータ基盤を再利用したい——この要求は、多くのプロジェクトで生じる典型的なニーズです。
静岡大学情報学部 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といった強力なツールを活用し、本来の役割を見極めながら、最小限かつ必要十分な抽象化を行うことで、健全で拡張しやすいアーキテクチャを実現できるでしょう。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?