1
Help us understand the problem. What are the problem?

posted at

updated at

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

DB の環境構築

docker-compose.yml

docker-compose.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

コンテナ起動

コマンドを実行しコンテナを起動します。

docker-compose up

TypeORM の準備

インストール

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

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 を登録します。

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 との接続情報を記述します。

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 のコマンドを使って、マイグレーションを行います。

Entity の作成

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

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

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;
}

マイグレーションの実行

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

npx typeorm migration:generate -n CreateBook

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

npx typeorm migration:run

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

Repository の作成

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

BookRepository の作成

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

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

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 プロパティに登録します。

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 を定義します。

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 に保存できるように調整します。

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 するように変更します。

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 するように変更します。

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 がない場合の例外処理も合わせて記述します。

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 を非同期処理に対応するように調整する。

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 メソッドで更新した値を保存します。

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 の内容も同様に調整します。

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 メソッドを使い、削除機能を作成する。

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 });
  }
}

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

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);
  }
}

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?