本記事のゴール
Prismaで吐き出されたTypescriptの型ファイルに、独自のカラム(キー)を追加したうえで APIから結果を受け取り、以後拡張したクラスのオブジェクトとして、色々なところで使いたい。
なお、弊ブログではPrismaについて以下の記事も執筆しておりますのでよろしければご覧になってください。
参考記事: サーバがなくてもMySQLが使える!PlanetScaleとNext.jsを組み合わせ速攻でDBに接続する方法。
現状の問題点
Prismaはさまざまなデータベースシステムの型ファイルを出力してくれ、Typescript環境のプロジェクトで利用するのに それはそれはとても便利なORM(ORマッパー)ライブラリ...なのですが、DBの現状を
npx prisma pull
で、ローカルのschema.prisma
に反映し、
npx prisma generate
で型ファイルとして出力する...という機械的な手順で反映する ため、場合によっては、DBにないカラムをクラスに追加できないということになりかねません。
前提
-
追加するキーの名称は「job」とします。1: 正社員 2: パート 3: アルバイト のように数字が入ります。
-
本記事ではPrisma Clientを使わずRestfulAPI を呼ぶ想定です。
Prisma Clientも、Javascriptのライブラリのように、DBレイヤを意識せずに(SQLなども使わずに)、結果を呼び出すことができてとても便利です。が、今回は既存のRestfulAPIを活かす形で、あくまでPrismaはPrisma Migrateのみを使用します。 -
Next.js 13を使用したプロジェクトで、axiosを利用しRestfulAPIにリクエストをかけています。
-
プロジェクト構成は以下のようにしています。(掲題に関係ない部分は省略。)今回はデザインパターンのうち、レポジトリーパターンを使って実装します。
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のレポジトリー層
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
に集約し、さまざまな処理で使えるようにしておきます。
- API_BASE_URL
- BEARER_TOKEN
は、Next.jsプロジェクトのルートディレクトリに置いた.env
に実際に使うものを記述してください。(BEARER_TOKENが不要な場合はこの行をコメントアウトしてください。)
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のページファイル
ページファイルでは以下のように使用します。
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のレポジトリー層
戻り値の型指定がよりシンプルになりましたし、行数も少なくできて、メンテナンス性も向上したことがお分かりいただけると思います。
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にあたるページファイルに、拡張定義をしなくてよくなります。
import { user } from "@prisma/client";
export type UserWithJob = user & { job: number
// 必要なものがあればここに追加して拡張していく。
};
解決策2のページファイル
変更後のページファイルは以下になります。
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においてこれ関連のポストが少ないと思ったので今回まとめてみた次第でした。
この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!