快速入门nest.js(5/10)--数据库之一PostgreSQL

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: 这时,它就会创建一个PostgreSQL数据库此时我们就可以毫不费力的运行一个数据库环境docker-compose up db -d,-d代表分离模式运行我们的容器,-db代表只运行db中配置的环境,如果不传该参,将是整个yaml文件。

环境配置


首先确保你的电脑本地安装了Docker

项目根目录下创建docker-compose.yml文件

version: '3'
services:
  db:
    image: postgres
    restart: always
    ports:
      -"5432:5432"    # 使用PostgreSQL的端口为前5432,Docker容器内部在后5432设置数据库
    environment:
      POSTGRES_PASSWORD: pass123


这时,它就会创建一个PostgreSQL数据库

此时我们就可以毫不费力的运行一个数据库环境docker-compose up db -d-d代表分离模式运行我们的容器,-db代表只运行db中配置的环境,如果不传该参,将是整个yaml文件。

集成进nest(typeORM)


安装对应包:

// npm i @nestjs/typeorm typeorm pg


然后导入:

@Module({
  imports: [
    CoffeesModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'pass123',
      database: 'postgres',
      autoLoadEntities: true,  // 有助于自动加载模块,而不是指定实体数组
      synchronize: true,  // 同步,确保TypeORM实体每次运行应用时都会与数据库保持同步
        // ! 仅生产环境可用
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})

Entity(实体)


Entity表示TypeScript类和数据库之间的关系,使用@Entity()装饰的类。

// coffee.entity.ts
import { Entity } from "typeorm";
@Entity('coffees')  // sql table === 'coffee' <默认为小写的类名>,或者传入你想要的表名
export class Coffee {
  id: number;
  name: string;
  brand: string;
  flavors: string[];
}

每个Entity类代表一个SQL

为每一列赋上意义

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()  // sql table === 'coffee' <默认为小写的类名>,或者传入你想要的表名
export class Coffee {
  @PrimaryGeneratedColumn()  // 为id设置自增主键
  id: number;
  @Column()
  name: string;
  @Column()
  brand: string;
  // 这里的每一列(除flavors)都是非空的
  @Column('json', {nullable: true})  // TypeORM知道将flavors 数组存储为json
  flavors: string[];
}


@Module({
  imports:[TypeOrmModule.forFeature([Coffee])],
  controllers: [CoffeesController],
  providers: [CoffeesService],
})


使用forFeatureTypeORM注册到此子模块中。

记住:我们在AppModule中会使用一次forRoot,注册实体时,所有其他模块都将使用forFeature

然后就可以看到数据库中生成了对应的表:

Repository


TypeORM支持存储库设计模式,这意味着我们创建的每个实体都有自己的存储库。

// coffees.service.ts 中删除这一部分
// 模拟一个假数据源进行CRUD
private coffees: Coffee[] = [    {        id: 1,        name: 'Shipwreck Roast',        brand: 'Buddy Brew',        flavors: ['chocolate', 'vanilla'],
    },
];


// 添加这一部分
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; 
export class CoffeesService {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository: Repository<Coffee>,
  ){}
  // ...
}


修改CURD


我们需要将其中的大多数方法更新为asyncawait

  // CRUD
  findAll() {
    return this.coffeeRepository.find();
  }
  async findOne(id: string) {
    const coffee = await this.coffeeRepository.findOne(id);
    if(!coffee){
      // throw new HttpException(`Coffee ${id} not found`, HttpStatus.NOT_FOUND);
      throw new NotFoundException(`Coffee ${id} not found`);  // 简化类
    }
    return coffee;
  }
  create(createCoffeeDto: CreateCoffeeDto) {
    const coffee = this.coffeeRepository.create(createCoffeeDto);  // 创建对应实例
    return this.coffeeRepository.save(coffee);   // 保存入数据库中,返回一个期约
  }
  async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
    // preload 会首先查看数据库是否存在实体,存在则会更新,否则返回undefined
    const coffee = await this.coffeeRepository.preload({
      id: +id,
      ...updateCoffeeDto,
    });
    if(!coffee){
      throw new NotFoundException(`Coffee ${id} not found`)
    }
    return this.coffeeRepository.save(coffee);   // 保存入数据库中,返回一个期约
  }
  async remove(id: string) {
    const coffee = await this.findOne(id);
    return this.coffeeRepository.remove(coffee);
  }


测试:

等...

关系

  • 一对一@OneToOne():主表的每一行在外部表都有且只有一个关联行;
  • 一对多@OneToMany(),多对一@ManyToOne():主表的每一行在外部表中都有一个或多个相关行;
  • 多对多@ManyToMany():主表中的每一行在外表都有许多相关的行,并且外表中的每条记录在主表中都有许多相关的行;

arduino


// nest g class coffees/entities/flavor.entity --no-spec


将类名FlavorEntity==>Flavor,因为我们不希望数据库出现Entity这样的后缀。

然后修改flavors属性为多对多:

// coffee.entity
@Entity()
export class Coffee {
 // ...
  @JoinTable()  // 该装饰器有助于指定关系的owner端,在这里是coffee
  @ManyToMany(type=>Flavor, (flavor)=>flavor.coffees)  // 第二个参数为反向怎么指过来(关系的反面)
  flavors: string[];
}


// flavor.entity
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Coffee } from './coffee.entity';
@Entity()
export class Flavor {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  name: string;
  @ManyToMany(type => Coffee, coffee => coffee.flavors)
  coffees: Coffee[];  // 由于Coffee是这种关系的所有者,我们不必再次使用@JoinTable
}


效果,数据库多出一张Flavor表以及一张关系表:

查询数据

现在就没有查到与之关联的Flavor关系了,因为默认情况下是不会直接加载关系的。

修改:

typescript

// coffees.service
findAll() {
    return this.coffeeRepository.find({
        relations: ['flavors']
    });
}
  async findOne(id: string) {
    const coffee = await this.coffeeRepository.findOne(id, {
      relations: ['flavors']
    });
  // ...
  }

级联插入


首先在关系内部将Casecade属性设置为true

// coffee.entity中
@ManyToMany((type) => Flavor, (flavor) => flavor.coffees, {cascade: true})

我们也可以将级联限制为仅插入或仅更新:true==>[‘insert’]

其他部分

在此之前,我们首先需要将Flavor Respository注入到CoffeesService

// coffee.service
// 1
export class CoffeesService {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository: Repository<Coffee>,
    @InjectRepository(Flavor)
    private readonly flavorRepository: Repository<Flavor>,
  ) {}
    // ...


然后修改创建和更新的方法

// 2.
 // 先定义创建flavor的方法
  private async preloadFlavorByName(name: string): Promise<Flavor> {
    const existingFlavor = await this.flavorRepository.findOne({ name });
    if (existingFlavor) {
      return existingFlavor;
    }
    return this.flavorRepository.create({ name });
  }


// 3.
  async create(createCoffeeDto: CreateCoffeeDto) {
    // 1 创建
    const flavors = await Promise.all(
      createCoffeeDto.flavors.map((name) => this.preloadFlavorByName(name)),
    );
    // 2 结合
    const coffee = this.coffeeRepository.create({
      ...createCoffeeDto,
      flavors,
    }); // 创建对应实例
    return this.coffeeRepository.save(coffee); // 保存入数据库中,返回一个期约
  }
  async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
      // 有则使用,无则创建
    const flavors = updateCoffeeDto.flavors && 
    (await Promise.all(
      updateCoffeeDto.flavors.map(name=>this.preloadFlavorByName(name)),
    ))
    // preload 会首先查看数据库是否存在实体,存在则会更新,否则返回undefined
    const coffee = await this.coffeeRepository.preload({
      id: +id,
      ...updateCoffeeDto,
      flavors,
    });
    if (!coffee) {
      throw new NotFoundException(`Coffee ${id} not found`);
    }
    return this.coffeeRepository.save(coffee); // 保存入数据库中,返回一个期约
  }


此时你创建coffee对象时,没有对应的flavors选项会自动创建入库;

分页查询


生成对应的数据传输对象

//  nest g class common/dto/pagination-query.dto --no-spec


将那些不与特定域相联系的,可以被多个控制器重复使用的放common这个文件夹下。

// pagination-query.dto
import { Type } from 'class-transformer';
import { IsOptional, IsPositive } from 'class-validator';
export class PaginationQueryDto {
  @IsOptional()  // 缺失或者未定义将抛出错误
  @IsPositive()  // 检查为正数
  @Type(() => Number)  // 确保传入的值被解析为数字
  limit: number;
  @IsOptional()
  @Type(() => Number)
  offset: number;
}

当然,关于@Type(() => Number)你可以全局配置如下:

// coffees.controller
@Get('flavors')
findAll(@Query() paginationQuery:PaginationQueryDto) {
    return this.coffeeService.findAll(paginationQuery);
}


// coffees.service
findAll(paginationQuery: PaginationQueryDto) {
    const { limit, offset } = paginationQuery;
    return this.coffeeRepository.find({
        relations: ['flavors'],
        skip: offset,
        take: limit,
    });
}

事务


当我们想要在某函数调用完成之后并存储该事件,这是对数据库的两项操作,为了保证一致性,我们需要用到事务。

先生成对应的实体

// nest g class events/entities/event.entity  --no-spec


然后添加一些列

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Event {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  type: string;
  @Column()
  name:string;
  @Column('json')
  payload: Record<string, any>;  // 存储event payload的通用列
}


同样的你需要添加到TypeORM forFeature()

@Module({
  imports:[TypeOrmModule.forFeature([Coffee, Flavor, Event])],
    // ...


Coffee实体添加一列

  @Column({default: 0})
  recommendations: number;

回到coffees.service中把所有的东西放在一起

为了创建事务,我们将使用TypeORM中的Connection对象,使用传统方式将Connection注入到我们的CoffeeService构造函数中,无需任何装饰器。

@Injectable()
export class CoffeesService {
  constructor(
  // ...
    private readonly connection: Connection,
  ) {}


现在我们就可以创建我们的第一个事务,并将其命名为recommend

async recommendCoffee(coffee: Coffee) {
    const queryRunner = this.connection.createQueryRunner(); // 创建queryRunner
    await queryRunner.connect(); // 连接数据库
    await queryRunner.startTransaction(); // 开始事务
    try {
        coffee.recommendations++;
        const recommendEvent = new Event();
        recommendEvent.name = 'recommend_coffee';
        recommendEvent.type = 'coffee';
        recommendEvent.payload = { coffeeId: coffee.id };
        await queryRunner.manager.save(coffee);
        await queryRunner.manager.save(recommendEvent);
        await queryRunner.commitTransaction();
    } catch (err) {
        await queryRunner.rollbackTransaction();  // 回滚
    } finally {
        await queryRunner.release();
    }
}


这样我们就实现了对数据库的多个操作,确保它们只有在一切都成功的情况下才会发生。

索引


假设一个非常常见的搜索请求就是:根据名称检索一个事件。

我们可以使用@index()装饰器在对应的name上定义一个索引。

  @Index()
  @Column()
  name:string;


更高级的情况下,我们可能想要定义包含多个列的复合索引,我们可以将@index()装饰器添加到Event类本身,并在装饰器内传递一个列名数组作为参数。

@Index(['name', 'type'])
@Entity()
export class Event {
  // ...
}

索引就是空间换事件可以帮助我们的应用程序快速查找和有效访问。

迁移



一种增量更新数据库的模式与应用程序保持同步的方法,同时保留数据库的现有信息。

迁移类与我们的Nest应用程序源代码是分开的,这是因为它们的生命周期由TypeORM CLI维护,由于迁移为Nest之外,我们无法利用依赖注入和其他Nest特定功能进行数据库迁移。在创建新的迁移之前,我们需要创建一个新的TypeORM配置文件并正确设置我们的数据库连接

根目录中创建ormconfig.js

module.exports = {
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'pass123',
  database: 'postgres',
  entities: ['dist/**/*.entity.js'],
  migrations: ['dist/migrations/*.js'],
  cli:{
    migrationsDir: 'src/migrations',
  },
};

开始迁移:

// npx typeorm migration:create -n CoffeeRefactor


npx可以让我们使用可执行包而不用安装它们。

synchronize: true,的区别就是生产环境使用的和开发环境使用的,因为不使用迁移而直接修改列名的话会删除该列的全部数据。

// 迁移的记录如下
import {MigrationInterface, QueryRunner} from "typeorm";
export class CoffeeRefactor1650897347302 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
    }  // 指示需要更改的内容以及如何更改的内容
    public async down(queryRunner: QueryRunner): Promise<void> {
    }  // 是我们要撤销或回滚任何这些更改的地方,万一我们的迁移出现问题,需要一个退出策略。
}

当我们将name===>title我们需要这样做:

    public async up(queryRunner: QueryRunner): Promise<void> {
        `ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"`
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
        `ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"`
    }


测试:

// npm run build
// npx typeorm migration:run


恢复我们的更改

// npx typeorm migration:revert

当然npx typeorm migration:create -n会自动对比你的实体与数据库中的,自动生成对应的迁移命令alter table等。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
27天前
|
关系型数据库 分布式数据库 数据库
成都晨云信息技术完成阿里云PolarDB数据库产品生态集成认证
近日,成都晨云信息技术有限责任公司(以下简称晨云信息)与阿里云PolarDB PostgreSQL版数据库产品展开产品集成认证。测试结果表明,晨云信息旗下晨云-站群管理系统(V1.0)与阿里云以下产品:开源云原生数据库PolarDB PostgreSQL版(V11),完全满足产品兼容认证要求,兼容性良好,系统运行稳定。
|
1月前
|
关系型数据库 分布式数据库 数据库
PolarDB常见问题之数据库不能自己减少节点如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
1月前
|
缓存 关系型数据库 分布式数据库
PolarDB常见问题之数据库cpu突然飙高如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
2天前
|
关系型数据库 OLAP 分布式数据库
「杭州*康恩贝」4月26日PolarDB开源数据库沙龙,开启报名!
4月26日周五,PolarDB开源社区联合康恩贝将共同举办开源数据库技术沙龙,本次沙龙我们邀请了众多数据库领域的专家,期待大家的参与!
「杭州*康恩贝」4月26日PolarDB开源数据库沙龙,开启报名!
|
12天前
|
运维 关系型数据库 分布式数据库
「合肥 * 讯飞」4 月 19 日 PolarDB 开源数据库沙龙,报名中!
4月19日周五,PolarDB开源社区联合科大讯飞共同举办开源数据库技术沙龙,本次沙龙我们邀请了众多数据库领域的专家,期待大家的参与!
「合肥 * 讯飞」4 月 19 日 PolarDB 开源数据库沙龙,报名中!
|
1月前
|
存储 关系型数据库 分布式数据库
PolarDB常见问题之PolarDB突然有大量服务连不上数据库如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
1月前
|
存储 关系型数据库 MySQL
TiDB与MySQL、PostgreSQL等数据库的比较分析
【2月更文挑战第25天】本文将对TiDB、MySQL和PostgreSQL等数据库进行详细的比较分析,探讨它们各自的优势和劣势。TiDB作为一款分布式关系型数据库,在扩展性、并发性能等方面表现突出;MySQL以其易用性和成熟性受到广泛应用;PostgreSQL则在数据完整性、扩展性等方面具有优势。通过对比这些数据库的特点和适用场景,帮助企业更好地选择适合自己业务需求的数据库系统。
|
1月前
|
Cloud Native 关系型数据库 分布式数据库
**PolarDB IMCI:云原生时代的智能数据库新选择**
**PolarDB IMCI:云原生时代的智能数据库新选择**
26 4
|
1月前
|
关系型数据库 MySQL 分布式数据库
PolarDB for MySQL数据库外网连接解析失败的原因可能有以下几点
【2月更文挑战第16天】PolarDB for MySQL数据库外网连接解析失败的原因可能有以下几点
24 1
|
1月前
|
SQL JavaScript 关系型数据库
js链接sql数据库
js链接sql数据库

热门文章

最新文章