Mục lục : Show
User テーブルに対して CRUD を行う機能を実装します。
CRUD とはデータの作成(Create)、読み出し(Read)、更新(Update)、削除(Delete)のことを指します。
リポジトリパターンの適用
app/_repositories
ディレクトリを作成し、データベースにアクセスするような機能は全てその中に実装することとします。
これは「リポジトリパターン」と呼ばれる設計パターンで、データベースに関する処理を「永続化レイヤー」にまとめておくことで、 例えば「データベースを PostgreSQL から Oracle に変更したい」だとか「サーバロジックとAPI両方からデータベースにアクセスしたい」といった場合に、 データベースアクセスの処理が一元化されているため保守しやすくなります。
ここでは User テーブルに対する リポジトリパターンの実装を行います。
User リポジトリの作成
User テーブルに対する処理を app/_repositories/User.ts
に集約します。
今後、User テーブルに対するデータアクセスの処理が追加になった場合 User.ts
に処理(関数)を追加してください。
app/_repositories/User.ts
import { prisma } from '@/app/_utils/prismaSingleton';
import { User } from '@prisma/client';
export namespace UserRepository {
export async function findMany() {
return await prisma.user.findMany({
include: {
role: true,
department: true,
},
});
}
export async function findUnique(id: string) {
return await prisma.user.findUnique({
where: {
id: id,
},
});
}
export async function create(user: User) {
return await prisma.user.create({
data: user,
});
}
export async function update(id: string, user: User) {
return await prisma.user.update({
where: {
id: id,
},
data: {
...user,
},
});
}
export async function remove(id: string) {
return await prisma.user.delete({
where: {
id: id,
},
});
}
}
Role リポジトリの作成
Role テーブルに対する処理を app/_repositories/Role.ts
に集約します。
app/_repositories/Role.ts
import { prisma } from '@/app/_utils/prismaSingleton';
import { Role } from '@prisma/client';
export namespace RoleRepository {
export async function findMany() {
const users = await prisma.role.findMany();
return users;
}
export async function create(role: Role) {
const createdRole = await prisma.role.create({
data: role,
});
return createdRole;
}
}
Department リポジトリの作成
Department テーブルに対する処理を app/_repositories/Department.ts
に集約します。
app/_repositories/Department.ts
import { prisma } from '@/app/_utils/prismaSingleton';
import { Department } from '@prisma/client';
export namespace DepartmentRepository {
export async function findMany() {
const users = await prisma.department.findMany();
return users;
}
export async function create(department: Department) {
const createdDepartment = await prisma.department.create({
data: department,
});
return createdDepartment;
}
}
ThanksCard リポジトリの作成
ThanksCard テーブルに対する処理を app/_repositories/ThanksCard.ts
に集約します。
app/_repositories/ThanksCard.ts
import { prisma } from '@/app/_utils/prismaSingleton';
import { Prisma } from '@prisma/client';
// ThanksCardRepository.findMany(ThanksCardとUser(from, to)をjoinした結果) が返すリストの型から
// Promise を取り省いた型を export する
export type ThanksCardWithFromToList = Prisma.PromiseReturnType<
typeof ThanksCardRepository.findMany
>;
export namespace ThanksCardRepository {
export async function findMany() {
return await prisma.thanksCard.findMany({
include: {
from: true,
to: true,
},
});
}
export async function findUnique(id: string) {
return await prisma.thanksCard.findUnique({
include: {
from: true,
to: true,
},
where: {
id: id,
},
});
}
}
読み出し(Read)
User テーブルに対して Read を行う機能を実装します。
ユーザ一覧画面の実装
ユーザ一覧表示コンポーネントの実装
app/user/_components/user-list.tsx
を以下のように実装してください。
今後「ユーザの作成」「ユーザの更新」「ユーザの削除」を行うためのリンクも以下に実装しています。
app/user/_components/user-list.tsx
'use client';
import Link from 'next/link';
/* ライブラリ Material-UI が提供するコンポーネントの import */
import Button from '@mui/material/Button';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
/* icons */
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import { Prisma } from '@prisma/client';
import { useRouter } from 'next/navigation';
type User = Prisma.UserGetPayload<{
include: {
role: true;
department: true;
};
}>;
type Props = {
users: User[];
};
export default function UserList(props: Props) {
const users = props.users;
const router = useRouter();
const onDelete = async (id: string) => {
const response = await fetch(`/api/user/${id}`, {
method: 'DELETE',
});
router.refresh();
};
return (
<>
<Link href='/user/create' passHref>
<Button variant='contained' color='primary'>
<PersonAddIcon /> Create User
</Button>
</Link>
<div>
<Table size='small'>
<TableHead>
<TableRow>
<TableCell>id</TableCell>
<TableCell>name</TableCell>
<TableCell>email</TableCell>
<TableCell>role</TableCell>
<TableCell>department</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{/* users 全件をテーブル出力する */}
{users.map((user) => {
return (
/* 一覧系の更新箇所を特定するために一意となる key を設定する必要がある */
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role.name}</TableCell>
<TableCell>{user.department.name}</TableCell>
<TableCell>
<Link href={`/user/edit/${user.id}`} passHref>
<Button variant='contained' color='primary'>
Edit
</Button>
</Link>
</TableCell>
<TableCell>
<Button onClick={() => onDelete(user.id)} variant='contained' color='warning'>
Delete
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</>
);
}
pageコンポーネント(page.tsx)の実装
app/user/page.tsx
を以下のように実装してください。
app/user/page.tsx
import { UserRepository } from '@/app/_repositories/User';
import UserList from '@/app/user/_components/user-list';
export default async function UserPage() {
const users = await UserRepository.findMany();
return (
<>
<UserList users={users} />
</>
);
}
この状態で http://localhost:3000/user にアクセスすると、User 一覧が表示されます。
作成(Create)
User テーブルに対して Create を行う機能を実装します。
ユーザ作成APIの実装
User テーブルにレコードを1件挿入する API を実装します。
URL は /api/user とします。 HTTP メソッドは POST とします。
pages/api/user/route.ts
を作成し、以下のように実装してください。
app/api/user/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { User } from '@prisma/client';
import { UserRepository } from '@/app/_repositories/User';
export async function POST(request: NextRequest) {
try {
const user: User = await request.json();
const createdUser = UserRepository.create(user);
return NextResponse.json(createdUser);
} catch (e) {
//return NextResponse.next({ status: 500 });
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
ユーザ作成画面の実装
バリデーションライブラリの導入
「ユーザ登録機能」では利用者がユーザに関する情報を入力して、新しいデータを作成します。こういった「ユーザがデータを入力する画面」のことを「入力フォーム」と言います。
入力フォームでは、利用者が「正しく情報を入力しているか」をチェックする必要があります。そうしなければ、不正なデータが作成されてしまい、後々利用できない場合も出てきます。このような「利用者が正しく情報を入力しているか」をチェックする仕組みのことを「バリデーションチェック(妥当性検証)」と呼びます。
バリデーションチェックはいろいろなバリエーションがありますが、例を以下に示します。
バリデーションの例
- 必須入力の項目に値が入力されているか
- パスワードが8文字以上になっているか
- OK: longpass
- NG: shortp
- 数量が数字になっているか
- OK: 0, 100
- NG: ゼロ, 100(マルチバイト文字), 1,980
- メールアドレスがちゃんとしたフォーマットになっているか
- OK: test@occ.co.jp
- NG: testocc.co.jp
これらバリデーションの機能は多くのシステムで汎用的に利用されるため、良く設計された「バリデーションライブラリ」を利用すると、私たちの開発コストを低減することができます。
ここでは yup
というバリデーションライブライを利用します。yup には上で示したような良くあるバリデーションの機能が実装されています。
以下で yup パッケージをインストールしてください。
npm install --save yup
yup のスキーマ定義
yup ではスキーマと呼ばれるデータ構造でバリデーションの設定を行います。
今回はユーザ作成で使用する yup スキーマですので User モデルに関するスキーマを定義します。
formSchema/user.ts
を作成し、以下のように編集してください。
formSchema/user.ts
import * as yup from "yup";
export const userFormSchema = yup
.object({
// Prismaが生成する型と整合性を取るために nullable() を追加している
name: yup.string().nullable(),
email: yup
.string()
.email("Invalid mail format.")
.required("email is a required field"),
password: yup.string().min(4).required("password is a required field"),
roleId: yup.string().required(),
departmentId: yup.string().required(),
})
.required();
export type UserFormData = yup.InferType<typeof userFormSchema>;
// Same as...
/*
type UserFormData = {
name: string | null | undefined;
email: string;
password: string;
roleId: string;
departmentId: string;
};
*/
上記 yup スキーマ定義は以下を意味しています。
- name: yup.string().nullable()
- → name プロパティは文字列
string()
である - → name プロパティは Null でも良い
nullable()
- → name プロパティは文字列
- email: yup.string().email(“Invalid mail format.”).required(“email is a required field”)
- → email プロパティは文字列
string()
である - → email プロパティは email フォーマット
email()
で、バリデーションエラー時のメッセージは「Invalid mail format.
」とする - → email プロパティは必須入力
required()
で、バリデーションエラー時のメッセージは「email is a required field.
」とする
- → email プロパティは文字列
- password: yup.string().min(4).required(“password is a required field”)
- → password プロパティは文字列
string()
である - → password プロパティは4文字以上
min(4)
でなければならない- (必要であれば
max()
も用意されています)
- (必要であれば
- → password プロパティは必須入力
required()
で、バリデーションエラー時のメッセージは「password is a required field.
」とする
- → password プロパティは文字列
最後の行にある export type UserFormData = yup.InferType<typeof userFormSchema>;
は、yup で定義した User モデルのスキーマから TypeScript の型を生成(型推論)しています。手動で型を定義する場合の例をコメントで残していますが、スキーマ定義と型定義で同じような文言が重複しますので、yup.InferType
を使用して自動生成している現状の書き方の方が好ましいです。
参考リンク
入力フォームの状態管理ライブラリの導入
入力フォームではバリデーション以外でも以下のことを考慮する必要があります。
- フォームの初期値(デフォルト値)をどうするか
- バリデーションチェックがエラーの場合、どのように利用者に通知するか
これら機能も多くのシステムで汎用的に利用されるため、良く設計された「入力フォーム管理ライブラリ」を利用すると、私たちの開発コストを低減することができます。 スポンサー記事 カラフルで明るくスタイリッシュなソックスのコレクションから、誰にでも合うものを見つけてください。個別に購入することも、バンドルで購入することもできます。 sock 引き出し!
React には様々な入力フォーム管理ライブラリがありますが、2022年現在、Formik
と React Hook Form
が良く使われています。
ここでは React Hook Form
ライブライを利用します。
以下で Reac Hook Form パッケージをインストールしてください。
npm install --save react-hook-form @hookform/resolvers
ユーザ作成フォーム表示コンポーネントの実装
app/user/_components/user-form.tsx
'use client';
import { useRouter } from 'next/navigation';
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import FormHelperText from '@mui/material/FormHelperText';
import Button from '@mui/material/Button';
import { userFormSchema, UserFormData } from '@/app/_formSchema/user';
import { Department, Role, User } from '@prisma/client';
type Props = {
user?: User | null;
roles: Role[];
departments: Department[];
onSuccessUrl: string;
};
export default function UserForm(props: Props) {
const user = props.user;
const roles = props.roles;
const departments = props.departments;
const onSuccessUrl = props.onSuccessUrl;
const router = useRouter();
// props.user が与えられていれば「編集モード(edit)」とする。
// props.user が与えられていなれば「作成モード(create)」とする。
let mode: 'edit' | 'create';
if (user) {
mode = 'edit';
} else {
mode = 'create';
}
const [postError, setPostError] = React.useState<string>();
const {
register,
handleSubmit,
formState: { errors },
reset,
// } = useForm<IFormInputs>({
} = useForm({
resolver: yupResolver(userFormSchema),
defaultValues: { ...user },
});
// フォームに初期値を入力する
/*
React.useEffect(() => {
if (user) {
reset(user);
}
}, [reset, user]);
*/
const onSubmit = handleSubmit(async (formData) => {
let response: Response;
if (mode == 'edit') {
response = await fetch(`/api/user/${user?.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
} else {
// mode == 'create'
response = await fetch(`/api/user`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
}
if (response.ok) {
//const response_json = await response.json();
router.refresh();
router.push(onSuccessUrl);
} else {
setPostError('server error');
}
});
return (
<>
<span className='error'>{postError}</span>
<form onSubmit={onSubmit}>
<FormControl fullWidth error={'name' in errors}>
<TextField
label='Name'
variant='standard'
helperText={errors.name?.message}
{...register('name')}
/>
</FormControl>
<FormControl fullWidth error={'email' in errors}>
<TextField
label='Email'
variant='standard'
required
helperText={errors.email?.message}
{...register('email')}
/>
</FormControl>
<FormControl fullWidth error={'password' in errors}>
<TextField
label='Password'
variant='standard'
type='password'
required
helperText={errors.password?.message}
{...register('password')}
/>
</FormControl>
<FormControl fullWidth error={'roleId' in errors}>
<InputLabel>Role</InputLabel>
<Select
label='role'
required
defaultValue={user ? user.roleId : ''}
{...register('roleId')}
>
{roles?.map((role) => {
return (
<MenuItem key={role.id} value={role.id}>
{role.name}
</MenuItem>
);
})}
</Select>
<FormHelperText error={true}>{errors.roleId?.message}</FormHelperText>
</FormControl>
<FormControl fullWidth error={'departmentId' in errors}>
<InputLabel>Department</InputLabel>
<Select
label='department'
required
defaultValue={user ? user.departmentId : ''}
{...register('departmentId')}
>
{departments?.map((department) => {
return (
<MenuItem key={department.id} value={department.id}>
{department.name}
</MenuItem>
);
})}
</Select>
<FormHelperText error={true}>{errors.departmentId?.message}</FormHelperText>
</FormControl>
<Button type='submit' variant='contained' color='primary'>
Submit
</Button>
</form>
</>
);
}
pageコンポーネント(page.tsx)の実装
app/user/create/page.tsx
import UserForm from '@/app/user/_components/user-form';
import { DepartmentRepository } from '@/app/_repositories/Department';
import { RoleRepository } from '@/app/_repositories/Role';
export default async function UserCreate() {
const roles = await RoleRepository.findMany();
const departments = await DepartmentRepository.findMany();
return <UserForm departments={departments} roles={roles} onSuccessUrl='/user/' />;
}
更新(Update)
User テーブルに対して Update を行う機能を実装します。
ユーザ更新APIの実装
User テーブルの既存のレコードを1件更新する API を実装します。
URL は /api/user/[id]
とします。 HTTP メソッドは PUT
とします。
[id]
は更新するユーザの id です。
参考リンク
(idを含んだ URL の具体例)
/api/user/cl7ts8yvu0045ssa2e2vcrezk
/api/user/cl7ts8yw20054ssa2hxj5ii9h
これから実装する API のプログラムの中で、URL から id を取得し、データベースに Update 文を実行する際の条件に加えます。
app/api/user/[id]/route.ts
を作成し、以下のように実装してください。
app/api/user/[id]/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { User } from '@prisma/client';
import { UserRepository } from '@/app/_repositories/User';
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
try {
const user: User = await request.json();
const updatedUser = await UserRepository.update(params.id, user);
return NextResponse.json(updatedUser);
} catch (e) {
//return NextResponse.next({ status: 500 });
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
参考リンク
ユーザ更新フォーム表示コンポーネントの実装
ユーザ作成の際に作成した app/user/_components/user-form.tsx
はユーザ更新用としても使えるように作成してありますので、追加実装は必要ありません。
pageコンポーネント(page.tsx)の実装
app/user/edit/[id]/page.tsx
import UserForm from '@/app/user/_components/user-form';
import { UserRepository } from '@/app/_repositories/User';
import { DepartmentRepository } from '@/app/_repositories/Department';
import { RoleRepository } from '@/app/_repositories/Role';
// Dynamic Segments (/user/edit/[id]) から [id] を取得する
type Props = {
id: string;
};
export default async function UserEdit({ params }: { params: Props }) {
const user = await UserRepository.findUnique(params.id);
const roles = await RoleRepository.findMany();
const departments = await DepartmentRepository.findMany();
return <UserForm user={user} departments={departments} roles={roles} onSuccessUrl='/user/' />;
}
削除(Delete)
User テーブルに対して Delete を行う機能を実装します。
ユーザ削除APIの実装
app/api/user/[id]/route.ts
を以下のように変更してください。 (DELETE 関数を追加しています。)
app/api/user/[id]/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { User } from '@prisma/client';
import { UserRepository } from '@/app/_repositories/User';
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
try {
const user: User = await request.json();
const updatedUser = await UserRepository.update(params.id, user);
return NextResponse.json(updatedUser);
} catch (e) {
//return NextResponse.next({ status: 500 });
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
try {
const deletedUser = await UserRepository.remove(params.id);
return NextResponse.json(deletedUser);
} catch (e) {
//return NextResponse.next({ status: 500 });
console.log(e);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
参考リンク
pageコンポーネント(page.tsx)の実装
ユーザ削除画面はユーザ一覧画面に「DELETE」ボタンとして既に実装済みです。
「DELETE」ボタンで実際にユーザが削除されることを確認してください。