Next.js + TypescriptでミニCMSを作る（2. Prisma・画面設計・DB設計）

# DBや画面を決めていきます。
:::note info
[前回](https://qiita.com/AndanteSysDes/items/85178d222b9c1dfc7105)の続きです。
:::

今回はDB周りを整理します。
設計～開発を広い範囲でやると大変ので、まずはフェーズ１の「レビュー投稿機能」にだけ焦点を当てて、アジャイル的な感じで進めていきましょう。
基本設計（簡易）レベルを決めてから、プログラミングに移るという流れです。（共同作業者もいないので、詳細設計レベルは省きます。）

> ##### フェーズ１：管理者画面の新規作成
> - 管理者ログイン機能
> - レビュー投稿機能　(CRUD操作ができる)

# 設計 - 制作物のイメージを作ります

## DB設計
仮想クライアントが既存で運営している静的Webサイトから、レビュー機能に利用しているデータを拾い上げる想定で、リレーションを考えていきます。
前回の記事に記載しましたが、レビュー記事は、**ブログタイプとYoutubeタイプの２種類**がある前提です。

### 現在のサイトで表示されているレビュー記事

##### ブログ記事のイメージ
![Review-sample-Blog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547786/6cb506ee-537b-ad05-6e2f-3b74f137c5a0.png)

##### Youtube記事のイメージ
![Review-sample-Youtube.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547786/255e9f96-d076-05eb-02cd-c0e96eafbf7e.png)
(個人的に後で視聴してみようかなと思っているNext.jsの動画をおいてみました。良ければ検索してみてください。)

#### レビューデータのピックアップ

表示されているデータで共通のものは、以下になるかと思います。
- 記事タイトル
- 記事の日付
- 記事の要約文
- 引用元のURL

ブログ記事でのみ必要なデータは、
- 画像URL

Yourube記事でのみ必要なデータは、
- 動画埋め込みコード（これは、YoutubeのURLがわかれば生成できるものとします。）

というところでしょうか。
見えていない部分では、ユーザー公開画面で各種出し分けをするための**フラグ**が必要そうです。
- Youtube記事のフラグ
- トップページの掲載フラグ
- 下書きフラグ

これらは別のテーブルを作って管理することもできますが、今回は設計・実装の負担を減らすためフラグ管理にしてみます。

※本番記事用のテーブルと下書き記事用のテーブルを双方用意する、という設計手法もあるように思いますが、皆さんどちらで実装することが多いでしょうか？？教えて詳しい人！！

#### 製品データの検討
今回の要件では、製品ページにレビュー記事を載せることになります。
ということは、レビューデータを製品データに紐づける必要があります。
製品テーブルの内容精査は別のフェーズで行う予定ですが、エンティティそのものを作って関係性を紐づけておきましょう。ひとまず**ID、製品名称**が設定されていれば現段階は良しとします。
また、仮想クライアントの販売製品は、それぞれ〇〇シリーズのようにグループ分けがなされているようです。
シリーズごとに公開画面の表示順も制御されているので、それも現段階で設計に含めていきます。


### 利用するツールに依存する項目
今回は開発にあたって、[Prisma ORM](https://www.prisma.io/)と[Cloudinary](https://cloudinary.com/)を導入します。それらに依存する項目を洗い出します。

#### Prisma ORM
今回は、１製品に多数のレビュー記事が掲載される想定です。対して、１つのレビュー記事が複数製品に共通して掲載されることもありえます。同じシリーズの製品比較の記事とかを想定しています。
そうすると、レビューテーブルと製品テーブルのリレーションは多対多の関係になります。
Prismaを使ってこの関係を表すには、明示的（Explicit）に設計するか暗示的（Implicit）にPrismaに設計を任せるかのどちらかを選択します。
今回は明示的にする理由もないので、暗示的にPrisma自動生成に任せる方針にします。
詳しい実装は後述。

https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations

#### Cloudinary
Next.js単体では、管理画面でアップロードした画像を即座に公開画面に反映することができないので、画像をアップロード及び配布できる場所が必要になります。そこで今回はCloudinaryを利用します。
Cloudinaryは、画像をアップロードすると**パブリックID**なるものが振られます。画像の表示は画像のURLを使うことも、パブリックIDを使うこともできそうなので、現段階ではどちらもテーブルに格納することにしましょう。

## ER図
・・・・・・っていうのを、実際に実装しながらER図にまとめてみたのが以下です。
今回は作図ツールに[Miro](https://miro.com/ja/templates/entity-relationship-diagram/)を使ってみました。緑色の`_ProductToReview`は、Prismaが自動生成してくれる中間テーブルで、「そういうつなぎ方になっているんだな」とだけご理解いただければOKです。
Next.js, Prismaで使いやすいよう、自動生成以外はキャメルケースにて記載しています。

![ER-Review-Product.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547786/7e0346e0-5e9b-64eb-c464-79241cf74cd6.png)


## 画面設計
ユーザー公開画面の設計はフェーズ３、製品やジャンルの登録はフェーズ２でやることにするので、今回はレビュー記事を投稿する管理画面の設計だけ進めます。現時点では画面遷移図を作るほど画面数も多くないので、必要になったら作ることにします。
これは初期のざっくりワイヤーフレームなので、実装中にあれこれ足し引きしたくなったら、その都度対応していく感じです。
実際の案件ではきちんとクライアントレビューしてもらってFIXしていきましょうね。火種になりかねません。

`管理画面　レビュー一覧ページ`
![レビュー一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547786/499abfaa-31e0-dc34-b0d8-d0f7b2337048.png)

`管理画面　レビュー投稿ページ`
![レビュー原稿　入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547786/5037e948-dce9-fa09-3ecd-f6bab448d979.png)


# 実装

## DBスキーマ
前回まででNext.jsの`create app`は終わったので、Prisma導入と、Docker上のDBとの接続、スキーマ作成を進めていきます。
[Prisma公式](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-postgresql)を見ながら進めます。
`create-next-app`で作成したアプリディレクトリへ移動し、以下のコマンドを入力します。

```bash:
npm init -y
npm install typescript ts-node @types/node --save-dev
```

クイックスタートには、↓のコマンドも記載がありますが、Next.jsの`create-next-app`の際に'tsconfig.json'が生成されているので、不要だと思います。
```
npx tsc --init
```

#### Prisma CLIのダウンロード

```
npm install prisma --save-dev
```

#### Prismaスキーマファイルの作成と接続設定

```
npx prisma init
```

ここまで進めると、Next.jsプロジェクトファイル内に`prisma`フォルダが生成されています。中に`schema.prisma`というファイルがあり、ここにどんどん追記していくことでDB設計をしていくことができます。
Prismaの良いところは、どのDBSを選んでもアプリ用ソースコードの記述が統一され、かつフロント側からわかりやすい記法が使えるところです。そのあたりは後々確認することにします。

まず、接続するデータベースの設定を書き換えます。初期状態だとPostgreSQLになっているので、これをMySQLに変更します。

```diff_javascript:schema.prisma
datasource db {
-  provider = "postgresql"
+  provider = "mysql"
  url      = env("DATABASE_URL")
}
```

次に、プロジェクトのルートディレクトリに作成されている.envファイルを編集します。
MySQLの場合の接続URLの形式は、`mysql://USER:PASSWORD@HOST:PORT/DATABASE`という形になっています。英大文字で書かれた部分は、前回作ったDocker上のMySQLの設定に合わせて設定しましょう。ここに記載した`DATABASE_URL`の値が、上述`schema.prisma`のurlに反映されます。

```diff_javascript:.env
- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
+ DATABASE_URL="mysql://hoge:huga@localhost:3306/database"
```

#### DBスキーマの設定
`schema.prisma`にDBスキーマを追記していきます。
リレーションの記載方法は、[こちら](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations)を確認しながら進めます。

※本来であれば、DB上に記録する日時は日本時間に合わせたいところなのですが、現在Prismaを使ってそれを実現するのは大変困難なため、UTCで記録することにしています。アップデート求む・・・！

```javascript:schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

//以下を追記します

model Review {
  id          Int       @id @default(autoincrement())
  title       String?   @db.Text
  detail      String?   @db.Text
  url         String?   @db.VarChar(2100)
  isYoutube   Boolean?
  youtubeId   String?   @db.VarChar(30)
  articleDate DateTime? @db.DateTime(0) // Based on UTC time locale
  imgUrl      String?   @db.VarChar(2100)
  imgPublicId String?   @db.VarChar(40)
  displayTop  Boolean?
  isDraft     Boolean?
  createdAt   DateTime @default(now()) @db.DateTime(0)
  updatedAt   DateTime @updatedAt @db.DateTime(0)

  //Productテーブルとの多対多関係
  products    Product[]  
}

model Product {
  id            Int       @id @default(autoincrement())
  name          String?   @unique @db.VarChar(50)
  isDiscontinue Boolean   @default(false)
  isDraft       Boolean?
  createdAt     DateTime @default(now()) @db.DateTime(0)
  updatedAt     DateTime @updatedAt @db.DateTime(0)
  
  //Reviewテーブルとの多対多関係
  reviews       Review[] 
  
  //Seriesテーブルとの一対多関係（一の方）
  seriesId      Int      
  series        Series    @relation(fields: [seriesId], references: [id]) 

}

model Series {
  id           Int       @id @default(autoincrement())
  name         String    @unique @db.VarChar(30)
  displayOrder Int
  createdAt    DateTime @default(now()) @db.DateTime(0)
  updatedAt    DateTime @updatedAt @db.DateTime(0)

  //Productテーブルとの一対多関係 （多の方）
  products     Product[] 
}

```

この先もフィールドの追加など、DB設計が変動していく予定なので、`migration`ではなく`db push`というプロトタイピング用コマンドを使用していきます。

```
npx prisma db push
```

https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema



:::note warn

`db push`は、特定の条件下での利用は推奨されていません。以下、公式サイトの引用とDeepLでの翻訳文を掲載します。

>db push is not recommended if:
>
> - You want to replicate your schema changes in other environments without losing data. You can use db push for prototyping, but you should use migrations to commit the schema changes and apply these in your other environments.
> - You want fine-grained control over how the schema changes are executed - for example, renaming a column instead of dropping it and creating a new one.
> - You want to keep track of changes made to the database schema over time. db push does not create any artifacts that allow you to keep track of these changes.
> - You want the schema changes to be reversible. You can use db push again to revert to the original state, but this might result in data loss.

>db pushは次のような場合には推奨されません：
>
> - データを失うことなく、スキーマの変更を他の環境に複製したい場合。db push をプロトタイピングに使うことはできますが、スキーマの変更をコミットし、他の環境に適用するにはマイグレーションを使うべきです。
> - スキーマ変更の実行方法をきめ細かく制御したい。例えば、あるカラムを削除して新しいカラムを作成する代わりに、そのカラムの名前を変更したい。
> - データベーススキーマに加えられた変更を時系列で追跡したい。db pushはこれらの変更を追跡するための成果物を作成しない。
> - スキーマの変更を元に戻せるようにしたい。db pushを再度使用して元の状態に戻すことはできますが、その結果データが失われる可能性があります。

:::

Prismaクライアントを生成します。これでアプリ用コード内でPrisma機能を使うことができます。

https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/generating-prisma-client


```
npx prisma generate
```


#### DBの同期確認
ここまでできたら、DBにスキーマが反映されているかを確認しましょう！
Docker用のディレクトリ`sample-db`に移動してから以下のコマンドを打つことで、コンテナ内に入ることができます。

```
docker-compose exec db /bin/bash
```

コンテナに入れたらMySQLを起動しましょう。

```bash
sh-4.4# mysql -u hoge -p -D database
Enter password: # MySQLに設定したパスワードを入れます。この場合はhuga。
```

以下の表示が出たら、MySQL起動OKです。
```
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.3.0 MySQL Community Server - GPL

Copyright (c) 2000, 2024, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
```

コマンドラインが`mysql>`に変わりますので、ここからはSQLを打ってテーブルを確認していきます。

```
mysql> show tables;
+--------------------+
| Tables_in_database |
+--------------------+
| Product            |
| Review             |
| Series             |
| _ProductToReview   |
+--------------------+
4 rows in set (0.00 sec)
```

きちんと指定したテーブルが出来上がっていますね！
カラムも確認していきます。

```
mysql> describe Review;
+-------------+---------------+------+-----+-------------------+-------------------+
| Field       | Type          | Null | Key | Default           | Extra             |
+-------------+---------------+------+-----+-------------------+-------------------+
| id          | int           | NO   | PRI | NULL              | auto_increment    |
| title       | text          | YES  |     | NULL              |                   |
| detail      | text          | YES  |     | NULL              |                   |
| url         | varchar(2100) | YES  |     | NULL              |                   |
| isYoutube   | tinyint(1)    | YES  |     | NULL              |                   |
| youtubeId   | varchar(30)   | YES  |     | NULL              |                   |
| articleDate | datetime      | YES  |     | NULL              |                   |
| imgUrl      | varchar(2100) | YES  |     | NULL              |                   |
| imgPublicId | varchar(40)   | YES  |     | NULL              |                   |
| displayTop  | tinyint(1)    | YES  |     | NULL              |                   |
| isDraft     | tinyint(1)    | YES  |     | NULL              |                   |
| createdAt   | datetime      | NO   |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updatedAt   | datetime      | NO   |     | NULL              |                   |
+-------------+---------------+------+-----+-------------------+-------------------+
13 rows in set (0.00 sec)

mysql> describe Product;
+---------------+-------------+------+-----+-------------------+-------------------+
| Field         | Type        | Null | Key | Default           | Extra             |
+---------------+-------------+------+-----+-------------------+-------------------+
| id            | int         | NO   | PRI | NULL              | auto_increment    |
| seriesId      | int         | NO   | MUL | NULL              |                   |
| name          | varchar(50) | YES  | UNI | NULL              |                   |
| isDiscontinue | tinyint(1)  | NO   |     | 0                 |                   |
| isDraft       | tinyint(1)  | YES  |     | NULL              |                   |
| createdAt     | datetime    | NO   |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updatedAt     | datetime    | NO   |     | NULL              |                   |
+---------------+-------------+------+-----+-------------------+-------------------+
7 rows in set (0.00 sec)

mysql> describe Series;
+--------------+-------------+------+-----+-------------------+-------------------+
| Field        | Type        | Null | Key | Default           | Extra             |
+--------------+-------------+------+-----+-------------------+-------------------+
| id           | int         | NO   | PRI | NULL              |                   |
| name         | varchar(30) | NO   | UNI | NULL              |                   |
| displayOrder | int         | NO   |     | NULL              |                   |
| createdAt    | datetime    | NO   |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updatedAt    | datetime    | NO   |     | NULL              |                   |
+--------------+-------------+------+-----+-------------------+-------------------+
5 rows in set (0.00 sec)

mysql> describe _ProductToReview;
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| A     | int  | NO   | PRI | NULL    |       |
| B     | int  | NO   | PRI | NULL    |       |
+-------+------+------+-----+---------+-------+
2 rows in set (0.00 sec)
```

良さそうですね！修正点がある場合は、`schema.prisma`を訂正して、再度`db push`をかけます。簡単ですね！

### Prisma Studio
アプリフォルダに移動したあと、以下のコマンドを打つことで、`http://localhost:5555`でPrisma Studioを起動することができます。テーブル操作がGUIでできるので、ダミーデータを作るときなどに使いましょう。
SQLが苦手な方は、Prisma Studioでテーブルができているか確認してください。

```
npx prisma studio
```


# 次回
次回はNext.jsの特徴をまとめて、実装の方針を考えます。