Prisma Client Extensions で拡張機能を追加する

Prisma Client Extensions は、Prisma クライアントにカスタム機能を追加する強力な機能です。従来の Prisma 開発では、ビジネスロジックが散らばりがちで、コードの重複や保守性の低下に悩まされることが多かったのではないでしょうか。
この記事では、Prisma Client Extensions を使うことで、どのようにしてこれらの課題を解決し、より保守性の高いコードを書けるようになるかを実践的な例を通じて学んでいきます。特に、実際のプロジェクトでよく遭遇する課題とその解決策に焦点を当て、あなたの開発効率を劇的に向上させる方法をお伝えします。
Prisma Client Extensions とは
Prisma Client Extensions は、Prisma クライアントにカスタム機能を追加するための公式機能です。これにより、データベース操作に独自のロジックを組み込むことができ、アプリケーション全体の一貫性と保守性を大幅に向上させることができます。
基本的な仕組み
Prisma Client Extensions は、Prisma クライアントの生成時に拡張機能を適用する仕組みです。スキーマファイルで拡張機能を定義し、Prisma クライアントが生成される際に、それらの機能が自動的に組み込まれます。
typescript// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["clientExtensions"]
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
従来の方法との比較
従来の Prisma 開発では、ビジネスロジックを以下のような方法で実装していました:
従来のアプローチ(問題のある例)
typescript// ユーザー作成時にタイムスタンプを手動で設定
const createUser = async (data: CreateUserInput) => {
const user = await prisma.user.create({
data: {
...data,
createdAt: new Date(), // 毎回手動で設定
updatedAt: new Date(),
},
});
return user;
};
// 別の場所でも同じ処理を繰り返す
const updateUser = async (
id: number,
data: UpdateUserInput
) => {
const user = await prisma.user.update({
where: { id },
data: {
...data,
updatedAt: new Date(), // 重複したコード
},
});
return user;
};
この方法では、タイムスタンプの設定が複数の場所に散らばり、一貫性を保つのが困難になります。また、新しいフィールドを追加する際も、すべての関連箇所を修正する必要があります。
拡張機能の種類と実装方法
Prisma Client Extensions には、4 つの主要な拡張機能があります。それぞれの特徴と実装方法を見ていきましょう。
フィールド拡張
フィールド拡張は、既存のフィールドに計算された値や変換された値を追加する機能です。データベースに保存されない仮想的なフィールドを作成できます。
typescript// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["clientExtensions"]
}
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
}
// フィールド拡張の定義
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
// 計算フィールドの追加
@@extend({
fields: {
fullName: {
type: "String",
compute: (user) => `${user.firstName} ${user.lastName}`
},
isNewUser: {
type: "Boolean",
compute: (user) => {
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
return user.createdAt > oneWeekAgo;
}
}
}
})
}
この拡張により、以下のように使用できます:
typescript// 拡張フィールドの使用
const user = await prisma.user.findUnique({
where: { id: 1 },
});
console.log(user.fullName); // "田中 太郎"
console.log(user.isNewUser); // true or false
モデル拡張
モデル拡張は、モデルにカスタムメソッドを追加する機能です。ビジネスロジックをモデルレベルでカプセル化できます。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
@@extend({
methods: {
// ユーザーの年齢を計算するメソッド
getAge: {
type: "Int",
args: [],
implementation: (user) => {
const today = new Date();
const birthDate = new Date(user.birthDate);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
},
// ユーザーを更新するメソッド
updateProfile: {
type: "User",
args: [
{ name: "data", type: "UpdateUserInput" }
],
implementation: async (user, args, context) => {
return await context.prisma.user.update({
where: { id: user.id },
data: {
...args.data,
updatedAt: new Date()
}
});
}
}
}
})
}
クライアント拡張
クライアント拡張は、Prisma クライアント全体に機能を追加する機能です。すべてのモデルで共通して使用できる機能を実装できます。
typescript// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["clientExtensions"]
}
// クライアント拡張の定義
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
}
// クライアント全体の拡張
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
@@extend({
client: {
// ログ機能の追加
log: {
type: "Void",
args: [
{ name: "message", type: "String" },
{ name: "level", type: "LogLevel" }
],
implementation: (args) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${args.level}: ${args.message}`);
}
},
// データベース接続の健康チェック
healthCheck: {
type: "Boolean",
args: [],
implementation: async (args, context) => {
try {
await context.prisma.$queryRaw`SELECT 1`;
return true;
} catch (error) {
return false;
}
}
}
}
})
}
クエリ拡張
クエリ拡張は、特定のクエリ操作にカスタムロジックを追加する機能です。データの取得や更新時に自動的に処理を実行できます。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
createdAt DateTime @default(now())
@@map("users")
@@extend({
queries: {
// ユーザー作成時の自動処理
create: {
args: [
{ name: "data", type: "UserCreateInput" }
],
implementation: async (args, context) => {
// データ検証
if (!args.data.email.includes('@')) {
throw new Error('Invalid email format');
}
// タイムスタンプの自動設定
const dataWithTimestamps = {
...args.data,
createdAt: new Date(),
updatedAt: new Date()
};
return await context.prisma.user.create({
data: dataWithTimestamps
});
}
},
// ユーザー更新時の自動処理
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
const dataWithTimestamp = {
...args.data,
updatedAt: new Date()
};
return await context.prisma.user.update({
where: args.where,
data: dataWithTimestamp
});
}
}
}
})
}
具体的な実装例
実際のプロジェクトでよく使用される拡張機能の実装例を見ていきましょう。
自動タイムスタンプ機能
データの作成・更新時に自動的にタイムスタンプを設定する機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
const now = new Date();
const dataWithTimestamps = {
...args.data,
createdAt: now,
updatedAt: now
};
return await context.prisma.user.create({
data: dataWithTimestamps
});
}
},
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
const dataWithTimestamp = {
...args.data,
updatedAt: new Date()
};
return await context.prisma.user.update({
where: args.where,
data: dataWithTimestamp
});
}
}
}
})
}
この実装により、以下のようなエラーを防ぐことができます:
typescript// よくあるエラー:タイムスタンプの設定忘れ
const user = await prisma.user.create({
data: {
email: 'test@example.com',
name: 'テストユーザー',
// createdAt と updatedAt が設定されていない
},
});
// エラー: Field 'createdAt' is required but not provided
データ検証機能
データの整合性を保つための検証機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
age Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
// メールアドレスの形式検証
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(args.data.email)) {
throw new Error('Invalid email format');
}
// 年齢の範囲検証
if (args.data.age && (args.data.age < 0 || args.data.age > 150)) {
throw new Error('Age must be between 0 and 150');
}
// 名前の長さ検証
if (args.data.name.length < 1 || args.data.name.length > 50) {
throw new Error('Name must be between 1 and 50 characters');
}
const now = new Date();
const dataWithTimestamps = {
...args.data,
createdAt: now,
updatedAt: now
};
return await context.prisma.user.create({
data: dataWithTimestamps
});
}
}
}
})
}
この実装により、以下のようなエラーを適切に処理できます:
typescript// 検証エラーの例
try {
const user = await prisma.user.create({
data: {
email: 'invalid-email', // 無効なメール形式
name: 'テストユーザー',
age: 200, // 無効な年齢
},
});
} catch (error) {
console.error(error.message);
// 出力: Invalid email format
}
カスタムメソッド追加
モデルにビジネスロジックを含むカスタムメソッドを追加します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
age Int?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
methods: {
// ユーザーの年齢グループを取得
getAgeGroup: {
type: "String",
args: [],
implementation: (user) => {
if (!user.age) return "Unknown";
if (user.age < 18) return "Minor";
if (user.age < 30) return "Young Adult";
if (user.age < 50) return "Adult";
return "Senior";
}
},
// ユーザーを非アクティブにする
deactivate: {
type: "User",
args: [],
implementation: async (user, args, context) => {
return await context.prisma.user.update({
where: { id: user.id },
data: {
isActive: false,
updatedAt: new Date()
}
});
}
},
// ユーザーの完全名を取得
getFullName: {
type: "String",
args: [],
implementation: (user) => {
return user.name;
}
}
}
})
}
ログ機能の実装
データベース操作のログを自動的に記録する機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
const now = new Date();
const dataWithTimestamps = {
...args.data,
createdAt: now,
updatedAt: now
};
const user = await context.prisma.user.create({
data: dataWithTimestamps
});
// ログの記録
console.log(`[${now.toISOString()}] User created: ${user.email}`);
return user;
}
},
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
const dataWithTimestamp = {
...args.data,
updatedAt: new Date()
};
const user = await context.prisma.user.update({
where: args.where,
data: dataWithTimestamp
});
// ログの記録
console.log(`[${new Date().toISOString()}] User updated: ${user.email}`);
return user;
}
},
delete: {
args: [{ name: "where", type: "UserWhereUniqueInput" }],
implementation: async (args, context) => {
const user = await context.prisma.user.delete({
where: args.where
});
// ログの記録
console.log(`[${new Date().toISOString()}] User deleted: ${user.email}`);
return user;
}
}
}
})
}
実践的なユースケース
実際のプロジェクトでよく使用される高度な拡張機能の実装例を見ていきましょう。
認証情報の自動付与
ユーザーの認証情報を自動的に付与する機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
// 認証情報の自動付与
const authData = {
...args.data,
role: args.data.role || "user",
createdAt: new Date(),
updatedAt: new Date()
};
const user = await context.prisma.user.create({
data: authData
});
return user;
}
}
},
methods: {
// 権限チェックメソッド
hasPermission: {
type: "Boolean",
args: [{ name: "permission", type: "String" }],
implementation: (user, args) => {
const permissions = {
admin: ["read", "write", "delete", "admin"],
moderator: ["read", "write", "moderate"],
user: ["read", "write"]
};
return permissions[user.role]?.includes(args.permission) || false;
}
},
// 管理者かどうかをチェック
isAdmin: {
type: "Boolean",
args: [],
implementation: (user) => {
return user.role === "admin";
}
}
}
})
}
データ暗号化・復号化
機密データを自動的に暗号化・復号化する機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
ssn String? // 社会保障番号(暗号化)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
fields: {
// 復号化されたSSN
decryptedSsn: {
type: "String",
compute: (user) => {
if (!user.ssn) return null;
// 実際の実装では適切な暗号化ライブラリを使用
return decrypt(user.ssn);
}
}
},
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
// SSNの暗号化
const encryptedData = {
...args.data,
ssn: args.data.ssn ? encrypt(args.data.ssn) : null,
createdAt: new Date(),
updatedAt: new Date()
};
return await context.prisma.user.create({
data: encryptedData
});
}
},
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
// SSNの暗号化(更新時)
const encryptedData = {
...args.data,
ssn: args.data.ssn ? encrypt(args.data.ssn) : undefined,
updatedAt: new Date()
};
return await context.prisma.user.update({
where: args.where,
data: encryptedData
});
}
}
}
})
}
// 暗号化・復号化関数(簡易版)
function encrypt(text: string): string {
return Buffer.from(text).toString('base64');
}
function decrypt(encryptedText: string): string {
return Buffer.from(encryptedText, 'base64').toString();
}
キャッシュ機能の実装
頻繁にアクセスされるデータをキャッシュする機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
findUnique: {
args: [{ name: "where", type: "UserWhereUniqueInput" }],
implementation: async (args, context) => {
const cacheKey = `user:${args.where.id || args.where.email}`;
// キャッシュから取得を試行
const cached = await getFromCache(cacheKey);
if (cached) {
console.log(`Cache hit for ${cacheKey}`);
return cached;
}
// データベースから取得
const user = await context.prisma.user.findUnique({
where: args.where
});
if (user) {
// キャッシュに保存(5分間)
await setCache(cacheKey, user, 300);
console.log(`Cache miss for ${cacheKey}, stored in cache`);
}
return user;
}
},
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
const user = await context.prisma.user.update({
where: args.where,
data: {
...args.data,
updatedAt: new Date()
}
});
// キャッシュを無効化
const cacheKey = `user:${user.id}`;
await invalidateCache(cacheKey);
console.log(`Cache invalidated for ${cacheKey}`);
return user;
}
}
}
})
}
// キャッシュ関数(簡易版)
const cache = new Map();
async function getFromCache(key: string) {
const item = cache.get(key);
if (item && item.expiresAt > Date.now()) {
return item.value;
}
cache.delete(key);
return null;
}
async function setCache(key: string, value: any, ttlSeconds: number) {
cache.set(key, {
value,
expiresAt: Date.now() + (ttlSeconds * 1000)
});
}
async function invalidateCache(key: string) {
cache.delete(key);
}
監査ログの自動記録
データの変更履歴を自動的に記録する監査ログ機能を実装します。
typescript// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
const now = new Date();
const dataWithTimestamps = {
...args.data,
createdAt: now,
updatedAt: now
};
const user = await context.prisma.user.create({
data: dataWithTimestamps
});
// 監査ログの記録
await createAuditLog({
table: "users",
action: "CREATE",
recordId: user.id,
oldData: null,
newData: user,
userId: getCurrentUserId(), // 現在のユーザーIDを取得
timestamp: now
});
return user;
}
},
update: {
args: [
{ name: "where", type: "UserWhereUniqueInput" },
{ name: "data", type: "UserUpdateInput" }
],
implementation: async (args, context) => {
// 更新前のデータを取得
const oldData = await context.prisma.user.findUnique({
where: args.where
});
const dataWithTimestamp = {
...args.data,
updatedAt: new Date()
};
const user = await context.prisma.user.update({
where: args.where,
data: dataWithTimestamp
});
// 監査ログの記録
await createAuditLog({
table: "users",
action: "UPDATE",
recordId: user.id,
oldData,
newData: user,
userId: getCurrentUserId(),
timestamp: new Date()
});
return user;
}
},
delete: {
args: [{ name: "where", type: "UserWhereUniqueInput" }],
implementation: async (args, context) => {
const user = await context.prisma.user.delete({
where: args.where
});
// 監査ログの記録
await createAuditLog({
table: "users",
action: "DELETE",
recordId: user.id,
oldData: user,
newData: null,
userId: getCurrentUserId(),
timestamp: new Date()
});
return user;
}
}
}
})
}
// 監査ログ作成関数
async function createAuditLog(logData: {
table: string;
action: string;
recordId: number;
oldData: any;
newData: any;
userId: number;
timestamp: Date;
}) {
// 実際の実装では監査ログテーブルに保存
console.log(`[AUDIT] ${logData.action} on ${logData.table}:${logData.recordId}`);
console.log(`Old data:`, logData.oldData);
console.log(`New data:`, logData.newData);
console.log(`User: ${logData.userId}, Time: ${logData.timestamp}`);
}
// 現在のユーザーIDを取得する関数(実装は環境に依存)
function getCurrentUserId(): number {
// 実際の実装では認証コンテキストから取得
return 1;
}
パフォーマンスとベストプラクティス
Prisma Client Extensions を使用する際のパフォーマンスとベストプラクティスについて説明します。
拡張機能の設計指針
拡張機能を設計する際は、以下の指針に従うことをお勧めします:
1. 単一責任の原則
typescript// 良い例:各拡張機能が明確な責任を持つ
model User {
// タイムスタンプ機能
@@extend({
queries: {
create: { /* タイムスタンプ設定のみ */ },
update: { /* タイムスタンプ更新のみ */ }
}
})
// 検証機能(別の拡張として分離)
@@extend({
queries: {
create: { /* データ検証のみ */ }
}
})
}
// 悪い例:複数の責任が混在
model User {
@@extend({
queries: {
create: {
// タイムスタンプ設定、検証、ログ記録、キャッシュ更新が混在
implementation: async (args, context) => {
// 複雑で保守しにくいコード
}
}
}
})
}
2. エラーハンドリングの統一
typescript// 統一されたエラーハンドリング
model User {
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
try {
// データ検証
validateUserData(args.data);
// データ作成
const user = await context.prisma.user.create({
data: {
...args.data,
createdAt: new Date(),
updatedAt: new Date()
}
});
// ログ記録
await logUserCreation(user);
return user;
} catch (error) {
// 統一されたエラーログ
console.error(`User creation failed: ${error.message}`);
throw error;
}
}
}
}
})
}
function validateUserData(data: any) {
if (!data.email || !data.email.includes('@')) {
throw new Error('Invalid email format');
}
if (!data.name || data.name.length < 1) {
throw new Error('Name is required');
}
}
パフォーマンスへの影響
拡張機能は便利ですが、パフォーマンスに影響を与える可能性があります。以下の点に注意してください:
1. 不要な計算の回避
typescript// 良い例:必要な時のみ計算
model User {
@@extend({
fields: {
expensiveCalculation: {
type: "String",
compute: (user) => {
// キャッシュされた値があれば使用
if (user._cachedExpensiveCalculation) {
return user._cachedExpensiveCalculation;
}
// 重い計算を実行
const result = performExpensiveCalculation(user);
user._cachedExpensiveCalculation = result;
return result;
}
}
}
})
}
// 悪い例:毎回重い計算を実行
model User {
@@extend({
fields: {
expensiveCalculation: {
type: "String",
compute: (user) => {
// 毎回重い計算を実行(非効率)
return performExpensiveCalculation(user);
}
}
}
})
}
2. データベースクエリの最適化
typescript// 良い例:必要なデータのみ取得
model User {
@@extend({
queries: {
findMany: {
args: [{ name: "args", type: "UserFindManyArgs" }],
implementation: async (args, context) => {
// デフォルトで必要なフィールドのみ取得
const defaultArgs = {
...args,
select: args.select || {
id: true,
email: true,
name: true,
createdAt: true
}
};
return await context.prisma.user.findMany(defaultArgs);
}
}
}
})
}
デバッグとトラブルシューティング
拡張機能のデバッグには、以下の方法が効果的です:
1. デバッグログの追加
typescriptmodel User {
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
console.log('[DEBUG] User creation started:', args.data);
try {
const user = await context.prisma.user.create({
data: {
...args.data,
createdAt: new Date(),
updatedAt: new Date()
}
});
console.log('[DEBUG] User created successfully:', user.id);
return user;
} catch (error) {
console.error('[DEBUG] User creation failed:', error);
throw error;
}
}
}
}
})
}
2. エラーの詳細情報
typescript// よくあるエラーとその解決方法
model User {
@@extend({
queries: {
create: {
args: [{ name: "data", type: "UserCreateInput" }],
implementation: async (args, context) => {
try {
return await context.prisma.user.create({
data: args.data
});
} catch (error) {
// エラーの種類に応じた詳細な情報を提供
if (error.code === 'P2002') {
throw new Error(`Email already exists: ${args.data.email}`);
}
if (error.code === 'P2003') {
throw new Error('Foreign key constraint failed');
}
if (error.code === 'P2011') {
throw new Error('Null constraint violation');
}
throw error;
}
}
}
}
})
}
3. 開発環境での拡張機能の無効化
typescript// 開発環境では拡張機能を無効化してデバッグ
const isDevelopment = process.env.NODE_ENV === 'development';
if (!isDevelopment) {
model User {
@@extend({
// 拡張機能の定義
})
}
}
まとめ
Prisma Client Extensions は、Prisma クライアントにカスタム機能を追加する強力な機能です。この記事で学んだ内容を活用することで、以下のような効果が期待できます:
開発効率の向上
- コードの重複を削減
- ビジネスロジックの集約
- 一貫性のあるデータ処理
保守性の向上
- 共通処理の一元管理
- エラーハンドリングの統一
- デバッグの容易さ
セキュリティの強化
- データ検証の自動化
- 認証情報の自動付与
- 監査ログの自動記録
パフォーマンスの最適化
- キャッシュ機能の実装
- 不要なクエリの削減
- 効率的なデータ処理
実際のプロジェクトでは、これらの拡張機能を組み合わせることで、より堅牢で保守性の高いアプリケーションを構築できます。最初は小さな拡張機能から始めて、徐々に複雑な機能を追加していくことをお勧めします。
Prisma Client Extensions の可能性は無限大です。あなたのプロジェクトに最適な拡張機能を見つけて、開発効率を劇的に向上させてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来