DB の環境構築
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 を登録します。
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 との接続情報を記述します。
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 を記述していきます。
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 を作成することができます。
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 プロパティに登録します。
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 を定義します。
import { BookStatus } from '../book-status.enum';
export class CreateBookDto {
name: string;
status: BookStatus;
}
Create 機能の実装
Repository Class からBookRepository Class に継承されている、create、saveメソッドを使って、作成したBook のデータをDB に保存できるように調整します。
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 するように変更します。
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 するように変更します。
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 がない場合の例外処理も合わせて記述します。
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 を非同期処理に対応するように調整する。
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 メソッドで更新した値を保存します。
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 の内容も同様に調整します。
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 メソッドを使い、削除機能を作成する。
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 });
}
}
今までと同様コントローラーも調整する。
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);
}
}