DBの設定(Vercel)
データ取得手順¶
ライブラリをインストール
pnpm i @vercel/postgres
/app/libs
にDB操作モジュールを作る。
各コンポーネントではSQL実行関数を実行し、返り値を取得する。
import { sql } from '@vercel/postgres';
返却値はsql
の後ろに定義することで、型を指定できる。
const data = await sql<InvoiceForm>`~~~`
SELECT¶
import { sql } from '@vercel/postgres';
export async function fetchInvoiceById(id: string) {
try {
const data = await sql<InvoiceForm>`
SELECT
invoices.id,
invoices.customer_id,
invoices.amount,
invoices.status
FROM invoices
WHERE invoices.id = ${id};
`;
const invoice = data.rows.map((invoice) => ({
...invoice,
// Convert amount from cents to dollars
amount: invoice.amount / 100,
}));
return invoice[0];
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch invoice.');
}
}
INSERT¶
Formから受け取ったデータをDBに挿入するケースを想定する。
-
form
タグを使い、/app/lib/actions.ts
で定義したアクションをaction
にセットする。/app/lib/actions.tsimport { createInvoice } from '@/app/lib/actions'; ... <form action={createInvoice}> <input />... <Button type="submit"> </form>
'use server'; export async function createInvoice(formData: FormData) {}
use server
を記載すると、ファイル内のすべてのエクスポートされた関数がサーバー アクション
としてマークされる。
現状Button
を押下すると、form
タグ内のinputの値を全てformData
という変数にオブジェクトとして代入される。 -
任意の形で入ってくることがあるので、型検証する。
Zodライブラリimport { z } from 'zod'; // DBスキーマに準じた型 const FormSchema = z.object({ id: z.string(), customerId: z.string(), // 文字列から強制的に数字に変換するよう設定 amount: z.coerce.number(), status: z.enum(['pending', 'paid']), date: z.string(), }); // ここでの検証する値は"customerId", "amount", "status"なので // 検証不要なものは除外して変数に格納する。 const CreateInvoice = FormSchema.omit({ id: true, date: true });
-
FormDataを分解して、検証に問題がなかったらDBに登録する
const { customerId, amount, status } = CreateInvoice.parse({ customerId: formData.get('customerId'), // Zodによって数値型に変換 amount: formData.get('amount'), status: formData.get('status'), }); // DBに挿入するための値づくり const amountInCents = amount * 100; const date = new Date().toISOString().split('T')[0]; await sql` INSERT INTO invoices (customer_id, amount, status, date) VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) `;
-
DBを更新して一覧画面にリダイレクトする
Next.jsにはルートセグメントをユーザーのブラウザに一定期間保存するクライアント側ルーターキャッシュ
がある。
そのままリダイレクトしたら、DB更新前の値で作られた一覧が表示されたままになってしまうので、
更新したい場合は、古いページのキャッシュを削除する必要がある。
そのうえで、目当てのルートにリダイレクトする。import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; // キャッシュを削除したいパスを引数に指定 revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices');
まとめると以下
/app/lib/actions.ts
'use server';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(['pending', 'paid']),
date: z.string(),
});
const CreateInvoice = FormSchema.omit({ id: true, date: true });
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
UPDATE¶
form
で受け取ったIDのデータを更新したい。
下記のようにsubmitを受けたらメソッドに引数を入れて実行したい。
このメソッドはinputのデータを受取り、引数で受けたidのデータを変更するメソッド。
// Passing an id as argument won't work
<form action={updateInvoice(id)}>
ただし、これはできない。
これを実現するには、bind
を使用する。
export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm;
customers: CustomerField[];
}) {
// bind
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
return (
<form action={updateInvoiceWithId}>
<input type="hidden" name="id" value={invoice.id} />
</form>
);
}
Warning
<input type="hidden" name="id" value={invoice.id} />
のようにinputにvalueを入れる方法もあるが、IDなどの機密データを入れるのには適していない。
Insert同様Zod
を使用して肩を検証したうえでクエリを実行する。
// Use Zod to update the expected types
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
// ...
export async function updateInvoice(id: string, formData: FormData) {
const { customerId, amount, status } = UpdateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
try {
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
} catch (error) {
return { message: 'Database Error: Failed to Update Invoice.' };
}
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
新しいサーバー要求を行うために呼び出します。 6. redirectユーザーを請求書のページにリダイレクトするための呼び出し。
DELETE¶
他と同様。
import { deleteInvoice } from '@/app/lib/actions';
// ...
export function DeleteInvoice({ id }: { id: string }) {
const deleteInvoiceWithId = deleteInvoice.bind(null, id);
return (
<form action={deleteInvoiceWithId}>
<button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
<span className="sr-only">Delete</span>
<TrashIcon className="w-4" />
</button>
</form>
);
}
actionにDELETEメソッド作成。
export async function deleteInvoice(id: string) {
try {
await sql`DELETE FROM invoices WHERE id = ${id}`;
revalidatePath('/dashboard/invoices');
return { message: 'Deleted Invoice.' };
} catch (error) {
return { message: 'Database Error: Failed to Delete Invoice.' };
}
}
Promise¶
リクエストウォーターフォール¶
前のリクエストの完了に依存する一連のネットワーク リクエストを指します。
各リクエストは前のリクエストがデータを返した後にのみ開始できます
並列データ取得¶
ウォーターフォールを回避する一般的な方法は、
すべてのデータ要求を同時に、つまり並行して開始することです。
Promise.all()
, Promise.allSettled()
関数を使用して
すべてのPromiseを開始する。
export async function fetchCardData() {
try {
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
// ...
}
}
パフォーマンス向上。
Zod¶
Zodで型検証を行う。
型を宣言するのと同時に、バリデーションメッセージも設定できる。
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});