likes
comments
collection
share

Nest探究(二): Nest的TypeOrm前言 连接数据库 既然是后端的项目,连接数据库是必不可少的,这里我选择更为

作者站长头像
站长
· 阅读数 94

前言

最近在探究Nest,发现这东西还不错,分享给大家,上一篇文章Nest探究(一):Nest已经大致说明了一下Nest的结构以及Nest简单的使用,而这篇文章将会介绍Nest数据库的相关操作,如有错误,还请各位大佬指出!

连接数据库

既然是后端的项目,连接数据库是必不可少的,这里我选择更为熟悉的MySQL数据库。

而Nest当中,有很多连接数据库的ORM(对象关系映射器),如TypeORMSequelizePrisma,在此我选择TypeORMSequelize实际上与koa一致,是使用sqly语句,来对数据库进行操作,而TypeORM它本身是由TypeScript写的,对Nest的支持相对较好,而且Nest也提供了@nestjs/typeorm包,对于一些常用的增删改查sql语句也是进行了函数封装,开箱即用,比较方便;Prisma呼声很高,有兴趣的同学可以自行查看,在这里我使用TypeORM

在此之前,我先解释一下,什么是ORM? 举个例子,我们有如下一张表,

 +----+--------+------------+ 
 | id | title | text |
 +----+-------------+--------------+
 | 1 |  标题   | 内容文本 | 
 +----+--------+------------+

而这张表的每一行,可以使用一个javaScript对象来表示

{
    id: 1,
    title:"标题",
    content:"内容文本"
}

ORM就是这样,把关系数据库的结构映射到对象上。而我们只要传入这些结构的javaScript对象,数据库就会自行对应存储数据,是不是简单方便?接下来,我们看看如何使用TypeORM

  • 首先我们需要安装依赖

npm install --save @nestjs/typeorm

  • 其次使用TypeORM连接mysql数据库,也需要安装依赖

npm install --save typeorm mysql2

环境配置

  • 首先需要在项目根目录下创建两个文件.env文件和.env.prod文件,存储的分别是开发环境和线上环境不同的环境变量。
// 数据库地址
DB_HOST=localhost  
// 数据库端口
DB_PORT=3306
// 数据库登录名
DB_USER=root
// 数据库登录密码
DB_PASSWD=root
// 数据库名字
DB_DATABASE=demo

.env.prod中的是上线要用的数据库信息,如果你的项目要上传到线上管理,为了安全性考虑,建议这个文件添加到.gitignore中。

  • 接着在根目录下创建一个文件夹config(与src同级),然后再创建一个env.ts用于根据不同环境读取相应的配置文件。
import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV === 'production';

function parseEnv() {
  const localEnv = path.resolve('.env');
  const prodEnv = path.resolve('.env.prod');

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('缺少环境配置文件');
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
  return { path:filePath };
}
export default parseEnv();
  • nest带有环境配置
yarn add @nestjs/config
  • @nestjs/config 默认会从项目根目录载入并解析.env 文件,从.env文件和process.env合并环境变量键值对,forRoot() 方法注册了 ConfigService 提供者,后者提供了一个 get() 方法来读取这些解析/合并的配置变量。要注入ConfigService,需要在需要使用的地方先导入ConfigModule
  • 而在app.module中使用了ConfigModule.forRoot(),将isGlobal设置为true,在其他地方使用时不需要做任何事,表示全局使用,此时就可以在全局范围内使用process.env.xxx读取全局变量
  • app.module.ts文件中使用@nestjs/config进行全局配置,以及使用@nestjs/typeorm提供的TypeOrmModule连接数据库如下
  • 在app.module.ts使用
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
// 环境配置相关
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'mysql',
        host: process.env.DATABASE_HOST,
        port: +process.env.DATABASE_PORT, // 来自process.env的每个值都是字符串,前面加+转数字
        username: process.env.DATABASE_USER,
        password: process.env.DATABASE_PASSWORD,
        database: process.env.DATABASE_NAME,
        autoLoadEntities: true, // 自动加载模块 推荐
        // entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')], // 不推荐
        synchronize: true // 开启同步,生产中要禁止
      })
    }),
  ],
  controllers: [],
  providers: []
})
export class AppModule {}
  • 上文代码中forRootAsync使用了TypeORM的异步工程模式,这样可以解决imports的顺序问题,也就是说,使用了forRootAsync,可以不用在意imports这个数组中使用TypeOrmModule的顺序.
  • 因为每个创建的实体必须在连接选项中进行注册,所以TypeORM提供了autoLoadEntities来自动加载创建的数据库实体,使用这种方式也比较推荐,也可以使用entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')]的方式,这表示,指定包含所有实体的整个目录,该目录下所有实体都将被加载.

Entity(实体)

使用@Entity()来标记通过定义一个新类创建的实体,装饰器根据编写的类,通过映射自动生成一个SQL表,以及他们包含的元数据,其中包含

Column(普通列)

  • @Colimn()来标记普通列

  • 为列指定类型:

    • @Column("int")/@Column({ type: "int" }):数字类型
  • 还需要指定其他参数:

    • @Column("varchar", { length: 200 }):字符串类型,长度为200
    • @Column({ type: "int", length: 200 }):数字类型,长度为200
列选项
  • 列选项定义实体列的其他选项。 你可以在@Column()上指定列选项:

    • title: string: 数据库表中的列名。
    • unique: true:将列标记为唯一列,里面的值不可重复
    • nullable: boolean: 在数据库中使列NULLNOT NULL。 默认情况下,列是nullable:false
    • 更多的请看TypeORM中文文档

主列

  • 每个实体都必须要有一个主列

  • @PrimaryColumn()来标记主列,需要给它手动分配值

  • @PrimaryGeneratedColumn()来标记主列,该值将使用自动增量值自动生成

  • @PrimaryGeneratedColumn('uuid')来标记主列,该值将使用uuid(通用唯一标识符)自动生成,uuid可以被认为是唯一的

    • uuid是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的uuid。在这样的情况下,就不需考虑数据库创建时的名称重复问题
    • uuid的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-1232个字符,如:550e8400-e29b-41d4-a716-446655440000

关系

OneToOne(一对一关系)

  • 使用OneToOne指明一对一的关系,在一对一关系中,表A中的一条记录,只能关联表B中的一条记录。比如:每一个用户都只能有一个用户档案,
  • 在用户表的实体当中使用OneToOne指明一对一的关系,并使用档案表实体作为用户表的某个属性。使用@ JoinColumn定义外键,并允许自定义连接列名和引用的列名。

@JoinColumn 必须在且只在关系的一侧的外键上, 设置@JoinColumn在该表的实体中,该表将会包含一个relation id和目标实体表的外键。记住,不能同时在二者的entity中使用。

OneToMany(一对多关系)

  • 使用OneToMany指明一对多的关系,在一对多关系中,表A中的一条记录,可以关联表B中的一条或多条记录。比如:每一个文章分类都可以对应多篇文章,反过来一篇文章只能属于一个分类,这种分类表和文章表的关系就是一对多的关系。
  • ,即分类表的实体当中使用OneToMany指明一对多的关系,并使用即文章表的实体作为的某个属性。
  • ,即文章表的实体当中使用ManyToOne指明关系,并使用即分类表的实体作为的某个属性。使用@ JoinColumn定义外键,并允许自定义连接列名和引用的列名。
  • TypeORM在处理“一对多”的关系时, 将的主键作为的外键,即@ManyToOne装饰的属性;这样建表时用最少的数据表操作代价,避免数据冗余,提高效率。

ManyToMany(多对多关系)

  • @ManyToMany()指明多对多关系,多对多是表A的一条记录关联表B的多个记录,而表B的一条记录也可以关联表A的多条记录的关系。比如:一篇文章可以对应多个板块,而一个板块下面也有多个文章。

  • @JoinTable用于描述“多对多”关系, @JoinTable()@ManyToMany()关系所必需的, 通过配置joinColumnsinverseJoinColumns来自定义中间表的列名称。

  • 保存这种关系,需要启动级联cascade

    • @ManyToMany((type) => Role, (role) => role.users, { cascade: true })

使用TypeORM

  • 进行注册实体,首先我们创建user用户实体。

    • Role是角色实体
// user.entity.ts
import {
  Entity,
  Column,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn
} from 'typeorm';

import { Role } from './role.entity';

// @Entity()装饰器自动从所有类生成一个SQL表,以及他们包含的元数据
// @Entity('users') // sql表名为users
@Entity() // sql表名为user
export class User {
  // 主键装饰器,也会进行自增
  @PrimaryGeneratedColumn()
  id: number;

  // 列装饰器
  @Column()
  username: string;

  // @Column('json', { nullable: true }) json格式且可为空
  @Column()
  password: string;

  // 定义与其他表的关系
  // name 用于指定创中间表的表名
  @JoinTable({ name: 'user_roles' })
  // 指定多对多关系
  /**
   * 关系类型,返回相关实体引用
   * cascade: true,插入和更新启用级联,也可设置为仅插入或仅更新
   * ['insert']
   */
  @ManyToMany((type) => Role, (role) => role.users, { cascade: true })
  roles: Role[];

  @CreateDateColumn()
  createAt: Date;

  @UpdateDateColumn()
  updateAt: Date;
}
复制代码
  • 创建角色实体
// role.entity.ts
import {
  Entity,
  Column,
  ManyToMany,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn
} from 'typeorm';

import { User } from './user.entity';

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

  @Column()
  name: string;

  @ManyToMany((type) => User, (user) => user.roles)
  users: User[];

  @CreateDateColumn()
  createAt: Date;

  @UpdateDateColumn()
  updateAt: Date;
}

在查找关联表的信息时,需要注意:

  • 使用relations表示需要加载主体,如下是一个查询用户列表所有数据的代码
    • 方式一:relations: { roles: true }

    • 方式二:relations: ['roles']

async getUserList() {
    return await this.userRepository.find({
      // 1
      // relations: {
      //   roles: true
      // },
      // 2
      relations: ['roles'],
    });
  }

方法

TypeOrm其中有封装好的操作数据库的函数,代码奉上。

async create(createArticleDto: CreateArticleDto) {
    const article = await this.articleRepository.create({
      ...createArticleDto
    });
    return await this.articleRepository.save(article);
  }

  async getArticleList(paginationsQuery: PaginationQueryDto) {
    const { limit, offset } = paginationsQuery;
    return await this.articleRepository.find({
      skip: offset,
      take: limit
    });
  }

  async findOneById(id: number) {
    return await this.articleRepository.findOneBy({ id });
  }

  async update(id: number, updateArticleDto: UpdateArticleDto) {
    const article = await this.articleRepository.preload({
      id,
      ...updateArticleDto
    });
    if (!article) {
      throw new NotFoundException(`${id} not found`);
    }
    return await this.articleRepository.save(article);
  }

  async remove(id: number) {
    const article = await this.articleRepository.findOneBy({ id });
    if (!article) {
      throw new NotFoundException(`${id} not found`);
    }
    return await this.articleRepository.remove(article);
  }

总结

这里我们只是讲解了TypeOrm的数据库连接和基本操作,下一篇我们将会讲到一些更加复杂的管道配置和各种装饰器。

我是小白,我们一起学习!

转载自:https://juejin.cn/post/7184350230492479545
评论
请登录