【Node.js+typeORM+MySQL】実務未経験＆独学でおすすめワイン診断アプリを作ってみた【バックエンド】

先日投稿した記事のバックエンド側の解説記事になります。
👇フロント側の実装も見てみたい方は下記記事をご参照ください！

https://qiita.com/mamimami0709/items/6633da2cafbbfd1841cb

バックエンド側ですが、主にどのような処理をしているのかと言うと
ワイン情報を全てDBに入れてあるので、その出し入れの処理や入稿といった処理をやっております。
DBとのやりとりはtypeORMというORマッパーを使っております。
今後、ログイン機能やお気に入り機能なども追加していきたいと思ってます！



## 作成したものの紹介
WineChecker
（GitHub👇)

https://github.com/mami0709/WineAppExpress/


![video-output-3EE04214-F665-49C6-97D6-5920AE5425BA_MOV_AdobeExpress (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819187/332395c4-be74-bcba-cd39-2102e37bac62.gif)



先日までHerokuへデプロイしていたんですが、有料になってしまったので今は非公開となっております。。
なのでGifでお許しを。

機能としては
__・自分に合ったおすすめワインの診断__
__・DBを活用したおすすめ一覧__


## ディレクトリ構成図
```
├-node_modules
├-public -- images
├--- src -┐
│.        ├- db-- migrations   
│.        ├- entities --┐
│         │             ├- Image.ts
│         │             ├- Wine.ts
│         │             ├- Winery.ts
│         │             └- WineType.ts
│         └- data-source.ts             
├- index.ts                      
├- .gitinore
├- pakage.json
├- package-lock.json
└- tsconfig.json
```

## ライブラリの追加
```
$ npm install typeorm
$ npm install mysql
$ npm install reflect-metadata
$ npm install @types/mysql --save-dev
```

# 1.****TypeORMの設定****

少し前のTypeORMだと`ormconfig.json` に記述していたそうなのですが、アプデにより変わったそうです。

(最初分からずに`ormconfig.json`に書いてしまってうまく動かず初っ端から躓きました…私はOiiitaを見てましたが、みんなはちゃんと公式リファレンスを読もうね!)

ということで`ormconfig.json` ではなく、`src/data-source.ts` に下記のように記述します。

```tsx:src/data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
  type: "mysql",
  host: "localhost",
  port: 3306,
  username: "ユーザーネーム",
  password: "パスワード",
  database: "wine_app",
  synchronize: true,
  logging: false,
  entities: [
    "src/entities/*.ts",
],
  migrations: [
    "src/db/migrations/*.ts",
  ],
  subscribers: [],
});
```

# 2.DB作成

事前準備として、mySQLのDBをあらかじめ作っておきましょう。

ターミナルからmySQLへアクセスして上記で設定した `wine_app` という名前のDBを作成してください。

DBの作成方法については[こちら](https://prog-8.com/docs/mysql-database-setup)のprogateさんの記事が分かりやすかったです！


# 3.****Entity を用意****

データベースとプログラムとの間でマッピングされたデータが、どのようなデータ構造をとるのかを定義します。
TypeORM では、このようなデータモデルを Entity という名称で表現するようです。

まずはsrcフォルダの中に `db` と `entities` という名前のフォルダを作成。

まずはメインのWineのEntityを作成しましょう。

### ①Wine.tsの定義

```tsx:src/entities/Wine.ts
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable} from 'typeorm'
import Winery from "./Winery";
import WineType from "./WineType";
import Image from "./Image";

@Entity('Wine')
export class Wine {
  @PrimaryGeneratedColumn()
	id!: number;

  @Column()
  name!: string; 

  @Column({
    type: "text",
    nullable: true,
  })
  description!: string;

  @Column({
    type: "text",
    nullable: true,
  })
  oneWord: string;

  @Column({
    type: "text",
    nullable: true,
  })
  country: string;

  @Column({
    type: "text",
    nullable: true,
  })
  breed: string;

  @Column({
    type: "text",
    nullable: true,
  })
  link: string;

  @ManyToOne(() => Winery, (winery) => winery.wines)
  winery!: Winery;

  @ManyToMany(() => WineType)
  @JoinTable()
  wineTypes!: WineType[];

  @ManyToOne(() => Image, (image) => image.wines)
  image!: Image;
  wine: any;

}

export default Wine;
```

初見だと何書いてるか分からないと思います。私も最初意味不明でした。

まあ簡単な部分から説明すると `id` は `number`、 `name` は `string` で定義してます。これは分かりやすいかと思います。

typeORMには型の定義がいくつかあるので、それに合うものを一つずつ定義していきます。

詳しくは[公式リファレンス](https://typeorm.io/)を読んでください。


:::note info
<aside>
💡 リレーションについて

次に ManyToMany や　ManyToOne についてです。これはリレーションと言われるもので依存関係を示します。
詳しくは[こちら](https://orkhan.gitbook.io/typeorm/docs/many-to-one-one-to-many-relations)を読んでください。
要は親と子を設定し、それらをリレーション(繋げている)という認識です。
例えばAという親にBという子を設定したとしましょう。AにはBの複数のインスタンスが含まれるが、Bには１つのインスタンスしか含まれない関係ということです。これがよくいう１対多。多対１です。
例) User (ManyToOne)とPhoto(OneToMany)のEntityがあった場合、Userは複数の写真を持つことが出来ますが、Photoは１つのユーザー情報しか保つことが出来ない。

</aside>
:::


ということで今回は、Winery (産地)はWine側では複数の情報を持ち、Winery側では1つの情報をもつだけでいいので`ManyToOne`を定義。

Imageも同様。WineTypeについては同じように`ManyToOne`で良いとも思ったのですが、複数の情報をもたせる可能性もあったので、ここでは`ManyToMany`で設定しています。


### ②Winery.tsの設定

```tsx:src/entities/Winery.ts
import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from 'typeorm'
import Wine from "./Wine";

@Entity('Winery')
export class Winery {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column({
    type: "text",
    nullable: true,
  })
  description!: string;

  @OneToMany(() => Wine, (wine) => wine.winery)
  wines!: Wine[];
}

export default Winery;
```

### ③WineType.tsの設定

```tsx:src/entities/WineType.ts
import {Entity, PrimaryGeneratedColumn, Column} from 'typeorm'

@Entity()
export class WineType {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column({
    type: "text",
    nullable: true,
  })
  description!: string;
}

export default WineType;
```

### ④Image.tsの設定

```tsx:src/entities/Image.ts
import {Entity, PrimaryGeneratedColumn, Column, OneToMany,} from 'typeorm'
import Wine from './Wine';

@Entity()
export class Image {
  @PrimaryGeneratedColumn()
	id!: number;

  @Column({ type: 'varchar', nullable: true })
  name! : string;
  // name!: any | null;

  @Column()
  src: string;

  @OneToMany(() => Wine, (wine) => wine.image)
  wines!: Wine[];

}

export default Image;
```

Wine.tsと同様に他のEntityも定義していく。

# 4.****migrationファイル作成****

migrationとは「現在のDBスキーマ」と「modelsフォルダ内のモデル定義」を比較し、差分を作って自動的にmigrationを作成してくれます。素晴らしい機能ですね！

ではmigrationのコマンドを実行していきましょう。

```tsx
//migrationファイルを作成
ts-node ./node_modules/.bin/typeorm migration:generate -d src/data-source.ts src/db/migrations/Initialize
//作成されたmigrationファイルが認識されているかチェック
ts-node ./node_modules/.bin/typeorm migration:show -d src/data-source.ts
//migrationを実行
ts-node ./node_modules/.bin/typeorm migration:run -d src/data-source.ts
```

そうすると、ormconfigで指定したディレクトリ（記事ではsrc/db/migrations）に、タイムスタンプ+マイグレーション名のファイルが出来上がります。

DBにも空のテーブルや型定義のみ設定されているかと思います。確認してみてください。

ちなみにこうゆうDBの中身を確認するときにいちいちターミナルでコードを毎回うって確認するのも大変なので、SequelAceというGUIアプリなどおすすめです。コードを打たなくても視覚的に確認できます。

最初これを知らずに毎回コード打ち込んでて大変でした…w

# 5.****index.tsの作成****

ここまできたらデータの入稿や、フロントエンドとの繋ぎ込み処理をしていきます。

### ①DB接続

```jsx:index.ts
const port = 8080;
const express = require("express");
const app = express();
app.get('/wine', async (req: Request, res: Response, next: any) => {
  });

  //expressミドルウェア(画像表示用)
  app.use(express.static(__dirname + "/public"));

  app.listen(port, async () => {
    // データベース接続を確立する
    try {
      await AppDataSource.initialize();
      console.log("Data Source has been initialized!");
    } catch (err) {
      console.error("Error during Data Source initialization:", err);
      throw err;
    }
```

console.logで表示されているのはちゃんとDBに接続できているか確認するためのものです。

### ②ワインの一覧を入稿する処理

```jsx
const count = await AppDataSource
        .getRepository(Wine)
        .count();
  
    // レコードがない場合データを入稿
    if (count === 0) {
      // WineTypeの入稿
      await AppDataSource
          .createQueryBuilder()
          .insert()
          .into(WineType)
          .values([
            { name: "白ワイン" },
            { name: "赤ワイン"},
          ])
          .execute();
// Winery（産地)の入稿
      await AppDataSource
      .createQueryBuilder()
      .insert()
      .into(Winery)
      .values([
        { name: "ボルドー"},
      ])
      .execute();

      // Imageの入稿
      await AppDataSource
          .createQueryBuilder()
          .insert()
          .into(Image)
          .values([
            { name: "01.webp" ,src: "/images/01.webp"},
          ])
          .execute();
// Wineテーブルにinsert
      const wineA = new Wine();
      wineA.name = "ドメーヌ バロン ド ロートシルト ポーイヤック レゼルブ スペシアル";
      wineA.winery = wineryA!;
      wineA.wineTypes = wineTypesA!;
      wineA.image = wineImage01!;
      wineA.country = "フランス";
      wineA.oneWord = "力強い飲み口ときめ細やかでしっかりとした果実味";
      wineA.breed = "カベルネ・ソーヴィニヨン、メルロー";
      wineA.link = "https://www.amazon.co.jp/dp/B00ISJ9DVC?ots=1&tag=s02a3-22&th=1";
      wineA.description = "最高級ワインの産地として知られる、フランスのメドック地区で造られたフルボディ赤ワインです。世界的に有名な「シャトー ラフィット・ロートシルト」を運営する、ロスチャイルド家が醸造しています。カベルネ・ソーヴィニヨンとメルローがブレンドされ、力強い飲み口と、きめ細やかでしっかりとした果実味が特徴です。使われているブドウの一部は、第1級の格付けのラフィット・ロートシルトで収穫されたモノ。エレガントで高級感のある味わいの赤ワインが5000円以下で楽しめる、コストパフォーマンスの高さもポイントです。";
      await AppDataSource.manager.save(wineA);
```

まずはレコードが０の時のレコードを入稿。
`AppDataSource`とは `data-source.ts` で設定したDBのこと。そこにワインの情報を入稿していく。

入稿は `createQueryBuilder` を使用。`insert()`　は挿入のこと。
`into(Winery)`　で入稿するテーブルを定義。
`values`　で入稿するものを設定。

最後の

```jsx
// Wineテーブルにinsert
      const wineA = new Wine();
      wineA.name = "ドメーヌ バロン ド ロートシルト ポーイヤック レゼルブ スペシアル";
      wineA.winery = wineryA!;
      wineA.wineTypes = wineTypesA!;
      wineA.image = wineImage01!;
      wineA.country = "フランス";
      wineA.oneWord = "力強い飲み口ときめ細やかでしっかりとした果実味";
      wineA.breed = "カベルネ・ソーヴィニヨン、メルロー";
      wineA.link = "https://www.amazon.co.jp/dp/B00ISJ9DVC?ots=1&tag=s02a3-22&th=1";
      wineA.description = "最高級ワインの産地として知られる、フランスのメドック地区で造られたフルボディ赤ワインです。世界的に有名な「シャトー ラフィット・ロートシルト」を運営する、ロスチャイルド家が醸造しています。カベルネ・ソーヴィニヨンとメルローがブレンドされ、力強い飲み口と、きめ細やかでしっかりとした果実味が特徴です。使われているブドウの一部は、第1級の格付けのラフィット・ロートシルトで収穫されたモノ。エレガントで高級感のある味わいの赤ワインが5000円以下で楽しめる、コストパフォーマンスの高さもポイントです。";
      await AppDataSource.manager.save(wineA);
```

でワインそれぞれに値を振り、内容を`await AppDataSource.manager.save(wineA);`で保存。

### ③ワイン一覧を返す処理

```jsx
// ワインの一覧を返す
  app.get('/wines', async (req: Request, res: Response, next: any) => {
    const wineRepository = AppDataSource.getRepository(Wine);
    const wines = await wineRepository.find({
      relations: {
        winery: true,
        wineTypes: true,
        image: true
      },
    });
    res.json({
      result: "SUCCESS",
      data: wines,
    });
  });
```

②で設定した情報を /wines のURLに返す処理。

```jsx
const wineRepository = AppDataSource.getRepository(Wine);
```

`getRepository`　は先ほど入稿した情報が入っている。それをWine.tsで定義しているEntityと紐づける。

`relations`でリレーションをtrueに。

フロントエンドではそれぞれをidで管理するので、idを定義。

```jsx
where: {
        id: Number(req.params.id),
      }
```

最後に"SUCCESS”とwinesの情報をdataで渡してあげる。

```jsx
console.log(wines);
    res.json({
      result: "SUCCESS",
      data: wines,
    });
```

④ワインの詳細情報を返すエンドポイント作成

```jsx
//ワインの詳細情報を返すエンドポイント作成(/wine/:id)
    app.get('/wine/:id', async (req: Request, res: Response, next: any) => {
    const wineRepository = AppDataSource.getRepository(Wine);
    const wines = await wineRepository.findOne({
      relations: {
        winery: true,
        wineTypes: true,
        image: true
      },
      where: {
        id: Number(req.params.id),
      }
    });
      console.log(wines);
    res.json({
      result: "SUCCESS",
      data: wines,
    });
    });
```

概ね上記のワイン情報を返す処理と同様。

一つ違うのはワイン情報では`find`で返していたが、ここでは複数の情報ではなく1つの情報でいいので、`findOne`を使用する。

あとは一緒。

### ⑤確認

これでバックエンドの処理が完了。

```jsx
$ ts-node index.ts
```

で実行してしっかりデータが入稿されているか確認してみよう。

[http://localhost:8080/wines](http://localhost:8080/wines)

にアクセスしてJSON形式で情報がずらっと出てきたら成功！
前回のフロントエンドでフロントの部分は完成しているので、フロントのURLへアクセスしてみよう。

[http://localhost:3000/](http://localhost:3000/)

診断結果、おすすめ一覧、詳細情報でそれぞれDBからの情報がしっかり表示されていればOK！




## 参考にさせてもらった記事

https://qiita.com/techneconn/items/0655f4d88187b65a8520#%E5%B0%8E%E5%85%A5

https://qiita.com/les-r-pan/items/8f11d90eb0538cac2812

https://qiita.com/ut0n/items/83b5836bc542c8f9bc71

https://kazuhira-r.hatenablog.com/entry/2022/03/13/235304