Skip to content

TypeORM

1.基本介绍

不同于现有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active RecordData Mapper 模式,这意味着你可以以最高效的方式编写高质量的、松耦合的、可扩展的、可维护的应用程序。

TypeORM 参考了很多其他优秀 ORM 的实现, 比如 Hibernate, DoctrineEntity Framework

TypeORM 可以应用于很多的平台,这里只介绍它在 nestjs 里面的应用

2.基本使用

1.安装

nest.js对他做了很好的集成, 虽然它的官网写的挺全的但是实际开发起来还是不太够, 并且里面有大坑我会把我知道的都列出来, 这篇也会把一些常见的解决方案写出来。

bash
yarn add @nestjs/typeorm typeorm mysql2 -S

2.连接数据库

/share/src/app.module.ts

ts
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 模型将自动载入。

3.实体

1.建立实体

实体是一个映射到数据库表(或使用 MongoDB 时的集合)的类。 你可以通过定义一个新类来创建一个实体,并用@Entity()来标记:

实体其实就是对应了一张表, 这个实体的 class 名字必须与表名对应, 新建 entity 文件夹 /share/src/modules/goods/entity/goods.entity.ts:

typescript
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";

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

  @PrimaryGeneratedColumn()
  id: number;

  @PrimaryGeneratedColumn("uuid")
  id: number;

  @Column()
  name: string;

  @CreateDateColumn()
  date: Date;

  @UpdateDateColumn()
  date: Date;
}
  1. @PrimaryColumn()装饰了 id 为主键, 类型为数字。

  2. @PrimaryGeneratedColumn()装饰了 id 为主键, 类型为数字。

  3. @PrimaryGeneratedColumn("uuid")创建一个主列,该值将使用uuid自动生成。 Uuid 是一个独特的字符串 id。 你不必在保存之前手动分配其值,该值将自动生成。

  4. @Column()装饰普通行, 类型为字符串。

  5. @CreateDateColumn()是一个特殊列,自动为实体插入日期。无需设置此列,该值将自动设置。

  6. @UpdateDateColumn()以后每次更新数据都会自动的更新这个时间值

属性命名

注意:数据库用_命名,实体类用驼峰即可!!!

列类型

TypeORM 支持所有最常用的数据库支持的列类型。 列类型是特定于数据库类型的 - 这为数据库架构提供了更大的灵活性。

写法:

@Column("int")

@Column({ type: "int" })

@Column("varchar", { length: 200 }) //注意:当长度不是 255 的时候一定要这样写,否则会误删表!

@Column({ type: "int", length: 200 })

注意:mysql/mariadb的列类型

int`, `tinyint`, `smallint`, `mediumint`, `bigint`, `float`, `double`, `dec`, `decimal`, `numeric`, `date`, `datetime`, `timestamp`, `time`, `year`, `char`, `varchar`, `nvarchar`, `text`, `tinytext`, `mediumtext`, `blob`, `longtext`, `tinyblob`, `mediumblob`, `longblob`, `enum`, `json`, `binary`, `geometry`, `point`, `linestring`, `polygon`, `multipoint`, `multilinestring`, `multipolygon`, `geometrycollection

注意事项:

默认情况下,列名称是从属性的名称生成的。 你也可以通过指定自己的名称来更改它。

  • length: number - 列类型的长度。 例如,如果要创建varchar(150)类型,请指定列类型和长度选项。(如果不指定会出现错误,默认是 255)

2.实体继承

这些实体都有共同的列:idtitledescription。 为了减少重复并产生更好的抽象,我们可以为它们创建一个名为Content的基类:

js
export abstract class Content {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    description: string;
}
@Entity()
export class Photo extends Content {
    @Column()
    size: string;
}

@Entity()
export class Question extends Content {
    @Column()
    answersCount: number;
}

@Entity()
export class Post extends Content {
    @Column()
    viewCount: number;
}

来自父实体的所有列(relations,embeds 等)(父级也可以扩展其他实体)将在最终实体中继承和创建。

3.嵌入式实体

假设我们有UserEmployeeStudent实体。

这些属性都有少量的共同点,first namelast name属性。

我们可以做的是通过创建一个包含firstNamelastName的新类:

typescript
import { Entity, Column } from "typeorm";

export class Name {
  @Column()
  first: string;

  @Column()
  last: string;
}

然后"connect"实体中的这些列:

typescript
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Name } from "./Name";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: string;

  @Column((type) => Name)
  name: Name;

  @Column()
  isActive: boolean;
}
typescript
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Name } from "./Name";

@Entity()
export class Employee {
  @PrimaryGeneratedColumn()
  id: string;

  @Column((type) => Name)
  name: Name;

  @Column()
  salary: number;
}

Name实体中定义的所有列将合并为useremployee:

js
+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| nameFirst   | varchar(255) |                            |
| nameLast    | varchar(255) |                            |
| isActive    | boolean      |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        employee                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| nameFirst   | varchar(255) |                            |
| nameLast    | varchar(255) |                            |
| salary      | int(11)      |                            |
+-------------+--------------+----------------------------+

注意:名字是反过来了,注意数据库的命名!

这种方式可以减少实体类中的代码重复。 你可以根据需要在嵌入式类中使用尽可能多的列(或关系)。 甚至可以在嵌入式类中嵌套嵌套列。

3.注册实体到 Module

nest自身设计的还不是很好, 引入搞得好麻烦 /share/src/modules/goods/goods.module.ts:

coffeescript
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() 方法定义在当前范围中注册哪些存储库。

5.引入实体到 Service

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

typescript
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方法后面会举例出更多。

4.关系

typeorm 的强大之处在于,应该先建立实体,再建立表,会省很多时间!! 不需要根据数据库表来建实体!

而关系可以很好地帮我们处理一对多和多对一关系!免去了一些中立表的建立!所以关系的建立是必要的!

说白了关系的建立,就是为了简化代码,简化表,帮助我们快速开发的!

关系可以帮助你轻松地与相关实体合作。 有几种类型的关系:

多对多关系?

举个例子,假设有一个博客系统,其中Post表示文章,Category表示文章的分类。一个文章可以属于多个分类,而一个分类下也可以有多篇文章。这种多对多的关系可以通过中间表来表示,中间表中记录了PostCategory之间的对应关系。

1.typeorm 中关系的理解

一开始我认为关系不重要,不需要外键就不需要定义关系,这是个鸡肋的功能,但是当我进行实践之后才发现,关系这种定义正是 typeorm 这种数据库操作工具最强大的地方,虽然使用关系会有点不适应和不容易理解,但是它存在以下的绝佳好处:

1.由于 typeorm 本身就是一种强大的实体与数据库同步的操作系统,它的优势就是可以通过实体自动创建表,将权力进行了反转!

为了发挥这种优势,我们就一定要应用关系这一强大的功能!

关系可以很好地帮我们处理一对多和多对一关系!免去了一些中立表的建立!

2.本质上多对一的关系也至少要进行一次循环(或者联表查询)才可以查询完整数据,关系的建立省去了 for 循环,省略了大量查询代码,提高了查询效率

多对一:避免数组字符串的关联(比如某个字段是[1,3,4]这样的字符串),避免新建一张表

多对多:避免新建一张表

image-20240313153408932

多对一时,利用数组字符串的关联查询,和利用关系查询,代码的区别:

jsx
  async getHobby(id: number) {
    //QueryBuilder
    /*
      或者这样写:
      this.hobbysRepository.findOne({
            where: {
                id: id,
            }
        })
    */
    let hobby = await this.hobbysRepository.createQueryBuilder().where("id = :id", { id: id }).getOne();
    return hobby.name;
  }
async getUserWithHobbyList(): Promise<UserType[]> {
    let userList = await this.usersRepository.find();
    let users = [] as UserType[];
    for (let j = 0; j < userList.length; j++) {
      let hobbys = JSON.parse(userList[j].hobbyIds.valueOf()).map(Number);
      console.log(hobbys)
      let theUserHobbys = hobbys.map(e => {
        return this.getHobby(e);
      })
      delete e.hobbyIds;
      let user = Object.assign(e, {
        hobbys: theUserHobbys
      })
      users.push(user);
    }
    return users;
  }

现在:省了非常多的代码!

jsx
async getUserWithHobbyList(): Promise<UserType[]> {
	return this.usersRepository.find({ relations: ["hobbys"] })
}

2.forEach 不能执行 await

1.原因

这是因为async 和 await 必须成对出现,如果调用 await 的地方无法正确匹配到 async 则会报错, forEach属于并发操作,在 forEach 循环内调用异步函数时,下一个循环并不会等到上个循环结束后再被调用,所以以上调用方式才会报错。

2.解决方案

以下将介绍 forEach 和其他循环中 async/await 异步调用方法

  1. forEach 循环中处理
jsx
async detailData (newVal) {
  await Promise.all(
     newVal.forEach(async line => {
       var list = await infoStore(line.id)
     })
  )
}
  1. for of 中处理
jsx
async detailData (newVal) {
  for (let item of newVal){
 var list = await infoStore(item.id)
  }
}
  1. for 循环中处理
jsx
async detailData (newVal) {
  for (var i=0; i<newVal.length; i++) {
 var list = await infoStore(newVal[i].id)
  }
}
  1. map 中处理
jsx
async detailData (newVal) {
  let list = newVal.map(async item => {
 return await infoStore(item.id)
  })
  // 这时候 list 拿到的值还是promise数据
  // 需要再使用promise.all()处理一下,最终listEnd就是Promise里面的数据
  let listEnd = await Promise.all(list)
}

3.数据库外键的理解

外键是否采用看业务应用场景,以及开发成本的,大致列下什么时候适合,什么时候不适合使用:

互联网行业应用不推荐使用外键:

用户量大,并发度高,为此数据库服务器很容易成为性能瓶颈,尤其受 IO 能力限制,且不能轻易地水平扩展;若是把数据一致性的控制放到事务中,也即让应用服务器承担此部分的压力,而引用服务器一般都是可以做到轻松地水平的伸缩;

为何说外键有性能问题:

1.数据库需要维护外键的内部管理;

2.外键等于把数据的一致性事务实现,全部交给数据库服务器完成

3.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;

4.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;

外键的使用和必要性:

  1. 外键字段和主键字段的名称可以不同,但是类型应该一致。

  2. 外键的必要性:

不用外键约束,也可以进行关联查询,但是有了它,MySQL 系统才会保护你的数据,避免出现误删的情况,从而提高系统整体的可靠性。

为什么在 MySQL 里,没有外键约束也可以进行关联查询呢?

(1)原因是外键约束是有成本的,需要消耗系统资源。对于大并发的 SQL 操作,有可能会不适合。比如大型网站的中央数据库,可能会因为外键约束的系统开销而变得非常慢。所以,MySQL 允许你不使用系统自带的外键约束,而是在应用层面完成检查数据一致性的逻辑。也就是说,即使你不用外键约束,也要想办法通过应用层面的附加逻辑,来实现外键约束的功能,确保数据的一致性。

(2)外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

(3)“外键约束,可以简单有效的保证数据的可靠性。适合内部管理系统使用,因为访问量不会太大。如果是面向外界用户使用的应用,外键所带来的性能损耗,可能无法支撑大批用户的同时访问。但是,数据的可靠性和唯一性才是最重要的,在保证一次对多张表操作的过程中,可以使用事务来确保操作的一致性。”===》作者回复: 在系统开销和使用的功能之间需要做好平衡,既要确保数据可靠性和唯一性,又要确保系统可用.

如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?

应用层面实现外键约束的功能,指的就是在应用里面加上检查。比如删除主表中记录的时候,需要检查从表中是否有记录引用到主表中的这条记录,有的话就不能删除,否则可以删除(或者连着从表中的数据一起删除)。各种不同的框架有自己的特点,要根据实际情况决定如何处理。

实际案例:

注意:带有外键的表称之为副表,不带外键的表称之为主表。

在这里插入图片描述

4.一对一

注意:如果我们不想要外键,那么就不要设置这个一对一的关系!

一对一是一种 A 只包含一个 B 实例,而 B 只包含一个 A 实例的关系。 我们以UserProfile实体为例。

用户只能拥有一个配置文件,并且一个配置文件仅由一个用户拥有。

jsx
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToOne,
  JoinColumn,
} from "typeorm";
import { Profile } from "./Profile";

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

  @Column()
  name: string;

  @OneToOne(() => Profile)
  @JoinColumn()
  profile: Profile;
}
js
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm";
import { User } from "./User";

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

  @Column()
  gender: string;

  @Column()
  photo: string;

  @OneToOne(() => User, (user) => user.profile) // 将另一面指定为第二个参数
  user: User;
}

这里我们将@OneToOne添加到profile并将目标关系类型指定为Profile。 我们还添加了**@JoinColumn,这是必选项并且只能在关系的一侧设置**。 你设置@JoinColumn的哪一方,哪一方的表将包含一个"relation id"和目标实体表的外键。

注意:@JoinColumn = 外键,外键应该是存在于主表上面的,主表的外键指向附表的主键!

此示例将生成以下表:

bash
+-------------+--------------+----------------------------+
|                        profile                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| gender      | varchar(255) |                            |
| photo       | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| profileId   | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

要加载带有配置文件的用户,必须在FindOptions中指定关系:

typescript
const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["profile"] });

5.一对多/多对一

多对一/一对多是指 A 包含多个 B 实例的关系,但 B 只包含一个 A 实例。 让我们以UserPhoto 实体为例。 User 可以拥有多张 photos,但每张 photo 仅由一位 user 拥有。

jsx
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { User } from "./User";

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

  @Column()
  url: string;

  @ManyToOne(() => User, (user) => user.photos)
  user: User;
}
jsx
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Photo } from "./Photo";

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

  @Column()
  name: string;

  @OneToMany(() => Photo, (photo) => photo.user)
  photos: Photo[];

  //特别注意:不要在实体中初始化数组!否则会误删数据!
  /**
  	即不要写:
  	photos: Photo[] = [];
  */
}

可以在@ManyToOne / @OneToMany关系中省略@JoinColumn除非你需要自定义关联列在数据库中的名称

@ManyToOne可以单独使用,但@OneToMany必须搭配@ManyToOne使用。

在你设置@ManyToOne的地方,相关实体将有"关联 id"和外键。

注意:关联 id 就会被设置为外键

此示例将生成以下表:

bash
+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| url         | varchar(255) |                            |
| userId      | int(11)      |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

要在内部加载带有 photos 的 user,必须在FindOptions中指定关系:

typescript
const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["photos"] });

// or from inverse side

const photoRepository = connection.getRepository(Photo);
const photos = await photoRepository.find({ relations: ["user"] });

数据库表形式:

image-20240313153633365

注意:userId 字段是设立关系之后自动建立的,一对多中“多”的表,会有重复数据(userId 不同的),避免新建一张表

image-20240313153646586

6.多对多

多对多是一种 A 包含多个 B 实例,而 B 包含多个 A 实例的关系。 我们以QuestionCategory 实体为例。 Question 可以有多个 categories, 每个 category 可以有多个 questions。

jsx
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Question } from "./Question";

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

  @Column()
  name: string;

  @ManyToMany(() => Question, (question) => question.categories)
  questions: Question[];
}
jsx
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToMany,
  JoinTable,
} from "typeorm";
import { Category } from "./Category";

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

  @Column()
  title: string;

  @Column()
  text: string;

  @ManyToMany(() => Category, (category) => category.questions)
  @JoinTable()
  categories: Category[];
}

@JoinTable()@ManyToMany关系所必需的。 你必须把@JoinTable放在关系的一个(拥有)方面。

此示例将生成以下表:

bash
+-------------+--------------+----------------------------+
|                        category                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        question                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|              question_categories_category               |
+-------------+--------------+----------------------------+
| questionId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
| categoryId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

要在 categories 里面加载 question,你必须在FindOptions中指定关系:

typescript
const questionRepository = connection.getRepository(Question);
const questions = await questionRepository.find({ relations: ["categories"] });

7.避免创建外键约束

有时出于性能原因,您可能希望在实体之间建立关系,但不需要外键约束。 您可以使用createForeignKeyConstraints选项来定义是否应该创建外键约束(默认值: true)。

jsx
import { Entity, PrimaryColumn, Column, ManyToOne } from "typeorm";
import { Person } from "./Person";
@Entity()
export class ActionLog {
  @PrimaryColumn()
  id: number;
  @Column()
  date: Date;
  @Column()
  action: string;

  @ManyToOne((type) => Person, {
    createForeignKeyConstraints: false,
  })
  person: Person;
}

5.Entity Manager 和 Repository

1.什么是 EntityManager

使用EntityManager,你可以管理(insert, update, delete, load 等)任何实体。 EntityManager 就像放一个实体存储库的集合的地方。

你可以通过getManager()Connection访问实体管理器。

如何使用它:

typescript
import { getManager } from "typeorm";
import { User } from "./entity/User";

const entityManager = getManager(); // 你也可以通过 getConnection().manager 获取
const user = await entityManager.findOne(User, 1);
user.name = "Umed";
await entityManager.save(user);

2.什么是 Repository(推荐)

Repository就像EntityManager一样,但其操作仅限于具体实体。

你可以通过getRepository(Entity)Connection#getRepositoryEntityManager#getRepository访问存储库。

例如:

typescript
import { getRepository } from "typeorm";
import { User } from "./entity/User";

const userRepository = getRepository(User); // 你也可以通过getConnection().getRepository()或getManager().getRepository() 获取
const user = await userRepository.findOne(1);
user.name = "Umed";
await userRepository.save(user);

有三种类型的存储库:

  • Repository - 任何实体的常规存储库。
  • TreeRepository - 用于树实体的Repository的扩展存储库(比如标有@ Tree装饰器的实体)。有特殊的方法来处理树结构。
  • MongoRepository - 具有特殊功能的存储库,仅用于 MongoDB。

3.Find 查询

所有存储库和管理器find方法都接受可用于查询所需数据的特殊选项,而无需使用QueryBuilder

  • select - 表示必须选择对象的哪些属性
typescript
userRepository.find({ select: ["firstName", "lastName"] });
  • where -查询实体的简单条件。
typescript
userRepository.find({ where: { firstName: "Timber", lastName: "Saw" } });

查询嵌入实体列应该根据定义它的层次结构来完成。 例:

typescript
userRepository.find({ where: { name: { first: "Timber", last: "Saw" } } });

使用 OR 运算符查询:

typescript
userRepository.find({
  where: [
    { firstName: "Timber", lastName: "Saw" },
    { firstName: "Stan", lastName: "Lee" },
  ],
});

将执行以下查询:

sql
SELECT * FROM "user" WHERE ("firstName" = 'Timber' AND "lastName" = 'Saw') OR ("firstName" = 'Stan' AND "lastName" = 'Lee')
  • order - 选择排序
typescript
userRepository.find({
  order: {
    name: "ASC",
    id: "DESC",
  },
});

返回多个实体的find方法(findfindAndCountfindByIds),同时也接受以下选项:

  • skip - 偏移(分页)
typescript
userRepository.find({
  skip: 5,
});
  • take - limit (分页) - 得到的最大实体数。
typescript
userRepository.find({
  take: 10,
});

注意:进阶选项

TypeORM 提供了许多内置运算符,可用于创建更复杂的查询

例子:

  • Not
ts
import { Not } from "typeorm";

const loadedPosts = await connection.getRepository(Post).find({
  title: Not("About #1"),
});

将执行以下查询:

sql
SELECT * FROM "post" WHERE "title" != 'About #1'

4.关联查询

注意:这里默认是用 id 字段进行 left join 关联的!

  • relations - 关系需要加载主体。 也可以加载子关系(joinleftJoinAndSelect 的简写)

示例一

typescript
userRepository.find({ relations: ["profile", "photos", "videos"] });

这行代码大致等价于以下 SQL 查询语句:

sql
SELECT *
FROM userRepository
LEFT JOIN profile ON userRepository.profileId = profile.id
LEFT JOIN photos ON userRepository.photoId = photos.id
LEFT JOIN videos ON userRepository.videoId = videos.id;

这条 SQL 查询使用了 LEFT JOIN 来将userRepository表与profilephotosvideos表进行连接,并根据相关的外键关系检索所有匹配的记录。

示例二:关联中的关联

videos.video_attributes 的意思就是 videos 与 video_attributes 两个表左连接

js
userRepository.find({
  relations: ["profile", "photos", "videos", "videos.video_attributes"],
});

这行代码则等价于以下 SQL 查询语句:

sql
SELECT *
FROM userRepository
LEFT JOIN profile ON userRepository.profileId = profile.id
LEFT JOIN photos ON userRepository.photoId = photos.id
LEFT JOIN videos ON userRepository.videoId = videos.id
LEFT JOIN video_attributes ON videos.video_attributesId = video_attributes.id;

这条 SQL 查询在第一条基础上,再额外进行了一个 LEFT JOIN,将videos表与video_attributes表进行连接,以获取视频的子属性数据。

  • join - 需要为实体执行联接,扩展版对的"relations"。(就是完整版,relations 就是简写版)
typescript
userRepository.find({
  join: {
    alias: "user",
    leftJoinAndSelect: {
      profile: "user.profile",
      photo: "user.photos",
      video: "user.videos",
    },
  },
});

5.Left Join 的含义

LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。

sql
SELECT column_name(s)
FROM table1
LEFT JOIN table2
ON table1.column_name=table2.column_name;

或:

sql
SELECT column_name(s)
FROM table1
LEFT OUTER JOIN table2
ON table1.column_name=table2.column_name;

**注释:**在某些数据库中,LEFT JOIN 称为 LEFT OUTER JOIN。

image-20240312165614019

6.自定义存储库

扩展了标准存储库的定制存储库

创建自定义 repository 的第一种方法是扩展Repository。 例如:

typescript
import { EntityRepository, Repository } from "typeorm";
import { User } from "../entity/User";

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  findByName(firstName: string, lastName: string) {
    return this.findOne({ firstName, lastName });
  }
}

然后你可以这样使用它:

typescript
import { getCustomRepository } from "typeorm";
import { UserRepository } from "./repository/UserRepository";

const userRepository = getCustomRepository(UserRepository); // 或connection.getCustomRepository或manager.getCustomRepository()
const user = userRepository.create(); // 和 const user = new User();一样
user.firstName = "Timber";
user.lastName = "Saw";
await userRepository.save(user);

const timber = await userRepository.findByName("Timber", "Saw");

如你所见,你也可以使用getCustomRepository 获取 repository, 并且可以访问在其中创建的任何方法以及标准实体 repository 中的任何方法。

还有两种,不重要,这里不进行学习!

7.Repository API

注意:Entity Manager API 这里我们先不学习,主要使用 Repository

  • manager - 存储库使用的EntityManager
typescript
const manager = repository.manager;
  • createQueryBuilder - 创建用于构建 SQL 查询的查询构建器。 更多关于QueryBuilder.——> 重点
typescript
const users = await repository
  .createQueryBuilder("user")
  .where("user.name = :name", { name: "John" })
  .getMany();
  • hasId - 检查是否定义了给定实体的主列属性。
typescript
if (repository.hasId(user)) {
  // ... do something
}
  • getId - 获取给定实体的主列属性值。复合主键返回的值将是一个具有主列名称和值的对象。
typescript
const userId = repository.getId(user); // userId === 1
  • create - 创建User的新实例。接受具有用户属性的对象文字,该用户属性将写入新创建的用户对象(可选)。——> 重点
typescript
const user = repository.create(); // 和 const user = new User();一样
const user = repository.create({
  id: 1,
  firstName: "Timber",
  lastName: "Saw",
}); // 和const user = new User(); user.firstName = "Timber"; user.lastName = "Saw";一样
  • merge - 将多个实体合并为一个实体。
typescript
const user = new User();
repository.merge(user, { firstName: "Timber" }, { lastName: "Saw" }); // 和 user.firstName = "Timber"; user.lastName = "Saw";一样
  • preload - 从给定的普通 javascript 对象创建一个新实体。如果实体已存在于数据库中,则它将加载它(以及与之相关的所有内容),并将所有值替换为给定对象中的新值,并返回新实体。 新实体实际上是从数据库加载的所有属性都替换为新对象的实体。

    就是相当于准备对数据库对象进行修改、新增!先构造一个具有新的属性的对象(但是是通过查询数据库来进行构造的)!

typescript
const partialUser = {
  id: 1,
  firstName: "Rizzrak",
  profile: {
    id: 1,
  },
};
const user = await repository.preload(partialUser);
// user将包含partialUser中具有partialUser属性值的所有缺失数据:
// { id: 1, firstName: "Rizzrak", lastName: "Saw", profile: { id: 1, ... } }
  • save - 保存给定实体或实体数组。——> 重点

    如果该实体已存在于数据库中,则会更新该实体。

    如果数据库中不存在该实体,则会插入该实体。

    它将所有给定实体保存在单个事务中(在实体的情况下,管理器不是事务性的)。因为跳过了所有未定义的属性,还支持部分更新。

typescript
await repository.save(user);
await repository.save([category1, category2, category3]);
  • remove - 删除给定的实体或实体数组。——> 重点

    它将删除单个事务中的所有给定实体(在实体的情况下,管理器不是事务性的)。

typescript
await repository.remove(user);
await repository.remove([category1, category2, category3]);

怎么理解?

  1. 单个实体情况下的事务性
    • 当你调用 remove 方法删除仅涉及单个实体的情况时,这个删除操作并不是在事务中执行的。这意味着它不会自动封装在事务中,也就是说,它不会自动启动一个事务来执行删除操作,并且删除操作不会被回滚。
    • 换句话说,如果你删除了一个单个实体,这个操作是立即执行的,而且如果删除失败,它也不会被回滚。
  2. 多个实体情况下的事务性
    • 但是,当你调用 remove 方法并传递一个实体数组时,TypeORM 会在单个事务中执行这些删除操作。这意味着如果其中的任何一个删除操作失败,整个事务都会被回滚,从而确保数据的一致性。

这个概念的要点在于,在处理多个实体时,TypeORM 会自动启动一个事务来确保这些操作的原子性和一致性,而在处理单个实体时,你需要自己考虑是否要将其包装在事务中。

事务

事务(Transaction)是数据库管理系统(DBMS)中用于确保数据库操作的原子性、一致性、隔离性和持久性(ACID)的一种机制。

  • 原子性(Atomicity):指数据库操作要么全部执行成功,要么全部不执行,即操作要么完全完成,要么完全不执行,不存在部分执行的情况。
  • 一致性(Consistency):指数据库操作使数据库从一个一致性状态转移到另一个一致性状态,不会破坏数据库的完整性约束。
  • 隔离性(Isolation):指在并发执行的事务中,一个事务的操作不会受到其他事务的影响,每个事务看到的数据都是一致的。
  • 持久性(Durability):指一旦事务提交,对数据库的修改将被永久保存在数据库中,并且不会因为系统崩溃或其他错误而丢失。

在数据库中,事务通常涉及一系列数据库操作,如插入、更新、删除等。事务的开始和结束由特定的事务控制命令来标识,如在 SQL 中通常是 BEGIN TRANSACTION、COMMIT 和 ROLLBACK。

事务可以保证数据库操作的完整性和可靠性,确保数据库在处理多个并发操作时保持一致性,并且可以回滚到之前的状态以避免数据损坏。

  • insert - 插入新实体或实体数组。——> 重点
typescript
await repository.insert({
  firstName: "Timber",
  lastName: "Timber",
});

await manager.insert(User, [
  {
    firstName: "Foo",
    lastName: "Bar",
  },
  {
    firstName: "Rizz",
    lastName: "Rak",
  },
]);
  • update - 通过给定的更新选项或实体 ID 部分更新实体。——> 重点
typescript
await repository.update({ firstName: "Timber" }, { firstName: "Rizzrak" });
// 执行 UPDATE user SET firstName = Rizzrak WHERE firstName = Timber

await repository.update(1, { firstName: "Rizzrak" });
// 执行 UPDATE user SET firstName = Rizzrak WHERE id = 1
  • delete -根据实体 id, ids 或给定的条件删除实体:——> 重点
typescript
await repository.delete(1);
await repository.delete([1, 2, 3]);
await repository.delete({ firstName: "Timber" });
  • count - 符合指定条件的实体数量。对分页很有用。
typescript
const count = await repository.count({ firstName: "Timber" });
  • increment - 增加符合条件的实体某些列值。
typescript
await manager.increment(User, { firstName: "Timber" }, "age", 3);
  • decrement - 减少符合条件的实体某些列值。
typescript
await manager.decrement(User, { firstName: "Timber" }, "age", 3);
  • find - 查找指定条件的实体。——> 重点
typescript
const timbers = await repository.find({ firstName: "Timber" });
  • findAndCount - 查找指定条件的实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skiptake 选项)。
typescript
const [timbers, timbersCount] = await repository.findAndCount({
  firstName: "Timber",
});
  • findByIds - 按 ID 查找多个实体。
typescript
const users = await repository.findByIds([1, 2, 3]);
  • findOne - 查找匹配某些 ID 或查找选项的第一个实体。——> 重点
typescript
const user = await repository.findOne(1);
const timber = await repository.findOne({ firstName: "Timber" });
  • findOneOrFail - - findOneOrFail - 查找匹配某些 ID 或查找选项的第一个实体。 如果没有匹配,则 Rejects 一个 promise。
typescript
const user = await repository.findOneByOrFail({ id: 1 });
const timber = await repository.findOneOrFail({ firstName: "Timber" });
  • query - 执行原始 SQL 查询。——> 重点
typescript
const rawData = await repository.query(`SELECT * FROM USERS`);
  • clear - 清除给定表中的所有数据(truncates/drops)。
typescript
await repository.clear();

6.QueryBuilder

QueryBuilder(查询构造器)是 TypeORM 最强大的功能之一 ,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的实体。

有几种方法可以创建Query Builder

  • 使用 repository:

    typescript
    import { getRepository } from "typeorm";
    
    const user = await getRepository(User)
      .createQueryBuilder("user")
      .where("user.id = :id", { id: 1 })
      .getOne();

还有两种,不学

1.基本使用

有 5 种不同的QueryBuilder类型可用:

  • SelectQueryBuilder - 用于构建和执行SELECT查询。 例如:

    typescript
    import { getConnection } from "typeorm";
    
    const user = await getConnection()
      .createQueryBuilder()
      .select("user")
      .from(User, "user")
      .where("user.id = :id", { id: 1 })
      .getOne();
  • InsertQueryBuilder - 用于构建和执行INSERT查询。 例如:

    typescript
    import { getConnection } from "typeorm";
    
    await getConnection()
      .createQueryBuilder()
      .insert()
      .into(User)
      .values([
        { firstName: "Timber", lastName: "Saw" },
        { firstName: "Phantom", lastName: "Lancer" },
      ])
      .execute();
  • UpdateQueryBuilder - 用于构建和执行UPDATE查询。 例如:

    typescript
    import { getConnection } from "typeorm";
    
    await getConnection()
      .createQueryBuilder()
      .update(User)
      .set({ firstName: "Timber", lastName: "Saw" })
      .where("id = :id", { id: 1 })
      .execute();
  • DeleteQueryBuilder - 用于构建和执行DELETE查询。 例如:

    typescript
    import { getConnection } from "typeorm";
    
    await getConnection()
      .createQueryBuilder()
      .delete()
      .from(User)
      .where("id = :id", { id: 1 })
      .execute();
  • RelationQueryBuilder - 用于构建和执行特定于关系的操作[TBD]。

你可以在其中切换任何不同类型的查询构建器,一旦执行,则将获得一个新的查询构建器实例(与所有其他方法不同)。

7.MongoDB 支持

注意:理论上来说,存在一对多和多对多的关系,使用 MongoDB 会简单很多,因为不用联表查询(或者 for 循环查询),但是实际上,这是一种投机取巧的方法,不提倡,因为大多数的数据还是结构化数据,需要用结构化数据库 mysql!

但是 mysql 处理一对多和多对一是会相对比较复杂的!

后面再学

什么时候需要使用 MongoDB,什么时候适合?

  • 游戏应用:使用 MongoDB 作为游戏服务器的数据库存储用户信息。用户的游戏装备、积分等直接以内嵌文档的形式存储,方便进行查询与更新。
  • 物流应用:使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来,方便快捷且一目了然。
  • 社交应用:使用 MongoDB 存储用户信息以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。并且 MongoDB 非常适合用来存储聊天记录,因为它提供了非常丰富的查询,并在写入和读取方面都相对较快。
  • 视频直播:使用 MongoDB 存储用户信息、礼物信息等。
  • 大数据应用:使用 MongoDB 作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。

8.坑点罗列

1.实体的强替换,莫名删表

以我们上面设置的实体为例:

less
export class Goods {
  @primarygeneratedcolumn ()
    id: number;

  @column ()
    name: string;
}

我们初始化的表里面name字段对应的类型是varchar(45), 但是name: string;这种方式初始化的类型是varchar(255), 此时类型是不一致的, typeorm选择清空我们的name列, 是的你没听错name列被清空了:

image-20240312002038124

并且是只要你运行nest项目的时候就同步热更新了, 完全无感, 甚至你都不知道被清空了, 如果此时是线上环境请准备点干粮'跑路'吧。

不光是string类型, 其他任何类型只要对不上就全给你删了, 毫无提示。

解决方案

原因:类型和数据库列类型不匹配,typeorm 认为不是一样的列,所以 typeorm 认为数据库中不应该有那个原来的列,所以删除了那一列

而且**@Column()什么都不写的话,默认是@Column( {type: “varchar”, length:256} ),即便是数据库中的 length 不一样,也会被认为是不一样的类型,会造成删列!**

1.设置数据库连接时

synchronize: false, //不同步实体到数据库

这样就不会删列了

2.当 length 不一样时,手动设置类型和长度,确保和数据库一致!——> 一定要注意

@Column({ type: "varchar", length: 40 })

特别注意:即使用了方案二,还是建议强烈关闭 synchronize,免得出现意外!!!

2.没有集成现有数据库的方案

我们很多时候数据库都是已有数据的, 全新的空白数据库空白表的情况并不是主流, 在typeorm官网也并没有找到很好的接入数据库的方案, 全部都是冒着删库的危险在定义类型, 更有甚者你改到一半不小心自动保存了, 那么你的表就空了...

我们不可能每次都是用空白数据库开发, 这点真难得很难人忍受。

3.entities 的三种设置方式

第一种: 单独定义/share/src/app.module.ts配置链接数据库时:

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

你用到哪些实体, 就逐一在此处引入, 缺点就是我们每写一个实体就要引入一次否则使用实体时会报错。

第二种: 自动加载我们的实体,每个通过forFeature()注册的实体都会自动添加到配置对象的 entities 数组中, forFeature()就是在某个service中的imports里面引入的, 这个是比较推荐的:

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

第三种: 自定义引入路径, 这个居然是官方推荐...

ts
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.多人开发, 极其混乱

这个多人开发简直是噩梦, 互相删表的情况逐渐出现, 一个实际的例子比如a同事优化所有实体的配置比如统一把varchar(255)改成varchar(45), 所有的相关数据都会被清空, 于此同时你发现了问题, 并把数据补充回来了, 但此时b同事的电脑里还是varchar(255)版本, 一起开发时就会导致你不管怎么改数据, 表里的数据都会被反复清除干净...

我们团队当时解决方案是, 每个人都复制一份当前库单独进行开发, 几个人开发就要有几个不同的库, 我们的mysql里全是已自己姓名命名的库。

每次 git 拉取代码都要修改库名, 否则会把其他人的库清空;

6.多版本开发

比如张三使用的是zhangsan_xxx库, 但是他同时开发几个版本, 这几个版本之前表的格式有差别, 那么张三要使用zhangsan_xxx_1_1, zhangsan_xxx_1_2这种命名格式来进行多个库的开发。

综上所述除非公司已经定了技术选型, 否则我不建议用 nest 开发...

9.Nest 结合最佳实践例子

1.entity 设置

jsx
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({
        type: "varchar",
        length: 150,
        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()以后每次更新数据都会自动的更新这个时间值。

2.find 方法, 简洁的查找命令

上面我们已经将goodsRepository注入到了GoodsService里面可以直接使用:

jsx
constructor(
    @InjectRepository(Goods)
    private goodsRepository: Repository<Goods>
) { }
1.无条件查询所有数据

this.goodsRepository.find()查询goods表的全部数据, 以及每条数据的信息。

2.只显示name, createDate两列数据:
stylus
this.goodsRepository.find({
   select: ['name', 'createDate']
})
3.搜索名字是'x2'并且 isActive 为'false'的数据
kotlin
this.goodsRepository.find({
 where: {
       name: 'x2',
       isActive: false
     }
 })
4.名字等于'x2'或者等于'x3'都会被匹配出来:
jsx
this.goodsRepository.find({
  where: [
    {
      name: "x2",
    },
    {
      name: "x3",
    },
  ],
});
5.排序, 以 name 降序, 创建时间升序排列
jsx
this.goodsRepository.find({
  order: {
    name: "DESC",
    createDate: "ASC",
  },
});
6.切割, skip跳过 1 条, take取出 3 条
jsx
this.goodsRepository.find({
  skip: 1,
  take: 3,
});
7.like模糊查询名字里带有 2 的项, not id 不是 1
jsx
this.goodsRepository.find({
  where: {
    id: Not(1),
    name: Like("%2%"),
  },
});
8.findAndCount 把满足条件的数据总数返回

数据是数组形式, [0]是匹配到的数组, [1]是符合条件的总数可能与[0]的长度不相同。

jsx
this.goodsRepository.findAndCount({
  select: ["name"],
});
7.findOne 只取配到的第一条, 并且返回形式为对象

只取配到的第一条, 并且返回形式为对象而非数组:

jsx
this.goodsRepository.findOne({
  select: ["name"],
});
10.findByIds, 传入 id 组成的数组进行匹配
jsx
this.goodsRepository.findByIds([1, 2]);
11.前端获取一个需要分页的列表

用户传入需要模糊匹配的name值, 以及当前第 n 页, 每页 s 条, 总数 total 条。

javascript
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
        }
    }

3.新增与修改的 dto

pgsql
yarn add class-validator class-transformer -S
1.新增

先建立一个简单的新增 dto 模型/share/src/modules/goods/dto/create-goods.dto.ts:

jsx
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

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

老样子, 先建立一份更新的 dto, 比如 name 是不可以更新的就不写 name, /share/src/modules/goods/dto/updata-goods.dto.ts:

jsx
import { MaxLength } from "class-validator";

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

在控制器里面就要限制用户传入的更新数据类型必须与 dto 相同/share/src/modules/goods/goods.controller.ts:

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

先找到对应的数据, 再进行数据的更新/share/src/modules/goods/goods.service.ts

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