NestJS のチュートリアル、TypeORM を使ったCRUD の実装

# DB の環境構築
## docker-compose.yml

docker-compose.yml を作成し、コンテナの設定を記述します。

```yml:docker-compose.yml
version: '3.7'
services:
  mysql:
    image: mysql:5.7
    container_name: mysql
    ports:
      - '3306:3306'
    volumes:
      - /var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: develop
      MYSQL_USER: develop
      MYSQL_PASSWORD: password
    hostname: mysql
    restart: always
    user: root
```

## コンテナ起動

コマンドを実行しコンテナを起動します。
```bash
docker-compose up
```

## TypeORM の準備
### インストール

npm を使い、nestjs/typeorm、typeorm、mysql のドライバーをインストールします。

```bash
npm install --save @nestjs/typeorm typeorm@0.2.45 mysql
```

typeorm. のver を指定せずにインストールを行うと0.3 系がインストールされます。

0.3 系で進めるとmigration ファイルの作成の部分で詰まったので、今回は0.2 系を使用しました。

0.3 系の破壊的変更が原因だと思われる。

### 接続情報の設定

app.module.ts のimports プロパティにTypeOrmModule を登録します。

```ts:src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BooksModule } from './books/books.module';

@Module({
  imports: [BooksModule, TypeOrmModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

forRoot() の中に接続情報を書くことでDB との接続を行うことができます。
今回はTypeORM CLI を使ってDB マイグレーションを行うので、ormconfig.js に接続情報を切り出します。

ルートディレクトリに、ormconfig.js を作成し、DB との接続情報を記述します。

```jsx:ormconfig.js
module.exports = {
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'develop',
  password: 'password',
  database: 'develop',
  autoLoadEntities: true,
  entities: ['dist/entities/*.entity.js'],
  migrations: ['dist/migrations/*.js'],
  cli: {
    entitiesDir: 'src/entities',
    migrationsDir: 'src/migrations',
  },
};
```
# DB マイグレーション

オブジェクトとRDB のマッピングを定義した、Entity Class を準備し、TypeORM CLI のコマンドを使って、マイグレーションを行います。

[](https://typeorm.io/migrations)

## Entity の作成

src ディレクトリの直下にentities ディレクトリを作成します。

entities ディレクトリの中に、book.entity.ts を作成し、Book オブジェクト 用のEntity Class を記述していきます。

```ts:src/entities/book.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { BookStatus } from '../book-status.enum';

@Entity()
export class Book {
  @PrimaryGeneratedColumn()
  id: string;

  @Column()
  name: string;

  @Column()
  status: BookStatus;

  @Column()
  createdAt: string;

  @Column()
  updatedAt: string;
}
```
## マイグレーションの実行

アプリをリスタート（コンパイルし直す）し、マイグレーションファイル作成用のコマンドを実行します。

```zsh
npx typeorm migration:generate -n CreateBook
```

再度、アプリを立ち上げ直し、マイグレーション実行用のコマンドを実行します。

```zsh
npx typeorm migration:run
```

実行後、book テーブルが作成されていれば成功です。
# Repository の作成

Repository を使えば、DB に対してEntity のinsert、update、delete、load などを簡単に行うことができます。
## BookRepository の作成

Book Entity を操作するためのRepository を作成します。

Class にEntityRepository デコレーターをつけ、引数にBook Entity を渡すことで、Book Entity に対するRepository を作成することができます。

```ts:src/books/book.repository.ts
import { Book } from 'src/entities/user.entity';
import { EntityRepository, Repository } from 'typeorm';

// Entity デコレーターをつけるとClassがRepositoryだと認識される
@EntityRepository(Book) // 引数に対応するEntityを渡す
export class BookRepository extends Repository<Book> {} // TypeORMのRepository Classを継承させる

import { Book } from 'src/entities/book.entity';
import { EntityRepository, Repository } from 'typeorm';

// EntityRepository デコレーターをつけるとClassがRepositoryだと認識される
@EntityRepository(Book)
export class BookRepository extends Repository<Book> {} // TypeORMのRepository Classを継承させる
```
## BookRepository の登録

BookRepository をBooksService 内で使用できるように、BooksModule のimports プロパティに登録します。

```ts:src/books/books.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookRepository } from './book.repository';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';

@Module({
  imports: [TypeOrmModule.forFeature([BookRepository])],
  controllers: [BooksController],
  providers: [BooksService],
})
export class BooksModule {}
```
# TypeORM を使用したCRUD の実装
## createBookDto  の作成

create メソッドにまとめてBook を作成するためのパラメーターをBody にわたせるように、create-book.dto.ts を作成し、CreateBookDto Class を定義します。

```ts:src/books/dto/create-book.dto.ts
import { BookStatus } from '../book-status.enum';

export class CreateBookDto {
  name: string;
  status: BookStatus;
}
```

## Create 機能の実装

 Repository Class からBookRepository Class に継承されている、create、saveメソッドを使って、作成したBook のデータをDB に保存できるように調整します。

```ts:src/books/book.repository.ts
import { Book } from 'src/entities/book.entity';
import { EntityRepository, Repository } from 'typeorm';
import { BookStatus } from './book-status.enum';
import { CreateBookDto } from './dto/create-book.dto';

@EntityRepository(Book)
export class BookRepository extends Repository<Book> {
  async createBook(createBookDto: CreateBookDto): Promise<Book> {
    // BooksController から受け取った値を各変数に渡す
    const { name } = createBookDto;

    // Bookインスタンスを作成して、bookに代入する
    const book = this.create({
      name,
      status: BookStatus.RENTABLE,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    });
    // DBにbookの情報を保存する
    await this.save(book);
    return book;
  }
}
```

BooksService をBookRepositoryを呼び出すように調整します。

また、Book Class はsrc/entities/book.entity からimport するように変更します。

```ts:src/books/books.service.ts
import { Injectable } from '@nestjs/common';
import { BookStatus } from './book-status.enum';
import { BookRepository } from './book.repository';
import { Book } from 'src/entities/book.entity';
import { CreateBookDto } from './dto/create-book.dto';

@Injectable()
export class BooksService {
  // BooksService　ClassにBookRepositoryの依存性を注入する
  constructor(private readonly bookRepository: BookRepository) {}
  private books: Book[] = [];
  findAll() {
    return this.books;
  }

  findById(id: string): Book {
    return this.books.find((book) => book.id === id);
  }

  // BookRepositoryのcreateBook()を呼び出す。
  async create(createBookDto: CreateBookDto): Promise<Book> {
    return await this.bookRepository.createBook(createBookDto);
  }

  updateStatus(id: string): Book {
    const book = this.findById(id);
    book.status = BookStatus.LENT_OUT;
    return book;
  }

  delete(id: string): void {
    this.books = this.books.filter((book) => book.id !== id);
  }
}
```

@Body にcreateBookDto を渡すようにBooksController の内容を調整します。
こちらもBook Class はsrc/entities/book.entity からimport するように変更します。

```ts:src/books/books.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { Book } from '../entities/book.entity';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}
  @Get()
  findAll() {
    return this.booksService.findAll();
  }

  @Get(':id')
  findById(@Param('id') id: string): Book {
    return this.booksService.findById(id);
  }

  // @Body にcreateBookDto だけを渡すように調整する
  @Post()
  async create(@Body() createBookDto: CreateBookDto): Promise<Book> {
    return await this.booksService.create(createBookDto);
  }

  @Patch(':id')
  updateStatus(@Param('id') id: string): Book {
    return this.booksService.updateStatus(id);
  }

  @Delete()
  delete(@Param('id') id: string): void {
    this.booksService.delete(id);
  }
}
```

## Read 機能の実装

BookRepository Class のfind を使い、DB からBook のデータを取得できるようにします。
findAll ではfind メソッド、findById では、findOne メソッドをそれぞれ使用するようにします。

また、findById に関しては、検索したID を持つBook がない場合の例外処理も合わせて記述します。

```ts:src/books/books.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { BookStatus } from './book-status.enum';
import { BookRepository } from './book.repository';
import { Book } from 'src/entities/book.entity';
import { CreateBookDto } from './dto/create-book.dto';

@Injectable()
export class BooksService {
  // BooksService　ClassにBookRepositoryの依存性を注入する
  constructor(private readonly bookRepository: BookRepository) {}
  private books: Book[] = [];

  // BookRepository のfind() メソッドを呼び出す
  findAll() {
    return this.bookRepository.find();
  }

  // BookRepository のfindOne メソッドを呼び出す
  async findById(id: string): Promise<Book> {
    const found = await this.bookRepository.findOne(id);
    // found に値が入らなかった場合は、例外処理としてNotFoundException を返すようにする
    if (!found) {
      throw new NotFoundException();
    }
    return found;
  }

  // BookRepositoryのcreateBook()を呼び出す。
  async create(createBookDto: CreateBookDto): Promise<Book> {
    return await this.bookRepository.createBook(createBookDto);
  }

  updateStatus(id: string): Book {
    const book = this.findById(id);
    book.status = BookStatus.LENT_OUT;
    return book;
  }

  delete(id: string): void {
    this.books = this.books.filter((book) => book.id !== id);
  }
}
```

BooksController を非同期処理に対応するように調整する。

```ts:src/books/books.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { Book } from '../entities/book.entity';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}
  @Get()
  async findAll() {
    return await this.booksService.findAll();
  }

  @Get(':id')
  async findById(@Param('id') id: string): Promise<Book> {
    return await this.booksService.findById(id);
  }

  // @Body にcreateBookDto だけを渡すように調整する
  @Post()
  async create(@Body() createBookDto: CreateBookDto): Promise<Book> {
    return await this.booksService.create(createBookDto);
  }

  @Patch(':id')
  updateStatus(@Param('id') id: string): Promise<Book> {
    return await this.booksService.updateStatus(id);
  }

  @Delete()
  delete(@Param('id') id: string): void {
    this.booksService.delete(id);
  }
}
```

## Update 機能の実装

DB 内に保存されているBook のBookStatus の値を更新できるようにします。

findById を使い、Book の検索を行い、BookRepository のsave メソッドで更新した値を保存します。

```ts:src/books/books.controller.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { BookStatus } from './book-status.enum';
import { BookRepository } from './book.repository';
import { Book } from 'src/entities/book.entity';
import { CreateBookDto } from './dto/create-book.dto';

@Injectable()
export class BooksService {
  // BooksService　ClassにBookRepositoryの依存性を注入する
  constructor(private readonly bookRepository: BookRepository) {}
  private books: Book[] = [];

  // BookRepository のfind() メソッドを呼び出す
  findAll() {
    return this.bookRepository.find();
  }

  // BookRepository のfindOne メソッドを呼び出す
  async findById(id: string): Promise<Book> {
    const found = await this.bookRepository.findOne(id);
    // found に値が入らなかった場合は、例外処理としてNotFoundException を返すようにする
    if (!found) {
      throw new NotFoundException();
    }
    return found;
  }

  // BookRepositoryのcreateBook()を呼び出す
  async create(createBookDto: CreateBookDto): Promise<Book> {
    return await this.bookRepository.createBook(createBookDto);
  }

  // findById を使ってID と一致するBook を取得し、BookStatus の値を更新して、save()メソッドで保存する
  async updateStatus(id: string): Promise<Book> {
    const book = await this.findById(id);
    book.status = BookStatus.LENT_OUT;
    book.updatedAt = new Date().toISOString();
    await this.bookRepository.save(book);
    return book;
  }

  delete(id: string): void {
    this.books = this.books.filter((book) => book.id !== id);
  }
}
```

BookController の内容も同様に調整します。

```ts:src/books/books.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { Book } from '../entities/book.entity';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}
  @Get()
  async findAll() {
    return await this.booksService.findAll();
  }

  @Get(':id')
  async findById(@Param('id') id: string): Promise<Book> {
    return await this.booksService.findById(id);
  }

  // @Body にcreateBookDto だけを渡すように調整する
  @Post()
  async create(@Body() createBookDto: CreateBookDto): Promise<Book> {
    return await this.booksService.create(createBookDto);
  }

  @Patch(':id')
  async updateStatus(@Param('id') id: string): Promise<Book> {
    return await this.booksService.updateStatus(id);
  }

  @Delete()
  delete(@Param('id') id: string): void {
    this.booksService.delete(id);
  }
}
```

## Delete 機能の実装

BookRepository Class のdelete メソッドを使い、削除機能を作成する。

```ts:src/books/books.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { BookStatus } from './book-status.enum';
import { BookRepository } from './book.repository';
import { Book } from 'src/entities/book.entity';
import { CreateBookDto } from './dto/create-book.dto';

@Injectable()
export class BooksService {
  // BooksService　ClassにBookRepositoryの依存性を注入する
  constructor(private readonly bookRepository: BookRepository) {}
  private books: Book[] = [];

  // BookRepository のfind() メソッドを呼び出す
  findAll() {
    return this.bookRepository.find();
  }

  // BookRepository のfindOne メソッドを呼び出す
  async findById(id: string): Promise<Book> {
    const found = await this.bookRepository.findOne(id);
    // found に値が入らなかった場合は、例外処理としてNotFoundException を返すようにする
    if (!found) {
      throw new NotFoundException();
    }
    return found;
  }

  // BookRepositoryのcreateBook()を呼び出す
  async create(createBookDto: CreateBookDto): Promise<Book> {
    return await this.bookRepository.createBook(createBookDto);
  }

  // findById を使ってID と一致するBook を取得し、BookStatus の値を更新して、save()メソッドで保存する
  async updateStatus(id: string): Promise<Book> {
    const book = await this.findById(id);
    book.status = BookStatus.LENT_OUT;
    book.updatedAt = new Date().toISOString();
    await this.bookRepository.save(book);
    return book;
  }
  
  BookRepository のdelete() メソッドを呼び出す
  async delete(id: string): Promise<void> {
    await this.bookRepository.delete({ id });
  }
}
```

今までと同様コントローラーも調整する。

```ts:src/books/books.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { Book } from '../entities/book.entity';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}
  @Get()
  async findAll() {
    return await this.booksService.findAll();
  }

  @Get(':id')
  async findById(@Param('id') id: string): Promise<Book> {
    return await this.booksService.findById(id);
  }

  // @Body にcreateBookDto だけを渡すように調整する
  @Post()
  async create(@Body() createBookDto: CreateBookDto): Promise<Book> {
    return await this.booksService.create(createBookDto);
  }

  @Patch(':id')
  async updateStatus(@Param('id') id: string): Promise<Book> {
    return await this.booksService.updateStatus(id);
  }

  @Delete(':id')
  async delete(@Param('id') id: string): Promise<void> {
    await this.booksService.delete(id);
  }
}
```