寫給初用Nestj作項目的你(四篇: TypeORM操做mysql數據庫, 內附坑點羅列)

寫給初用Nestj作項目的你(四篇: typeorm操做mysql數據庫, 內附坑點羅列)

TypeORM

     簡單理解他就是一款幫助咱們操做數據庫的工具, nest.js對他作了很好的集成, 雖然它的官網寫的挺全的可是實際開發起來仍是不太夠, 而且裏面有大坑我會把我知道的都列出來, 這篇也會把一些常見的解決方案寫出來。前端

1. 連接數據庫

此次是針對mysql數據庫
yarn add @nestjs/typeorm typeorm mysql2 -S

/share/src/app.module.tsmysql

import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
  imports: [
    TypeOrmModule.forRoot({
      port: 3306,
      type: 'mysql',
      username: 'root',
      host: 'localhost',
      charset: 'utf8mb4',
      password: '19910909',
      database: 'learn_nest',
      synchronize: true,
      autoLoadEntities: true,
    }),],
// ...
  1. 上面演示的是連接我本地的mysql, database是庫名。
  2. 能夠在imports 裏面定義多個 TypeOrmModule.forRoot 能夠操做多個庫, 多個時還須要填寫不一樣的name屬性。
  3. synchronize 自動載入的模型將同步。
  4. autoLoadModels 模型將自動載入。

當前的數據庫:
image.pnggit

建立模塊
// 控制檯裏輸入建立命令
nest g module modules/goods
nest g controller modules/goods
nest g service modules/goods

/share/src/modules/goods/goods.controller.tssql

import { Controller, Get } from '@nestjs/common';
import { GoodsService } from './goods.service';

@Controller('goods')
export class GoodsController {
    constructor(
        private readonly goodsService: GoodsService
    ) {}
    
    @Get()
    getList() {
        return this.goodsService.getList();
    }
}
創建實體

     實體其實就是對應了一張表, 這個實體的class名字必須與表名對應, 新建entity文件夾 /share/src/modules/goods/entity/goods.entity.ts:數據庫

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Goods {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;
}
  1. @PrimaryGeneratedColumn()裝飾了id爲主鍵, 類型爲數字。
  2. @Column()裝飾普通行, 類型爲字符串, 更多細節後面再講。
引入實體

nest自身設計的還不是很好, 引入搞得好麻煩 /share/src/modules/goods/goods.module.ts:數組

import { Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
import { Goods } from './entity/goods.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Goods])],
  controllers: [GoodsController],
  providers: [GoodsService]
})
export class GoodsModule { }
  1. forFeature() 方法定義在當前範圍中註冊哪些存儲庫。

/share/src/modules/goods/goods.service.ts:app

import { Injectable, } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Goods } from './entity/goods.entity'
import { Repository } from 'typeorm';

@Injectable()
export class GoodsService {
    constructor(
        @InjectRepository(Goods)
        private goodsRepository: Repository<Goods>
    ) { }
    getList() {
        return this.goodsRepository.find()
    }
}
  1. @InjectRepository()裝飾器將goodsRepository注入GoodsService中。
  2. 被注入進來的Repository都自帶屬性, 這裏使用了自帶的find方法後面會舉例出更多。

image.png

二. 坑點羅列(重點)

     滿紙荒唐言, 一把辛酸淚, 當時我被坑的不淺。async

1. 實體的強替換, 莫名刪表 (坑人指數 ⭐️ ⭐️ ⭐️ ⭐️)

     以咱們上面設置的實體爲例:編輯器

export class Goods {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;
}

     咱們初始化的表裏面name字段對應的類型是varchar(45), 可是name: string;這種方式初始化的類型是varchar(255), 此時類型是不一致的, typeorm選擇清空咱們的name列, 是的你沒聽錯name列被清空了:ide

image.png

     而且是隻要你運行nest項目的時候就同步熱更新了, 徹底無感, 甚至你都不知道被清空了, 若是此時是線上環境請準備點乾糧'跑路'吧。

     不光是string類型, 其餘任何類型只要對不上就全給你刪了, 毫無提示。

2. 沒有集成現有數據庫的方案 (坑人指數 ⭐️ ⭐️ ⭐️)

     咱們不少時候數據庫都是已有數據的, 全新的空白數據庫空白表的狀況並非主流, 在typeorm官網也並無找到很好的接入數據庫的方案, 所有都是冒着刪庫的危險在定義類型, 更有甚者你改到一半不當心自動保存了, 那麼你的表就空了...

     咱們不可能每次都是用空白數據庫開發, 這點真可貴很難人忍受。

3. entities的三種設置方式 (坑人指數 ⭐️)

第一種: 單獨定義
/share/src/app.module.ts配置連接數據庫時:

TypeOrmModule.forRoot({
      //...
      entities: [Goods, User],
    }),],

你用到哪些實體, 就逐一在此處引入, 缺點就是咱們每寫一個實體就要引入一次不然使用實體時會報錯。

第二種:
自動加載咱們的實體,每一個經過forFeature()註冊的實體都會自動添加到配置對象的entities數組中, forFeature()就是在某個service中的imports裏面引入的, 這個是比較推薦的:

TypeOrmModule.forRoot({
      //...
      autoLoadEntities: true,
    }),],

第三種:
自定義引入路徑, 這個竟然是官方推薦...

TypeOrmModule.forRoot({
      //...
      entities: ['dist/**/*.entity{.ts,.js}'],
    }),],
4. entities的大坑點, 莫名引入 (坑人指數 ⭐️ ⭐️ ⭐️ ⭐️ ⭐️)

     當咱們使用上述第三種方式引入實體時, 一個超級bug出現了, 情景步驟以下:

  1. 我要寫一個user的實體。
  2. 我直接複製了goods.entity.ts實體的文件更名爲user.entity.ts
  3. 修改其內部的屬性, 好比定義了userName, age, status等新屬性, 刪除了商品價格等舊屬性。
  4. 可是咱們尚未把導出的Goods類名改爲User, 因爲編輯器失去焦點等緣由致使vscode自動保存了。
  5. 驚喜來了, 你的goods表被清空了, 是的你尚未在任何地方引用這個user.entity.ts文件, 可是它已經生效了, 而且無聲無息的把你的goods表清空了。
  6. 我當時問該項目的負責人如何避免上述問題, 他研究了一下午, 告訴我關閉自動保存...(告辭)
5.官網的誤導 (坑人指數 ⭐️ ⭐️)

     如此坑的配置方式, 居然在官網裏找到了3處推薦如此使用, 簡直無語。
image.png

6. 多人開發, 極其混亂 (坑人指數 ⭐️ ⭐️ ⭐️ ⭐️ ⭐️)

     這個多人開發簡直是噩夢, 互相刪表的狀況逐漸出現, 一個實際的例子好比a同事優化全部實體的配置好比統一把varchar(255)改爲varchar(45), 全部的相關數據都會被清空, 於此同時你發現了問題, 並把數據補充回來了, 但此時b同事的電腦裏仍是varchar(255)版本, 一塊兒開發時就會致使你無論怎麼改數據, 表裏的數據都會被反覆清除乾淨...

     咱們團隊當時解決方案是, 每一個人都複製一份當前庫單獨進行開發, 幾我的開發就要有幾個不一樣的庫, 咱們的mysql裏全是已本身姓名命名的庫。

     每次git拉取代碼都要修改庫名, 不然會把其餘人的庫清空;

7. 多版本開發 (坑人指數 ⭐️ ⭐️ ⭐️ ⭐️ ⭐️)

     好比張三使用的是zhangsan_xxx庫, 可是他同時開發幾個版本, 這幾個版本以前表的格式有差異, 那麼張三要使用zhangsan_xxx_1_1, zhangsan_xxx_1_2這種命名格式來進行多個庫的開發。

綜上所述除非公司已經定了技術選型, 不然我不建議用nest開發...

三. entity設置

     看完坑點別灰心, 該學還得學, 下面咱們介紹一下entity設置能夠設置的比較實用的類型:

import { Entity, Column, Timestamp, UpdateDateColumn, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm';

export enum GoodsStatus {
    NORMAL = 1,
    HOT = 2,
    OFFSHELF = 3,
}

@Entity()
export class Goods {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({
        unique: true, nullable: false
    })
    name: string;

    @Column({
        length: 256,
        default: '暫無'
    })
    remarks: string;

    @Column({ default: true })
    isActive: boolean;

    @Column({
        type: 'enum',
        enum: GoodsStatus,
        default: GoodsStatus.NORMAL,
    })
    status: GoodsStatus;

    @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
    putDate: Timestamp;

    @CreateDateColumn()
    createDate: Timestamp;

    @UpdateDateColumn()
    updateDate: Timestamp;

}
  1. nullable: false 不能夠爲空。
  2. unique: true 惟一值, 不容許有重複的name的值出現, 須要注意的是若是當前的表裏面已經有重複的nametypeorm會報錯, 因此若是設置失敗請檢查表內容。
  3. length: 256限制字符的長度, 對應varchar(256)
  4. default: '暫無'默認值, 要注意當你手動設置爲空字符串時並不會被設置爲默認值。
  5. type: 'enum定義爲枚舉類型, enum: GoodsStatus 指定枚舉值, 當你賦予其非枚舉值時會報錯。
  6. type: 'timestamp'定義類型爲時間格式, CURRENT_TIMESTAMP默認就是建立時間。
  7. @CreateDateColumn()這個自動就能夠爲咱們設置值爲建立時間。
  8. @UpdateDateColumn()之後每次更新數據都會自動的更新這個時間值。

四. find方法類, 簡潔的查找命令

     上面咱們已經將goodsRepository注入到了GoodsService裏面能夠直接使用:

constructor(
        @InjectRepository(Goods)
        private goodsRepository: Repository<Goods>
    ) { }
1. 無條件查詢全部數據

this.goodsRepository.find()查詢goods表的所有數據, 以及每條數據的信息。

2. 只顯示name, createDate兩列數據:
this.goodsRepository.find({
       select: ['name', 'createDate']
    })

image.png

3. 搜索名字是'x2'而且isActive爲'false'的數據
this.goodsRepository.find({
     where: {
           name: 'x2',
           isActive: false
         }
     })

image.png

4. 名字等於'x2'或者等於'x3'都會被匹配出來:
this.goodsRepository.find({
       where: [{
          name: 'x2',
         }, {
          name: 'x3'
       }]
   })

image.png

5. 排序, 以name降序, 建立時間升序排列
this.goodsRepository.find({
    order: {
         name: "DESC",
         createDate: "ASC"
    }
})
6. 切割, skip跳過1條, take取出3條
this.goodsRepository.find({
     skip: 1,
     take: 3
})

image.png

7. like模糊查詢名字裏帶有2的項, notid不是1
this.goodsRepository.find({
       where: {
           id: Not(1),
           name: Like('%2%')
       }
   })

image.png

8. findAndCount 把知足條件的數據總數返回

數據是數組形式, [0]是匹配到的數組, [1]是符合條件的總數可能與[0]的長度不相同。

this.goodsRepository.findAndCount({
       select: ['name']
});

image.png

9. findOne

只取配到的第一條, 而且返回形式爲對象而非數組:

this.goodsRepository.findOne({
     select: ['name']
  });

image.png

10. findByIds, 傳入id組成的數組進行匹配
this.goodsRepository.findByIds([1, 2]);

這個就不展現了。

11. 前端獲取一個須要分頁的列表

用戶傳入須要模糊匹配的name值, 以及當前第n頁, 每頁s條, 總數total條。

async getList(query) {
        const { keyWords, page, pageSize } = query;
        const [list, total] = await this.goodsRepository.findAndCount({
            select: ['name', 'createDate'],
            where: {
                name: Like(`%${keyWords}%`)
            },
            skip: (page - 1) * pageSize,
            take: pageSize
        })
        return {
            list, total
        }
    }

image.png

五. dto 新增與修改

yarn add class-validator class-transformer -S
新增

先創建一個簡單的新增dto模型/share/src/modules/goods/dto/create-goods.dto.ts:

import { IsNotEmpty, IsOptional, MaxLength } from 'class-validator';

export class CreateGoodsDto {
    @IsNotEmpty()
    name: string;

    @IsOptional()
    @MaxLength(256)
    remarks: string;
}

使用/share/src/modules/goods/goods.service.ts

create(body) {
        const { name, remarks } = body;
        const goodsDto = new CreateGoodsDto();
        goodsDto.name = name;
        goodsDto.remarks = remarks;
        return this.goodsRepository.save(goodsDto)
    }

image.png

更新

老樣子, 先創建一份更新的dto, 好比name是不能夠更新的就不寫name, /share/src/modules/goods/dto/updata-goods.dto.ts:

import { MaxLength } from 'class-validator';

export class UpdataGoodsDto {
    @MaxLength(256)
    remarks: string;
}

在控制器裏面就要限制用戶傳入的更新數據類型必須與dto相同/share/src/modules/goods/goods.controller.ts:

@Put(':id')
    updata(@Param('id') id: string, @Body() updateRoleDto: UpdataGoodsDto) {
        return this.goodsService.updata(id, updateRoleDto);
    }

先找到對應的數據, 再進行數據的更新/share/src/modules/goods/goods.service.ts

async updata(id, updataGoodsDto: UpdataGoodsDto) {
        const goods = await this.goodsRepository.findOne(id)
        Object.assign(goods, updataGoodsDto)
        return this.goodsRepository.save(goods)
    }

image.png

6. 一對一關係

     同數據庫裏的一對一關係, 好比一個商品對應一個祕密廠家, 廠家是單獨一張表, 一塊兒來作下吧(這裏比喻不恰當, 當前現實意義不是重點):

nest g module modules/mfrs
nest g controller modules/mfrs
nest g service modules/mfrs

/share/src/modules/mfrs/entity/mfrs.entity.ts

import { Entity, Column, Timestamp, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Mfrs {
    @PrimaryGeneratedColumn('uuid')
    id: number;

    @Column()
    msg: string;

    @CreateDateColumn()
    createDate: Timestamp;
}
  1. 這裏我定義了uuid的加密類型。

在咱們的商品表裏面/share/src/modules/goods/entity/goods.entity.ts加上一個與mfrs表對應的行:

@OneToOne(() => Mfrs)
  @JoinColumn()
  mfrs: Mfrs
  1. 在你的表裏生成的列不叫mfrs而是叫mfrsId

goods模塊引入mfrs模塊:

第一步: 從mfrs模塊文件導出exports: [MfrsService]
第二步: 在goods的模塊文件中引入imports: [MfrsModule]
第三步: 在goods.service.ts的class類中注入mfrs的服務, private readonly mfrsService: MfrsService,

在咱們建立商品時, 把這個mfrs信息也插入進去:

async create(body) {
     const { name, remarks } = body;
     const goodsDto = new CreateGoodsDto();
     goodsDto.name = name;
     goodsDto.remarks = remarks;
     const mfrs = await this.mfrsService.create({
         msg: `${name}: 是正品`
      });
     goodsDto.mfrs = mfrs;
     return this.goodsRepository.save(goodsDto)
 }
搜索對應關係

     好比我直接用find方法查找goods表, 並無查找出mfrs的信息, 由於咱們須要配置相關的參數才能夠:

this.goodsRepository.findAndCount({
    relations: ['mfrs']
 })

image.png

7. 多對一, 與一對多關係

假設一個商品goods對應一個樣式style, 一個style對應多個商品就能夠寫成以下形式:

goods.entity.dto裏面添加設配置:

@ManyToOne(() => Style, style => style.goods)
    style: Style;

style.entity.dto裏面添加設配置:

@OneToMany(() => Goods, goods => goods.style)
    goods: Goods[];

create-goods.dto.ts裏面增長以下, 這樣才能正常的建立新的goods:

@IsOptional()
    style: Style;

建立goods時如此改動:

async create(body) {
        const { name, remarks, styleId } = body;
        const goodsDto = new CreateGoodsDto();
        goodsDto.name = name;
        goodsDto.remarks = remarks;
        const mfrs = await this.mfrsService.create({
            msg: `${name}: 是正品`
        });
        goodsDto.mfrs = mfrs;
        // 此處新增關聯關係
        goodsDto.style = await this.mtyleService.findOne(styleId)
        return this.goodsRepository.save(goodsDto)
    }

8. 多對多關係

     多對多與上面差異也不大, 但有一個細節值得注意, 好比你用a表與b表多對多關聯,則會產生一張名爲a_b的表, 當儲存的時候a.b = [b1, b2]這個樣子。

9. build語句, 處理更復雜場景

     find很簡潔好看, 但它沒法應對全部的場景:

QueryBuilder是 TypeORM 最強大的功能之一 ,它容許你使用優雅便捷的語法構建 SQL 查詢,執行並得到自動轉換的實體, 簡單理解其就是一種美觀上不如find可是比find能作的事要多的方法。
this.goodsRepository.createQueryBuilder('goods')就能夠建立出來。
好比一個goods商品
  1. goods有 name名稱, keywords關鍵字兩種屬性, 而且這兩個屬性都是單獨的表咱們須要去關聯, 此時咱們須要模糊匹配功能。
  2. (重點)goods有一個屬性maintainers是一個維護者的集合, 爲數組類型, 大概長這樣[{id:1, name:'張三'}, {id:2, name:'李四'}]
  3. (重點) 好比當前用戶的id爲9,咱們須要剔除掉maintainers數組中的id不爲9的數據。

這個語句大概的樣子是這樣的:

const qb = this.goodsRepository
      .createQueryBuilder('goods')
      .leftJoinAndSelect('goods.keywords', 'goods_keyword')
      .leftJoinAndSelect('goods.name', 'goods_name')
      .leftJoinAndSelect('goods.maintainers', 'user');
    const { keyword, name } = query;
    qb.where('goods.keyword LIKE :keyword', { keyword: `%${keyword}%` });
    qb.orWhere('goods.name LIKE :name', {
      name: `%${name}%`,
    });
    // 這裏的'user.id'指的是'user'表裏面查出的數據
    qb.andWhere('user.id = :id', { id: 9 });
    const [list, total] = await qb.getManyAndCount();

end.

     此次就是這樣, 快去突破自我吧, 但願和你一塊兒進步。

相關文章
相關標籤/搜索