本記事のゴール

Prismaで吐き出されたTypescriptの型ファイルに、独自のカラム(キー)を追加したうえで APIから結果を受け取り、以後拡張したクラスのオブジェクトとして、色々なところで使いたい。


なお、弊ブログではPrismaについて以下の記事も執筆しておりますのでよろしければご覧になってください。

参考記事: サーバがなくてもMySQLが使える!PlanetScaleとNext.jsを組み合わせ速攻でDBに接続する方法。

現状の問題点

Prismaはさまざまなデータベースシステムの型ファイルを出力してくれ、Typescript環境のプロジェクトで利用するのに それはそれはとても便利なORM(ORマッパー)ライブラリ...なのですが、DBの現状を

npx prisma pull

で、ローカルのschema.prismaに反映し、

npx prisma generate

で型ファイルとして出力する...という機械的な手順で反映する ため、場合によっては、DBにないカラムをクラスに追加できないということになりかねません。

前提
src
├── app
│   └── user
│       └── page.tsx
├── models
│   └── optionals
│       └── user_with_job.ts // 今回作成するPrismaのクラスを拡張したクラス
└── lib
    └── repositories
        ├── apiHelper.ts
        └── user.ts // http://apidomain/api/userへリクエスト

解決策1: APIから結果を受け取る

一番シンプルなのは、以下のようにaxiosで受け取るレスポンスのキーを2つにしてしまうことです。

解決策1のレポジトリー層
src/lib/repositories/user.ts
import { axiosWithBearerToken } from '@/lib/apiHelper';
import { User } from '@prisma/client';
import axios, { AxiosResponse } from 'axios';

export async function getUser(): Promise<{
  user: User,
  job: number
}> {
  try {
    const axiosInstance = axiosWithBearerToken();
    const response: AxiosResponse<
    {
      user: User, 
      job: number
    }> = await axiosInstance.get('/user');

    const { user, job } = response.data

    return { user, job }
  } catch (error) {
    console.error(error);
    throw error;
  }
}
解決策1のレポジトリー層・ベースクラス

リクエストを行うaxiosを呼び出す処理はapiHelper.tsに集約し、さまざまな処理で使えるようにしておきます。

は、Next.jsプロジェクトのルートディレクトリに置いた.envに実際に使うものを記述してください。(BEARER_TOKENが不要な場合はこの行をコメントアウトしてください。)

src/lib/repositories/apiHelper.ts
import axios from 'axios';

export const axiosWithBearerToken = () => {
  const instance = axios.create({
    baseURL: process.env.API_BASE_URL,
    headers: {
      Authorization: `Bearer ${process.env.BEARER_TOKEN}`,
    },
  });

  return instance;
};

prismaで自動的に吐かれた、userの型は以下のようなキー構成となります。(実際には関数ジャンプで飛べる index.d.tsはずっと複雑ですがわかりやすさを重視し、例示しています。)

export type User = {
  id: number
  name: string | null
  created_at: Date | null
  updated_at: Date | null
}
解決策1のページファイル

ページファイルでは以下のように使用します。

src/app/page.tsx
import { User } from "@prisma/client"
import { getUser } from "@/repositories/places"

const Page = async () => {

  let user: User | null = null;
  let job: number | null = null;

   try {
    const response = await getUser();
    if (response) {
      job = response.job;
      user = response.user
    }
  } catch (e) {
    console.error(e);
  }

  if (user == null) {
    return <main>ロード中...</main>
  }

  return (
    <main>
      ユーザー名: {user.name}
      職業番号: {user.job}
    </main>
  )
}

export default Page

Prisma Migrateで自動生成されたキー以外は、クラスとは別の変数で管理することになり、キーを必要に応じて気軽に追加しやすい 反面、数が増加する一方になるので、データを渡したいコンポーネントがある場合、引数が増えるほど保守性が落ちることになりかねません。


下記のような感じですね。(パラメータがずらずらと...)

let user: User | null = null;
let job: number | null = null;
let sex: number | null = null;
let yearsOld: number | null = null;
// 以下も増えていく!

try {
  const response = await getUser();
  if (response) {
    job = response.job
    user = response.user
    sex = response.sex
    yearsOld = response.yearsOld
    // 以下も増えていく!
  }
} catch (e) {
  console.error(e);
}

// 中略...

<TestComponent user={user} job={job} sex={sex} yearsOld={yearsOld} {/*追加するほど後に続く!*/}/>

こちらを解決するのが下で解説する、2.の方法となります。

解決策2: 「拡張」クラスを作成する。

便利なPrisma Migrateが利用できなくなるため、Prismaの型を直接変更するわけにはいかない。一方で、引き回す変数は極力少なくしたい...

これを実現するのがもとのPrismaで生成したクラスを「拡張」する方法です。

先ほどと同じ、userクラスでにjobというパラメータを入れる事例で説明しますと以下のようになってきます。

解決策2のレポジトリー層

戻り値の型指定がよりシンプルになりましたし、行数も少なくできて、メンテナンス性も向上したことがお分かりいただけると思います。

src/lib/repositories/user.ts
import { axiosWithBearerToken } from '@/lib/apiHelper';
import { UserWithJob } from '@/models/options/user_with_job';
import axios, { AxiosResponse } from 'axios';

export async function getUser(): Promise<user: UserWithJob> {
  try {
    const axiosInstance = axiosWithBearerToken();
    const response: AxiosResponse<UserWithJob> = await axiosInstance.get('/user');
    return response.data
  } catch (error) {
    console.error(error);
    throw error;
  }
}
解決策2のレポジトリー層・ベースクラス

上の、apiHelper.tsと同じものを使います。

解決策2の拡張したクラス定義

src/models/optionals/user_with_job.tsを新たに作り、prismaで出力されたuserを拡張します。こうすることでMVCフレームワークで申しますと、Controllerにあたるページファイルに、拡張定義をしなくてよくなります。

src/models/optionals/user_with_job.ts
import { user } from "@prisma/client";

export type UserWithJob = user & { job: number
// 必要なものがあればここに追加して拡張していく。
};
解決策2のページファイル

変更後のページファイルは以下になります。

src/app/page.tsx
import { UserWithJob } from "@/models/optionals/user_with_job.ts";

const Page = async () => {

  let user: UserWithJob | null = null;

   try {
    const user = await getUser();
  } catch (e) {
    console.error(e);
  }

  if (user == null) {
    return <main>ロード中...</main>
  }

  return (
    <main>
      ユーザー名: {user.name}
      職業番号: {user.job}
    </main>
  )
}

export default Page

まとめ

Prismaを実用的に使っていて、今回の問題に当たりました。商用のプロジェクトで保守性をどう維持しながら開発するか、もっというといかに適度にリファクタリングしながら開発していくか。 というのはどのフレームワークを使っていても必ず起こる問題ですし、意外にPrismaにおいてこれ関連のポストが少ないと思ったので今回まとめてみた次第でした。


この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!