# 主旨
本文將會介紹如何使用 [Typeorm](https://typeorm.io/) 用來連結 [Neon 平台](https://console.neon.tech/app/projects/delicate-silence-377852)所建制的 Postgresql DB
## 前置作業
0. 已安裝 nest cli
1. 已使用 nest cli 建立一個 空專案
## 什麼是 TypeORM
[TypeORM](https://typeorm.io/) 是一種支援 Typescript 型別宣告的 Nodejs [ORM](https://zh.wikipedia.org/zh-tw/%E5%AF%B9%E8%B1%A1%E5%85%B3%E7%B3%BB%E6%98%A0%E5%B0%84) 框架
將程式對於資料表紀錄的操作抽象化成為對於某個 Nodejs 記憶體物件的操作
讓對於資料庫的語法不被限制於特定資料庫
以及支援資料庫連接池的設定,有效簡化開發者對於資料庫的開發複雜度
## 為何使用 TypeORM
1. 想把對資料庫紀錄的操作抽象化(?, 方便隔離測試(?
2. 支援較多資料庫 ORM 的標準操作 相對於 [Prisma](https://www.prisma.io/)
3. ~~潮, 主流~~
4. ~~有人要我 demo~~
## 使用概念
### [DataSource](https://typeorm.io/data-source)
TypeORM 處理資料庫連線的最基礎元件就是 DataSource
啟動應用程式時需要呼叫 initial 這個函是來建立資料庫連線池的連線
關閉應用程式時需要呼叫 destroy 這個函是來關閉資料庫連線池的連線
在 Nestjs 應用則有 Component Manager 可以做到這個管理,不必特別設定
### [Entity](https://typeorm.io/entities)
Entity 就是 TypeORM 裏面對來對應資料表的 Data Model
每個 Entity 對應到一個資料表
每個 Entity 物件的一個屬性對應資料表內的每個欄位
舉例來說
```typescript=
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
isActive: boolean
}
```
對應到的表格如下
```
+-------------+--------------+----------------------------+
| user |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| firstName | varchar(255) | |
| lastName | varchar(255) | |
| isActive | boolean | |
+-------------+--------------+----------------------------+
```
### [Relation](https://typeorm.io/relations)
當把表格資訊做正規化
把一張表拆分成兩個表格
這時可以透過外部鍵把兩個表格做關聯
而這個關鏈也就是 TypeORM 所表示的 Relation
### [EntityManager](https://typeorm.io/working-with-entity-manager)
在程式運行時,可以透過 EntityManager 來操作所有的 Entity 物件
而資料庫事務性操作的單位也是以 EntityManager 為主
### [Repository](https://typeorm.io/working-with-repository)
類似於 EntityManager, Repository 可以操作特定某個類別的 Entity 物件
在 Nestjs 程式中常使用的方式
### [Migration](https://typeorm.io/migrations)
typeORM 提供 cli 以及一個 Migration 的介面用來做資料庫遷移
預設如果 synchronized 設定成 true, 則會自動更新資料表為當下 Entity 格式
## 在 Nestjs 中使用 TypeORM
### 0 Nestjs TypeORM module 重要的概念
#### 主要結構

基本上在最大的 AppModule 會有一個用來設定 DB 連線資訊的 TypeORM forRoot 的設定
然後在個別有使用到 DataSource 或是 Entity 的 SubModule 需要個別引入 TypeORM forFeature 來引用每個使用到的 Entity Model
這樣才能讓 Nestjs 在建制整個 Module 時讀取到 Entity 的資訊
另外, 假設不想要把所有 entities 路徑都放在 forRoot 那個 TypeORM 裡的設定,需要額外設定 autoloadEntity 這個設定
#### 連線設定
以下是針對 TypeORM forRoot 內部所需要設定的連線設定
```typescript=
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
// 設定 ssl 連線 flag
const IS_DB_SSL_MODE = configService.getOrThrow<boolean>(
'IS_DB_SSL_MODE',
false,
);
return {
ssl: IS_DB_SSL_MODE,
extra: {
ssl: IS_DB_SSL_MODE ? { rejectUnauthorized: false } : null,
poolSize: 5, // 連線池數量,影響會有幾個 connection pool 被建立
idleTimeoutMillis: 3600000, // idle 代表連線可以 idle 多久才被斷線
},
type: 'postgres',
url: configService.getOrThrow('DB_URI', ''),
synchronize: false,
autoLoadEntities: true, // 自動 load 所有 **/*.entity.ts 檔案到 TypeORM 設定
};
},
inject: [ConfigService],
}),
```
##### ssl 設定
這個設定是決定是否要使用 ssl 連線,一般正式連線會啟用
##### type 設定
這邊是決定要連線資料庫的類型,比如說: 這邊範例是 postgresql DB 所以貼入 'postgres'
##### url 設定
這個是使用 dsn 的方式做連線。格式為: ```postgresql://${db_username}:${db_password}@${db_hostname}:${db_port}/${db_name}```
##### synchronized 設定
這個設定決定是否自動同步 entity 到資料庫 table
如果設定為 true ,每次程式啟動會自動更新 table。範例採用 migration 方式,所以設定為 false
##### autoLoadEntities 設定
這個設定決定是否自動載入 Entity 到 forRoot TypeORM
如果設定為 true 則不需要填入 entities 參數,TypeORM 會自動搜尋 ```**/*.entity.ts``` 的檔案
如果沒有填,則需要透過 entities 這個參數填入所有 entity
##### entities 設定
這個參數是用來設定需要載入的 entity,讓 TypeORM 理解要載入的 DataModel
#### 引入 Entity
為了在每個有使用到的地方可以使用 TypeORM entity
我們需要在有使用到 Entity 的地方去載入該 entity 設定如下
```typescript=
@Module({
imports: [TypeOrmModule.forFeature([BookStoreEntity])],
controllers: [BookController],
providers: [BookStoreService],
})
export class BookStoreModule {}
```
### 1 安裝必要套件
```shell=
yarn add @nestjs/config
yarn add @nestjs/typeorm typeorm pg
yarn add joi
```
### 2 設定連線資料
```typescript=
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DB_URI: Joi.string().required(),
IS_DB_SSL_MODE: Joi.boolean().required(),
}),
}),
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
const IS_DB_SSL_MODE = configService.getOrThrow<boolean>(
'IS_DB_SSL_MODE',
false,
);
return {
ssl: IS_DB_SSL_MODE,
extra: {
ssl: IS_DB_SSL_MODE ? { rejectUnauthorized: false } : null,
poolSize: 5,
idleTimeoutMillis: 3600000,
},
type: 'postgres',
url: configService.getOrThrow('DB_URI', ''),
synchronize: false,
autoLoadEntity: true
};
},
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```
### 3 定義 Entity 內容
```typescript=
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('book_store', { schema: 'public' })
export class BookStoreEntity {
@PrimaryGeneratedColumn({
name: 'id',
type: 'bigint',
})
id: string;
@CreateDateColumn({
name: 'created_at',
type: 'time without time zone',
})
createdAt: Date;
@UpdateDateColumn({
name: 'updated_at',
type: 'time without time zone',
})
updatedAt: Date;
@Column({
name: 'author',
type: 'varchar',
length: '100',
})
author: string;
@Column({
name: 'name',
type: 'varchar',
length: '100',
})
name: string;
@Column({
name: 'publication',
type: 'varchar',
length: '100',
})
publication: string;
}
```
### 4 設置資料庫轉移
#### 新增 migration 設定檔
```typescript=
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';
config();
const configService = new ConfigService();
export default new DataSource({
type: 'postgres',
url: configService.get<string>('DATABASE_URL', ''),
migrations: ['src/migrations/*.ts'],
});
```
#### 新增 scripts
```jsonld=
"typeorm": "ts-node ./node_modules/typeorm/cli",
"schema:sync": "yarn run typeorm schema:sync -d src/typeorm.migration.ts",
"schema:drop": "yarn run typeorm schema:drop -d src/typeorm.migration.ts",
"schema:log": "yarn run typeorm schema:log -d src/typeorm.migration.ts",
"typeorm:show": "yarn run typeorm migration:show -d src/typeorm.migration.ts",
"typeorm:run-migrations": "yarn run typeorm migration:run -d src/typeorm.migration.ts",
"typeorm:create-migration": "npm run typeorm -- migration:create src/migrations/$npm_config_name",
"typeorm:generate-migration": "npm run typeorm -- migration:generate -d src/typeorm.migration.ts src/migrations/$npm_config_name",
"typeorm:revert-migration": "yarn run typeorm migration:revert -d src/typeorm.migration.ts"
```
#### 新增 migration
```shell=
npm run typeorm:create-migration --name=BOOK-STORE
```
```typescript=
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class BOOKSTORE1687517670891 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'book_store',
schema: 'public',
columns: [
{
name: 'id',
type: 'bigint',
generatedIdentity: 'ALWAYS',
generationStrategy: 'increment',
isGenerated: true,
isPrimary: true,
},
{
name: 'created_at',
type: 'time without time zone',
isNullable: false,
default: 'now()',
},
{
name: 'updated_at',
type: 'time without time zone',
isNullable: false,
default: 'now()',
},
{
name: 'author',
type: 'varchar',
length: '100',
isNullable: false,
},
{
name: 'name',
type: 'varchar',
length: '100',
isNullable: false,
},
{
name: 'publication',
type: 'varchar',
length: '100',
isNullable: false,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('public.book_store', true, true, true);
}
}
```
#### 執行 script 新增 schema
```shell=
yarn run typeorm:run-migrations
```
### 5 實作 基礎操作
#### 設置 submodule 連線配置
```typescript=
import { Module } from '@nestjs/common';
import { BookController } from './book.controller';
import { BookStoreService } from './book-store.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookStoreEntity } from './book-store.entity';
@Module({
imports: [TypeOrmModule.forFeature([BookStoreEntity])],
controllers: [BookController],
providers: [BookStoreService],
})
export class BookStoreModule {}
```
#### 對 book_store entity 操作
```typescript=
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { BookStoreDto } from '../dtos/book-store.dto';
import { BookStoreEntity } from './book-store.entity';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class BookStoreService {
private logger = new Logger(BookStoreService.name);
constructor(
private readonly dataSource: DataSource,
@InjectRepository(BookStoreEntity)
private readonly bookStoreRepo: Repository<BookStoreEntity>,
) {}
async getBookList(): Promise<BookStoreDto[]> {
try {
const result = await this.bookStoreRepo.find();
return result.map((book) => ({
id: book.id,
name: book.name,
author: book.author,
publication: book.publication,
createdAt: book.createdAt.getTime(),
updatedAt: book.updatedAt.getTime(),
}));
} catch (error) {
this.logger.error({ message: 'getBookList error' }, error);
throw error;
}
}
async getBookById(id: string): Promise<BookStoreDto> {
try {
const book = await this.bookStoreRepo.findOne({
where: {
id: id,
},
});
if (!book) {
this.logger.error({ message: `book with id ${id} not found` });
throw new NotFoundException(`book with id ${id} not found`);
}
return {
id: book.id,
author: book.author,
name: book.name,
publication: book.publication,
createdAt: book.createdAt.getTime(),
updatedAt: book.updatedAt.getTime(),
};
} catch (error) {
this.logger.error({ message: 'getBookById error' }, error);
throw error;
}
}
async updateById(
id: string,
updateDto: Partial<BookStoreDto>,
): Promise<BookStoreDto> {
try {
const queryBuilder = this.dataSource
.getRepository(BookStoreEntity)
.createQueryBuilder('book_store');
const result = await queryBuilder
.update<BookStoreEntity>(BookStoreEntity, updateDto)
.where('book_store.id = :id', { id })
.returning([
'id',
'author',
'name',
'publication',
'createdAt',
'updatedAt',
])
.updateEntity(true)
.execute();
const model = result.raw[0] as BookStoreEntity;
return {
id: model.id,
name: model.name,
author: model.author,
publication: model.publication,
createdAt: model.createdAt.getTime(),
updatedAt: model.updatedAt.getTime(),
};
} catch (error) {
this.logger.error({ message: 'updateById error' }, error);
throw error;
}
}
async insertBook(dto: BookStoreDto): Promise<BookStoreDto> {
try {
this.logger.log({ dto });
const data = new BookStoreEntity();
data.author = dto.author;
data.name = dto.name;
data.publication = dto.publication;
const queryBuilder = this.dataSource
.getRepository(BookStoreEntity)
.createQueryBuilder('book_store');
const result = await queryBuilder
.insert()
.into(BookStoreEntity)
.values([data])
.returning([
'id',
'author',
'name',
'publication',
'createdAt',
'updatedAt',
])
.updateEntity(true)
.execute();
const model = result.raw[0] as BookStoreDto;
model.createdAt = new Date(result.raw[0]['created_at']).getTime();
model.updatedAt = new Date(result.raw[0]['updated_at']).getTime();
return {
id: model.id,
name: model.name,
author: model.author,
publication: model.publication,
createdAt: model.createdAt,
updatedAt: model.updatedAt,
};
} catch (error) {
this.logger.error({ message: 'insertBook error' }, error);
throw error;
}
}
async deleteById(id: string): Promise<BookStoreDto> {
try {
this.logger.log({ id });
const queryBuilder = this.dataSource
.getRepository(BookStoreEntity)
.createQueryBuilder('book_store');
const result = await queryBuilder
.delete()
.where('book_store.id = :id', { id })
.returning([
'id',
'author',
'name',
'publication',
'createdAt',
'updatedAt',
])
.execute();
const model = result.raw[0] as BookStoreDto;
model.createdAt = new Date(result.raw[0]['created_at']).getTime();
model.updatedAt = new Date(result.raw[0]['updated_at']).getTime();
return {
id: model.id,
name: model.name,
author: model.author,
publication: model.publication,
createdAt: model.createdAt,
updatedAt: model.updatedAt,
};
} catch (error) {
this.logger.error({ message: 'deleteById error' }, error);
throw error;
}
}
}
```
## repository
[https://github.com/nodejs-typescript-classroom/neon-sample.git](https://github.com/nodejs-typescript-classroom/neon-sample.git)