🌏

TypeScriptとPostgreSQLを用いたcrudなアプリケーション開発 ~Backend ローカル環境セットアップ編~

2022/02/11に公開

作るもの

crud処理をするシンプルなアプリケーション。設計図も含め詳細は後ほど記載する。

実行環境

  • M1Mac
  • Node.js v14.16.0
  • Docker v20.10.12
  • psql (PostgreSQL) 14.1
  • TypeScript 4.5.5

1. DB選定

個人開発でcrudなアプリケーションを作る上でDBを安く抑えたかった。(学生だし。)いろいろなクラウドでコストを算出したところ、永久無料枠のあるHerokuPostgres@Herokuを利用することにした。算出の過程でAWSって意外と高いんだな〜という印象を持った。確か一番低スペックで月2000円とかした気がする。
参考記事:
https://qiita.com/nh321/items/e8c63b02e3fa9c28837f

フォルダ構成

crud-front 
crud-backend
|- app.ts
|- Repository = TyprORM
|- MyProject - inedx.ts
             |- ormconfig.json
	     - src - index.ts
	           |- entity - Choice.ts
		             |- Main.ts
crud-db

環境構築

1. PostgreSQLのサーバーを構築

ローカルで動作を確認するために、docker-composeでPostgreSQLのサーバーを立てていく。
以下の記事を参考にした。
https://mebee.info/2020/12/04/post-24686/

1. docker-compose.yml作成

version: '3'

services:
  postgres:
    image: postgres:latest
    restart: always
    environment:
      POSTGRES_USER: mebee
      POSTGRES_PASSWORD: password
      PGPASSWORD: password123
      POSTGRES_DB: sample
      TZ: "Asia/Tokyo"
    ports:
      - 5432:5432
    volumes:
      - postgres:/var/lib/postgresql/data

  pgadmin:
    image: dpage/pgadmin4
    restart: always
    ports:
      - 81:80
    environment:
      PGADMIN_DEFAULT_EMAIL: info@mebbe.info
      PGADMIN_DEFAULT_PASSWORD: password
    volumes:
      - pgadmin:/var/lib/pgadmin
    depends_on:
      - postgres

volumes:
  postgres:
  pgadmin:

docker-compose.ymlではdbの定義をしている。

2. pgadminを起動

以下のコマンドでdockerを立ち上げ、http://localhost:81
を入力しpgadminを起動。

$ docker-compose up -d

3. データベースを作成

記事に従ってsampleデータベースを作成していく。

2. ローカルで動作を確認

$ npm run start
MissingDriverError: Wrong driver: "postgresql" given. Supported drivers are: "aurora-data-api", "aurora-data-api-pg", "better-sqlite3", "capacitor", "cockroachdb", "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "nativescript", "oracle", "postgres", "react-native", "sap", "sqlite", "sqljs".

早速エラーが出た。
ormconfig.jsonのtypeプロパティの値がpostgresqlってなってたのが原因。以下のようにtypeに
修正を加える。

ormconfig.json
{
   "type": "postgres",
   "host": "localhost",
   "port": 5432,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ],
   "cli": {
      "entitiesDir": "src/entity",
      "migrationsDir": "src/migration",
      "subscribersDir": "src/subscriber"
   }
}

再度実行。するとまた違う内容のエラーが。

$ npm start
error: password authentication failed for user "test"

docker-compose.ymlでdbの定義をしているのに対し、ormconfig.jsonではdb接続のための定義が書かれている。だからdocker-compose.ymlに合わせてormconfig.jsonの内容を書き換えねばならない。username, password, database の値を適切に変えていく。

ormconfig.json
{
   "type": "postgres",
   "host": "localhost",
   "port": 5432,
   "username": "mebee",
   "password": "password",
   "database": "sample",
             :
             :
             :
}

3. PostgreSQLに接続

crud-backendリポジトリ直下にpsqlコマンドをインストールし、PostgreSQLに接続していく。

$ brew update
$ brew install libpq
$ echo 'export PATH="/usr/local/opt/libpq/bin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
$ psql --version
$ psql -h 0.0.0.0 -p 5432 -d sample -U mebee

PostgreSQL のDBインスタンスには、次のコマンドでログイン。DB接続情報を次のとおりオプションに指定しコマンドを実行。スペースはちゃんと入力するように。passwordの部分はpasswordだけ記述すればok。--dbnameのうしろに\はいらない。

psql \
   --host=localhost \
   --port=5432 \
   --username=mebee \
   --password \
   --dbname=test

参考記事:
https://zenn.dev/hdmt/articles/80e12573ec3a9051624b

2. TypeOrmのインストール

今回Clean Architectureを採用。以下の公式ドキュメントを参照してTypeORMをインストールしていく。
https://typeorm.io/#/
crud-backendリポジトリ内で以下のコマンドを実行。

$ sudo npm install typeorm -g

以下のコマンドを実行し、crud-backendリポジトリ内にMyProjectフォルダを作成する。

$ typeorm init --name MyProject --database postgressql
Project created inside /Users/xxxx/Documents/GitHub/crud-backend/MyProject directory.
$ npm i 
sh: ts-node: command not found

エラーが表示された。実行するディレクトリのパスが間違っていたので以下のように変更。MyProject直下でnpm i。

$ cd MyProject
$ npm install

作成されたormconfig.json を編集する。

ormconfig.json
{
   "type": "postgresql",
   "host": "localhost",
   "port": 5432,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ]
}

3. テーブルの作成

1. DB正規化

DBに格納されるオリジナルのデータは以下のようなものを想定している。

{
    "questionId" : "buMQwLVq5wZ2",
    "isPublished" : true,
    "text" : "",
    "correctChoiceId" : "QGYqT8Z0eieu4"
    "choices" : [
	{
	    "choiceId" : "QGYqT8Z0eieu1",
	    "text" : "AlexNet"
	},
	{

	    "choiceId" : "QGYqT8Z0eieu2",
	    "text" : "GoogLeNet"
	}
    ],
}
],
    "categories" : ""
}

非正規形は以下の通り。

第一正規形は以下の通り。

第二正規形は以下の通り。

4. Entity作成

1. Entityの作成

以下の公式ドキュメントを参照し、Many-to-One, One-to-Many のEntityを作成していく。
https://typeorm.io/#/one-to-one-relations

src/entity/Choice.ts
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne} from "typeorm";
import { Main } from "./Main";

@Entity()
export class Choice {

    @PrimaryGeneratedColumn()
    choiceId?: string;

    @Column()
    choiceText?: string;

    @ManyToOne(() => Main, main => main.choices)
    main: Main;
}

src/entity/Main.ts
import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from "typeorm";
import { Choice } from "./Choice";

@Entity()
export class Main {

    @PrimaryGeneratedColumn()
    questionId: string;

    @Column()
    isPublished?: boolean;

    @Column()
    correctChoiceId?: string;

    @Column({ nullable: true })
    question?: string;

    @Column({ nullable: true })
    categories?: string;

    @OneToMany(() => Choice, choice => choice.main)
    choices: Choice[];

}
src/index.ts
// import "reflect-metadata";
import {createConnection} from "typeorm";
import {Choice} from "./entity/Choice";
import {Main} from "./entity/Main";

createConnection(
).then(async connection => {
    const choice1 = new Choice();
    choice1.choiceText = "レアジョブ";
    await connection.manager.save(choice1);
    const choice2 = new Choice();
    choice2.choiceText = "ビズメイツ";
    await connection.manager.save(choice2);
    const choiceRepository = connection.getRepository(Choice);
    const choices = await choiceRepository.find();
    console.log(choices)

    const main = new Main();
    main.isPublished = true
    main.correctChoiceId = "xyz"
    main.choices = [choice1, choice2];
    main.categories = "english"
    main.question = "ビジネス英会話教室はどれでしょう?";
    console.log(main)
    await connection.manager.save(main);

    // const mainRepository = connection.getRepository(Main);
    // const mains = await mainRepository.find({ relations: ["choices"] });
    //console.log("Loaded mains: ", JSON.stringify(choices));


}).catch(error => console.log(error));

2. イニシャルデータの作成

以下の記事を参考にイニシャルデータを作成していく。
https://qiita.com/chocomintkusoyaro/items/092bc8dd9ddf3a191261
まず、docker-compose.ymlのvolumesにdb:/docker-entrypoint-initdb.dを定義する。

docker-compose.yml
version: '3'

services:
  postgres:
    image: postgres:latest
    restart: always
    environment:
      POSTGRES_USER: mebee
      POSTGRES_PASSWORD: password
      PGPASSWORD: password123
      POSTGRES_DB: sample
      TZ: "Asia/Tokyo"
    ports:
      - 5432:5432
    volumes:
      - postgres:/var/lib/postgresql/data

  pgadmin:
    image: dpage/pgadmin4
    restart: always
    ports:
      - 81:80
    environment:
      PGADMIN_DEFAULT_EMAIL: info@mebbe.info
      PGADMIN_DEFAULT_PASSWORD: password
    volumes:
      - pgadmin:/var/lib/pgadmin
      - ./db:/docker-entrypoint-initdb.d
    depends_on:
      - postgres

volumes:
  postgres:
  pgadmin:

次にdocker-compose.ymlが定義されているディレクトリ直下に新たにdbディレクトリを定義し、そのディレクトリ内にinit.sqlファイルを作成する。以下の記事を参考にした。
https://hasura.io/learn/database/postgresql/core-concepts/6-postgresql-relationships/

db/init.sql
set client_encoding = 'UTF8';

create table Main (
  questionId serial primary key,
  isPublished varchar not null,
  correctChoiceId varchar not null,
  question varchar not null,
  categories varchar not null
);

create table Choice (
  choiceId serial primary key,
  choiceText varchar not null,
  CONSTRAINT fk_main FOREIGN KEY(main_questionId) REFERENCES main(questionId)
);

insert into Choice(choiceId, choiceText) values 
  ('xxx', 'gcp'),
  ('yyy', 'aws'),
  ('zzz', 'azure')
;

insert into Main(questionId, isPublished, correctChoiceId, question, categories) values 
  ('aaa', true, 'xxx', 'よく利用するクラウドサービスは?', 'cluster')
;

続く

Discussion