こもるーとたぬきの備忘録

profile

【Prisma】DBの操作ができるようになりたい話②

DBを使えるようになりたい話②
ちょっと前まで、vercelのストレージ機能にPostgreSQLがあった気がするんだけど、いつの間にかその項目がなかった…
(それを使ってDBの学習をしようと思っていたのに、残念)
とりあえず、ローカル環境のDBを使用したToDoListを作成したので、その備忘録。
前回に引き続きprismaを使用してサーバとDBを接続。

開発環境バージョン
Next.js15.2.1
TypeScript5.0.0
sass1.85.1
prisma^6.4.1
@prisma/client^6.4.1
react-hook-form^7.54.2

サーバとDBをprismaを使用して接続

ローカルにnext.jsのプロジェクトを作成
その後、ローカルにxamppを立ち上げ、今回の学習用のDBを作成。
過去記事【Prisma】DBの操作ができるようになりたい話①と同様に、prismaと@prisma/clientをインストールする。
scema.prismaにテーブル定義を以下のように記述し、prismaのマイグレーションを実行して、DBにテーブルを作成しつつ、サーバとDBの接続を確認。


    model Lists {
        id Int @default(autoincrement()) @id
        title String
        completed Boolean @default(false)
        createdAt DateTime @default(now())
    }
    

DBの操作のためのAPIの作成

src/app/api/route.ts


    import { PrismaClient } from "@prisma/client";
    import { NextRequest } from "next/server";

    // DBのデータ取得
    export async function GET() {
        // 取得データの型定義
        type List = {
        id: number;
        title: string;
        completed: boolean;
        createdAt: Date;
        };

        // prismaインスタンスの生成
        const prisma = new PrismaClient();

        // DBからデータの取得
        const lists: Array<List> = await prisma.lists.findMany();
        return Response.json({
        status: 200,
        res: lists,
        });
    }

    // DBへデータ登録
    export async function POST(req: NextRequest) {
        // リクエストの型定義
        type ReqData = {
        task: string;
        };
        // prismaのインスタンス生成
        const prisma = new PrismaClient();
        // 登録内容の取得
        const data: ReqData = await req.json();
        // リストの登録
        const list = await prisma.lists.create({
        data: {
            title: data.task,
        },
        });

        return Response.json({
        status: 200,
        message: "post test",
        res: list,
        });
    }    
    

src/app/api/[id]/route.ts


    import { PrismaClient } from "@prisma/client";

    // 更新用API
    export async function PATCH(
      req: Request,
      { params }: { params: Promise<{ id: string }> }
    ) {
      type ReqData = {
        completed: boolean;
      };

      // 対象のIDを取得
      const { id } = await params;
      //prismaのインスタンス生成
      const prisma = new PrismaClient();
      // リクエストの取得
      const data: ReqData = await req.json();

      // DBのデータ更新
      const updateList = await prisma.lists.update({
        where: {
          id: Number(id),
        },
        data: {
          completed: !data.completed,
        },
      });

      return Response.json({
        status: 200,
        res: updateList,
      });
    }

    // 削除用API
    export async function DELETE(
      req: Request,
      { params }: { params: Promise<{ id: string }> }
    ) {
      // 対象のIDを取得
      const { id } = await params;

      //prismaのインスタンス生成
      const prisma = new PrismaClient();

      // 削除の実行
      const deleteList = await prisma.lists.delete({
        where: {
          id: Number(id),
        },
      });

      return Response.json({
        status: 200,
        res: deleteList,
      });
    }    
    

フロント部分の作成


    "use client";

    import { useEffect, useState } from "react";
    import { useForm } from "react-hook-form"; 

    import styles from "./style.module.scss";

    // DBから取得したデータの型
    type List = {
      id: number;
      title: string;
      completed: boolean;
      createdAt: Date;
    };

    // 入力データの型定義
    type Input = {
      task: string;
    };

    export const Lists = () => {
      // 表示させるリスト一覧をフロント側で管理する配列
      const [lists, setLists] = useState<Array<List>>([]);

      // 初回のレンダリングの時のみDBよりデータ取得
      useEffect(() => {
        type getDataRes = {
          status: number;
          res: Array<List>;
        };

        //// データの取得処理
        const getData = async () => {
          const getUrl = `${process.env.NEXT_PUBLIC_API_URL}`;
          try {
            const res = await fetch(getUrl);
            if (!res.ok) {
              throw new Error(`status:${res.status}`);
            }
            const resData: getDataRes = await res.json();
            setLists(resData.res);
          } catch (error) {
            console.log(error);
          }
        };
        getData();
      }, []);

      // react-hook-formの定義
      const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
      } = useForm<Input>();

      //// 登録処理
      const add = async (data: Input) => {
        const createUrl = `${process.env.NEXT_PUBLIC_API_URL}`;
        // DBへの登録処理
        try {
          const res = await fetch(createUrl, {
            method: "post",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ task: data.task }),
          });

          if (!res.ok) {
            throw new Error(`status:${res.status}`);
          }

          const resData = await res.json();
          // 登録処理が完了したら、フロント側で管理するタスク一覧の配列に格納
          setLists([...lists, resData.res]);
          // フォームの入力内容をリセット
          reset();
        } catch (error) {
          console.log(error);
        }
      };

      //// 削除処理
      const remove = async (e: React.MouseEvent<HTMLInputElement>) => {
        // id番号の取得
        const id = e.currentTarget.dataset.num;
        // 削除URLの生成
        const deleteUrl = `${process.env.NEXT_PUBLIC_API_URL}/${id}`;

        // APIを通して削除処理
        try {
          const res = await fetch(deleteUrl, {
            method: "DELETE",
          });

          if (!res.ok) {
            throw new Error(`status:${res.status}`);
          }
          // 問題なく削除APIが実行されたら、配列より対象のタスクを処理
          setLists((prevLists) =>
            prevLists.filter((item) => item.id != Number(id))
          );
        } catch (error) {
          console.log(error);
        }
      };

      // 更新処理
      const changeCompleted = async (e: React.ChangeEvent<HTMLInputElement>) => {
        // idの取得
        const id = e.currentTarget.dataset.num;
        // 更新URLの生成
        const updataUrl = `${process.env.NEXT_PUBLIC_API_URL}/${id}`;

        // 完了済みか否かを判定
        const completed = e.currentTarget.dataset.checked === "true";

        // 更新処理
        try {
          const res = await fetch(updataUrl, {
            method: "PATCH",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ completed }),
          });

          if (!res.ok) {
            throw new Error(`status:${res.status}`);
          }

          const resData = await res.json();

          console.log(resData);

          // 問題なく更新処理が完了したら、配列の内容を更新
          setLists((prevLists) =>
            prevLists.map((list) =>
              list.id === Number(id)
                ? { ...list, completed: !list.completed }
                : list
            )
          );
        } catch (error) {
          console.log(error);
        }
      };

      return (
        <div className={styles.wrapper}>
          <div className={styles.lists}>
            {lists.length ? (
              lists.map((list) => (
                <div className={styles.list} key={list.id}>
                  <div className={styles.left}>
                    <input
                      className={styles.checkBtn}
                      type="checkbox"
                      checked={list.completed}
                      data-num={String(list.id)}
                      data-checked={list.completed}
                      onChange={changeCompleted}
                    ></input>
                    <div className={styles.text}>{list.title}</div>
                  </div>
                  <div
                    className={styles.right}
                    onClick={remove}
                    data-num={String(list.id)}
                  >
                    削除
                  </div>
                </div>
              ))
            ) : (
              <div className={styles.notask}>No task</div>
            )}
          </div>
          <div className={styles.add}>
            <form onSubmit={handleSubmit(add)}>
              <input
                className={styles.textbox}
                type="text"
                id="task"
                placeholder="To Do Listを入力してください"
                {...register("task", { required: true })}
              />
              {errors.task && errors.task.type === "required" && (
                <p>※この項目は必須です。</p>
              )}
              <button className={styles.submit} type="submit">
                add
              </button>
            </form>
          </div>
        </div>
      );
    };    
    

Next.jsのプロジェクト作成の部分などはいつもと一緒なので省略。
項目の入力欄には、react-hook-formを使用。
なお、APIのURLについては、.envファイルに記述して管理

参照