先日投稿した記事のバックエンド側の解説記事になります。
👇フロント側の実装も見てみたい方は下記記事をご参照ください!
バックエンド側ですが、主にどのような処理をしているのかと言うと
ワイン情報を全てDBに入れてあるので、その出し入れの処理や入稿といった処理をやっております。
DBとのやりとりはtypeORMというORマッパーを使っております。
今後、ログイン機能やお気に入り機能なども追加していきたいと思ってます!
作成したものの紹介
WineChecker
(GitHub👇)
先日まで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
に下記のように記述します。
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の作成方法についてはこちらのprogateさんの記事が分かりやすかったです!
3.Entity を用意
データベースとプログラムとの間でマッピングされたデータが、どのようなデータ構造をとるのかを定義します。
TypeORM では、このようなデータモデルを Entity という名称で表現するようです。
まずはsrcフォルダの中に db
と entities
という名前のフォルダを作成。
まずはメインのWineのEntityを作成しましょう。
①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には型の定義がいくつかあるので、それに合うものを一つずつ定義していきます。
詳しくは公式リファレンスを読んでください。
次に ManyToMany や ManyToOne についてです。これはリレーションと言われるもので依存関係を示します。
詳しくはこちらを読んでください。
要は親と子を設定し、それらをリレーション(繋げている)という認識です。
例えばAという親にBという子を設定したとしましょう。AにはBの複数のインスタンスが含まれるが、Bには1つのインスタンスしか含まれない関係ということです。これがよくいう1対多。多対1です。
例) User (ManyToOne)とPhoto(OneToMany)のEntityがあった場合、Userは複数の写真を持つことが出来ますが、Photoは1つのユーザー情報しか保つことが出来ない。
ということで今回は、Winery (産地)はWine側では複数の情報を持ち、Winery側では1つの情報をもつだけでいいのでManyToOne
を定義。
Imageも同様。WineTypeについては同じようにManyToOne
で良いとも思ったのですが、複数の情報をもたせる可能性もあったので、ここではManyToMany
で設定しています。
②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の設定
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の設定
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のコマンドを実行していきましょう。
//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接続
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に接続できているか確認するためのものです。
②ワインの一覧を入稿する処理
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);
まずはレコードが0の時のレコードを入稿。
AppDataSource
とは data-source.ts
で設定したDBのこと。そこにワインの情報を入稿していく。
入稿は createQueryBuilder
を使用。insert()
は挿入のこと。
into(Winery)
で入稿するテーブルを定義。
values
で入稿するものを設定。
最後の
// 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);
で保存。
③ワイン一覧を返す処理
// ワインの一覧を返す
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に返す処理。
const wineRepository = AppDataSource.getRepository(Wine);
getRepository
は先ほど入稿した情報が入っている。それをWine.tsで定義しているEntityと紐づける。
relations
でリレーションをtrueに。
フロントエンドではそれぞれをidで管理するので、idを定義。
where: {
id: Number(req.params.id),
}
最後に"SUCCESS”とwinesの情報をdataで渡してあげる。
console.log(wines);
res.json({
result: "SUCCESS",
data: wines,
});
④ワインの詳細情報を返すエンドポイント作成
//ワインの詳細情報を返すエンドポイント作成(/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
を使用する。
あとは一緒。
⑤確認
これでバックエンドの処理が完了。
$ ts-node index.ts
で実行してしっかりデータが入稿されているか確認してみよう。
にアクセスしてJSON形式で情報がずらっと出てきたら成功!
前回のフロントエンドでフロントの部分は完成しているので、フロントのURLへアクセスしてみよう。
診断結果、おすすめ一覧、詳細情報でそれぞれDBからの情報がしっかり表示されていればOK!
参考にさせてもらった記事