😷

Nuxt3、Firebase、Prismaを利用し、SQL文を書かずに簡単なToDoアプリを作ってみる

2023/01/27に公開

どんなアプリ

  • ToDoとサブToDoを作れるアプリ
  • FirebaseでEメールパスワード認証とGoogle認証対応
  • DBはMySQL

この記事ではPrismaについてのみ詳しく書きますが、Firebase認証やMySQL、Nuxt3なんかは省きますのでご了承ください。

サンプルは私のブログの方で公開しております。
https://kote2.tokyo/post/post-9714

環境

  • DB: MySQL5.7(MAMP付属のローカルDB)
  • FW:Nuxt3(Vue3),node.js v17.0.0,

Prismaとは

Prismaはnode.jsからSQLを扱えるライブラリでORM(Object Relational Mapper)と呼ばれます。なんか説明が難しいのですが、「SQL書かなくてもリレーショナル・データベースを扱える」と理解。

なお言語によっていろんなORMがあり、JavaScriptならPrisma、やPHPはEloquent、PythonならSQLAlchemyなど、いろんな種類があります。

で、今回はJavaScript(node.js)で作るのでPrismaを使います。

Prisma対応のRDB

  • MySQL
  • SQLite
  • MongoDB
  • CockroachDB
  • Microsoft SQL Server


https://www.prisma.io/

実践

私の方ではNuxt3で構築してますが、Nextや他のツールでも基本同じかと思います。

事前準備

データベースの作成

私の方ではローカルでPHP開発構築が簡単にできるMAMPというツールに付属しているMySQLを使用していますが、Prismaでは以下対応していますのでお好きなものを使ってデータベースを作成してください。私はMySQL上に「Prisma」というデータベースを作成しました。

Prisma対応のRDB

  • MySQL
  • SQLite
  • MongoDB
  • CockroachDB
  • Microsoft SQL Server

※MariaDBに関してはv10じゃないとエラーが出ました。

詳しくは対応DBバージョン情報をご参照ください
https://www.prisma.io/docs/reference/database-reference/supported-databases

root権限相当のユーザーが必要

Prismaではmigrate dev や migrate reset などの開発コマンドを使用する際にシャドーデータベースを作成および削除するために、現在 Prisma Migrate では、データソースに定義されているデータベースユーザーにデータベースを作成する権限があることが必要とのことです。

なのでユーザーを作ってもよいのですが、ローカル開発環境であればrootユーザーでいいと思います。
参考 => https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database

Prismaのインストール

npm install prisma

初期化

prismaフォルダが用意され、設定ファイル(schema.prisma)ができます

npx prisma init

schema.prismaに使用するRDBとモデル構成を記述

参考 => https://www.prisma.io/docs/concepts/components/prisma-schema


// --------------------------------------------------
// Prisma settings
// --------------------------------------------------
generator client {
  provider = "prisma-client-js"
}

// MySQLの場合
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// PostgreSQLの場合
// datasource db {
//   provider = "postgresql"
//   url      = env("DATABASE_URL")
// }

// SQLiteの場合
// datasource db {
//   provider = "sqlite"
//   url      = "file:./dev.db"
// }

// --------------------------------------------------
// DB Models
// --------------------------------------------------

// ユーザーテーブル
model users {
  id Int @id @default(autoincrement())
  uid String @unique @db.VarChar(255)
  email String @unique
  displayName String? @db.VarChar(255)
  description String? @db.Text
  token String? @db.Text
  favorite Json?
  mytodo_id todo[]
  mysubtodo_id subtodo[]
  created_at DateTime @default(now()) @db.Timestamp(0)
  updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
}

// ToDoテーブル
model todo {
  id Int @id @default(autoincrement())
  uid String @db.VarChar(255)
  title String @db.VarChar(255)
  description String? @db.Text
  orner_id_more users? @relation(fields: [orner_id], references: [id])
  orner_id Int?
  subtodo_id subtodo[]
  favorited Json?
  created_at DateTime @default(now()) @db.Timestamp(0)
  updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
}

// サブToDoテーブル
model subtodo {
  id Int @id @default(autoincrement())
  uid String @db.VarChar(255)
  title String @db.VarChar(255)
  description String? @db.Text
  todo_id_more todo? @relation(fields: [todo_id], references: [id])
  todo_id Int?
  orner_id_more users? @relation(fields: [orner_id], references: [id])
  orner_id Int?
  created_at DateTime @default(now()) @db.Timestamp(0)
  updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
}

envファイルにデータベースの接続情報

参考 => https://www.prisma.io/docs/concepts/database-connectors/mysql

DATABASE_URL="mysql://root:root@127.0.0.1:3306/Prisma?schema=public"

マイグレーション

設定ファイルの記述が終わったらマイグレーションします。migrateとはSQL書かずにDBのテーブルなんかを作ることです。

npx prisma migrate dev
npx prisma generate # こちらはモデルを変更した場合に行う

なお、マイグレーションという作業はデータベースを構築し直したりするデリケートな作業のため、migrate devは開発環境だけに使い、本番環境はmigrate deployを使ってね、など注意書きがありますのでこちらも参考にしてください
参考 => https://www.prisma.io/docs/concepts/components/prisma-migrate/migrate-development-production

シードはまだできてません

本来であればマイグレーションと同時にダミーデータを入れるシードという作業もあるのですがまだやってないので省略します。

CRUD

データの作成(Create)、読み出し(Read)、更新(Update)、削除(Delete)の基本のやり方です。Nuxt3で行っています。

データの作成(Create)

表示する側

page/index.vue
<script setup>
const { createToDo } = useToDo();

const onSubmit = async () => {
  await createToDo(
    {
    title: 'テストのタイトル',
    description: 'テストの概要',
    }
  );
}
</script>

<template>
  <div>
    <button @click="onSubmit">作成</button>
  </div>
</template>

Nuxt3のComopositionAPI

composables/useToDo.js
const useToDo = () => {
  const createToDo = async (values) => {
    const data = await $fetch('/api/todo', {
      method: 'post',
      body: values,
    });
    await refreshNuxtData(); // データのリフレッシュ
    return data; // awaitしてるので何か返してあげる
  }
  return {
    createToDo
  }
}
export default useToDo;

Nuxt3のAPI。サーバーサイドで行う処理部分(SSR)です。ここでPrismaの処理を行います。

server/api/todo.ts
import { PrismaClient, Prisma } from '@prisma/client';

export default defineEventHandler(async (e) => {
  const prisma = new PrismaClient();
  const method = e.req.method;
  
  if (method === 'POST') {
    const body = await readBody(e);
    
    if (!body) {
      const detailError = createError({
        statusCode: 400,
        statusMessage: 'No item provided',
        data: {},
      });
      sendError(e, detailError);
    }
    try {
      const temp = await prisma.todo.create({
        data: body.data,
      });
    } catch (error) {
      console.log(error);
    }
    return temp; // awaitしてるので何か返してあげる
  }
  
});

読み出し(Read)

表示する側

page/index.vue
<script setup>
const { dataToDo } = useToDo();
</script>

<template>
  <div>
    <ul>
      <li v-for="n in dataToDo" :key="n.id">{{ n.title }}</li>
    </ul>
  </div>
</template>

Nuxt3のComopositionAPI

composables/useToDo.js
const useToDo = () => {
  const { data: dataToDo } = useFetch('/api/todo');
  return {
    dataToDo
  }
}
export default useToDo;

Nuxt3のAPI。サーバーサイドで行う処理部分(SSR)です。ここでPrismaの処理を行います。

server/api/todo.ts
import { PrismaClient, Prisma } from '@prisma/client';

export default defineEventHandler(async (e) => {
  const prisma = new PrismaClient();
  const method = e.req.method;
  
  if (method === 'GET') {
    const todo = await prisma.todo.findMany(); // 全件取得
    return todo;
  }
  
});

更新(Update)

表示する側

page/index.vue
<script setup>
const { updateToDo } = useToDo();

const onSubmit = async () => {
  await updateToDo(
    {
      targetId: 1, // 修正するToDoのid
      data: {
        title: '修正したタイトル',
        description: '修正したの概要',
      } 
    }
  );
}
</script>

<template>
  <div>
    <button @click="onSubmit">修正</button>
  </div>
</template>

Nuxt3のComopositionAPI

composables/useToDo.js
const useToDo = () => {
  const updateToDo = async (values) => {
  const data = await $fetch('/api/todo', {
      method: 'put', // ※putであることに注意
      body: values,
    });
    await refreshNuxtData(); // データのリフレッシュ
    return data; // awaitしてるので何か返してあげる
  }
  return {
    updateToDo
  }
}
export default useToDo;

Nuxt3のAPI。サーバーサイドで行う処理部分(SSR)です。ここでPrismaの処理を行います。

server/api/todo.ts
import { PrismaClient, Prisma } from '@prisma/client';

export default defineEventHandler(async (e) => {
  const prisma = new PrismaClient();
  const method = e.req.method;
  
  if (method === 'PUT') { // ※PUTであることに注意
    const body = await readBody(e);
    
    if (!body) {
      const detailError = createError({
        statusCode: 400,
        statusMessage: 'No item provided',
        data: {},
      });
      sendError(e, detailError);
    }
    try {
      const temp = await prisma.todo.update({
        where: { id: body.targetId },
        data: body.data,
      });
    } catch (error) {
      console.log(error);
    }
    return temp; // awaitしてるので何か返してあげる
  }
});

削除(Delete)

表示する側

page/index.vue
<script setup>
const { deleteToDo } = useToDo();

const onSubmit = async () => {
  await deleteToDo(
    {
      targetId: 1, // 削除するToDoのid
    }
  );
}
</script>

<template>
  <div>
    <button @click="onSubmit">削除</button>
  </div>
</template>

Nuxt3のComopositionAPI

composables/useToDo.js
const useToDo = () => {
  const deleteToDo = async (values) => {
  const data = await $fetch('/api/todo', {
      method: 'delete', // ※deleteであることに注意
      body: values,
    });
    await refreshNuxtData(); // データのリフレッシュ
    return data; // awaitしてるので何か返してあげる
  }
  return {
    deleteToDo
  }
}
export default useToDo;

Nuxt3のAPI。サーバーサイドで行う処理部分(SSR)です。ここでPrismaの処理を行います。

server/api/todo.ts
import { PrismaClient, Prisma } from '@prisma/client';

export default defineEventHandler(async (e) => {
  const prisma = new PrismaClient();
  const method = e.req.method;
  
  if (method === 'DELETE') { // ※DELETEであることに注意
    const body = await readBody(e);
    
    if (!body) {
      const detailError = createError({
        statusCode: 400,
        statusMessage: 'No item provided',
        data: {},
      });
      sendError(e, detailError);
    }
    try {
      const temp = await prisma.todo.delete({
        where: { id: body.id },
      });
    } catch (error) {
      console.log(error);
    }
    return temp; // awaitしてるので何か返してあげる
  }
});

以上です。

基本的なソースを貼っ付けるだけですが
詳しくは => https://www.prisma.io/docs/reference/api-reference/prisma-client-reference

また、動作をシミュレートできるPlayGroundも用意されてるのでご活用ください
https://playground.prisma.io/examples/reading/find/find-all?host=playground.prisma.io&path=examples

(余談)なんで作ろうとしたかのきっかけ

最近Linksh | みんなで作るリンク集という個人開発のアプリをテスト公開しました。

バックエンドはFirebase(Firestore)で構築したんですが、開発途中知らぬ間に1日80万読み取りを超える時があり(5万読み取りまで無料枠)、これはイカンと直したものの、それでも1アクセスごとにかなりの読み取り数を発生させており、気がついたら接続数を減らすことにだけに時間を取られ、機能追加やソースコード整理、UI改善など、他に時間を費やすべきところに着手できない自体になりました。

Firebaseは認証と画像アップロード、DBはSQLという選択

で、根本的なところから考えた結果、Firebaseは認証と画像アップロード、DBはSQLという選択が良いような気がしてきました。

Firebaseの認証はとてもシンプルに構築でき、画像アップロードも5Gまで無料なので画像を扱う時も便利です。

フロントエンド技術は新しい方が優れてますが、バックエンド技術は古いものほど安定していると言う、年末に会った神エンジニアの教えに背中を押され、この構成で今後仕事も個人開発もしばらく定着させようかと思います!

Discussion