搭建完前端与后端的基础结构后,我们就可以开始着手于数据库设计了。

1. 设计基础实体

在设计我们的购物平台的过程中,实体及其关系是核心的数据模型。我们从业务逻辑的角度来看,每个实体的意义和作用,这样可以更好地理解其在系统中的角色。

这里,我们以“用户”、“商品”、“订单”等为主线,讲解如何建立这些实体以及它们之间的关系,并举例说明其在业务场景中的应用。

1.1. 用户

用户实体是系统的核心,因为大部分操作都需要绑定到用户。每个用户都有其唯一的 ID、用户名、邮箱和密码,这些信息用于身份验证和授权。

示例,一个用户 John Doe 创建了账号,并通过邮箱 johndoe@example.com 登录系统。数据库会在 Users 表中新增一条记录,保存 John 的邮箱、加密密码和创建时间。

1.1.1. 代码实现分析

让我们深入分析 Users 实体的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
OneToOne
} from 'typeorm';

@Entity()
export class Users {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
username: string;

@Column({ unique: true })
email: string;

@Column()
password: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}

这部分使用了 TypeORM 的装饰器来定义表结构:

  • @Entity() 装饰器将这个类标记为一个数据库实体
  • @PrimaryGeneratedColumn('uuid') 表示自动生成 UUID 作为主键
  • @Column({ unique: true }) 为用户名和邮箱添加了唯一性约束,防止重复注册
  • @CreateDateColumn() 会自动记录实体的创建时间
  • @UpdateDateColumn() 会自动记录实体的更新时间

用户表的字段及其业务含义如下:

  • id:主键,用于唯一标识每个用户,类型为 UUID,确保安全性和唯一性
  • username:用户名,系统中用于显示的名称,通常在用户之间是唯一的
  • email:用户的邮箱,通常作为主要联系手段,同时也是登录的标识之一
  • password:用户密码,以加密方式存储,用于用户验证
  • created_atupdated_at:创建时间和更新时间,记录用户注册和资料更新的时间

1.1.2. 关联关系分析

Users 实体与其他实体之间建立了多个关联关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Orders } from './orders.entity';
import { Carts } from './carts.entity';
import { Addresses } from './addresses.entity';
import { Payments } from './payments.entity';

@Entity()
export class Users {
// ...

@OneToMany(() => Orders, order => order.user)
orders: Orders[];

@OneToOne(() => Carts, cart => cart.user)
cart: Carts;

@OneToMany(() => Addresses, address => address.user)
addresses: Addresses[];

@OneToMany(() => Payments, payment => payment.user)
payments: Payments[];
}

这些关联展示了用户实体在系统中的核心地位:

  1. 用户-订单关系(@OneToMany
    • 一个用户可以有多个订单
    • 这种一对多的关系允许系统追踪用户的所有购买历史
  2. 用户-购物车关系(@OneToOne
    • 每个用户只能有一个购物车
    • 一对一的关系确保购物车数据的独立性和安全性
  3. 用户-地址关系(@OneToMany
    • 用户可以保存多个收货地址
    • 方便用户在下单时快速选择收货地址
  4. 用户-支付关系(@OneToMany
    • 记录用户的所有支付记录
    • 用于追踪交易历史和财务统计

1.1.3. 用户角色

在开发应用程序时,管理用户的权限和访问控制是至关重要的。这是因为不同类型的用户可能需要访问系统的不同功能。

例如,一个购物平台可能有以下几种角色:

  • 管理员:可以管理用户、查看所有订单、编辑商品等
  • 普通用户:只能查看商品、下订单、查看个人资料等
  • 游客:仅限浏览,不进行任何交互

在这种场景下,我们需要为用户管理系统添加一个角色字段,以便区分不同的用户类型。

角色字段通常有助于完成以下任务:

  • 不同权限管理:每种角色都有不同的权限。管理员可能有权访问系统的所有数据,而普通用户只能查看他们自己的数据。通过角色字段,我们可以在数据库中存储每个用户的角色信息
  • 角色控制的用户界面:角色字段可以帮助系统为不同角色的用户显示不同的页面或功能。例如,管理员可以访问管理控制台,而普通用户只能看到他们的订单历史
  • 安全性增强:通过角色字段,系统可以在后端进行权限验证,确保不同角色的用户只能执行允许他们执行的操作,避免了恶意操作或权限滥用

为了明确区分不同角色的值,通常我们会使用枚举(enum)来为角色提供固定的值。

enum 是一种特殊的类,用来表示一组固定的常量。

通过将 role 字段设置为枚举类型,我们可以确保角色值仅限于我们定义的固定值,这些值会自动被 TypeORM 映射到数据库字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 添加角色的枚举类型
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}

@Entity()
export class Users {
// ...

// 在 Users 实体中添加 role 字段
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.USER, // 默认值为普通用户
})
role: UserRole;
}

1.2. 商品

商品实体是平台中与交易直接相关的核心实体,它承载了商品的基本信息、库存管理、定价等重要功能。一个设计良好的商品实体不仅要满足基本的展示需求,还要支持库存管理、订单处理等复杂业务场景。

商品表的作用是提供系统中可供购买的商品清单,记录商品价格和库存。在库存管理方面,当库存减少到零时,商品需要标记为“缺货”或“下架”。

示例,商家上架了一款新商品“智能手表”,库存数量为 100,售价为 200 元,商品类别为“电子产品”。这款商品的信息会存储在 Products 表中,供用户选择和购买。

1.2.1. 代码实现分析

让我们详细分析 Products 实体的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany
} from 'typeorm';

@Entity()
export class Products {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column('text')
description: string;

@Column('decimal')
price: number;

@Column()
stock: number;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}

代码实现的特点:

  • 使用 text 类型存储描述,支持长文本内容
  • 使用 decimal 类型存储价格,避免浮点数计算误差
  • stock 字段直接使用普通的数值类型,便于进行库存相关的计算

商品表的字段及其业务含义如下:

  • id:主键,用于唯一标识每个商品,类型为 UUID
  • name:商品名称,帮助用户识别商品
  • description:商品描述,用于展示商品的详细信息
  • price:商品价格,定义用户在购买该商品时的成本
  • stock:商品库存数量,代表当前商品的剩余数量
  • created_atupdated_at:创建时间和更新时间,记录商品上架和更新的时间

1.2.2. 关联关系分析

Products 实体与其他实体建立了多个关联关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Categories } from './categories.entity';
import { OrderItems } from './order-items.entity';
import { CartItems } from './cart-items.entity';
import { InventoryLogs } from './inventory-logs.entity';

@Entity()
export class Products {
// ...

@ManyToOne(() => Categories, category => category.products)
category: Categories;

@OneToMany(() => OrderItems, orderItem => orderItem.product)
orderItems: OrderItems[];

@OneToMany(() => CartItems, cartItem => cartItem.product)
cartItems: CartItems[];

@OneToMany(() => InventoryLogs, inventoryLog => inventoryLog.product)
inventoryLogs: InventoryLogs[];
}
  1. 商品-类别关系 (@ManyToOne)
    • 一个商品属于一个类别
    • 支持商品分类管理和分类检索
    • 多对一的关系使得类别可以包含多个商品
  2. 商品-订单项关系 (@OneToMany)
    • 一个商品可以出现在多个订单中
    • 通过 OrderItems 中间表存储具体的购买数量和价格
    • 支持订单历史查询和销售统计
  3. 商品-购物车项关系 (@OneToMany)
    • 一个商品可以被加入多个用户的购物车
    • 通过 CartItems 中间表记录购物车中的商品数量
  4. 商品-库存日志关系 (@OneToMany)
    • 记录商品库存变动历史
    • 支持库存追踪和审计

1.3. 类别

类别实体是商品分类的基础,它帮助我们组织和管理商品,提供更好的浏览和检索体验。一个良好的类别系统能够帮助用户更快地找到所需商品,同时也便于商家进行商品管理。

当平台增加新类别“智能家居”时,它会被添加到 Categories 表中。用户在浏览智能家居产品时,只需点击此类别,即可看到所有相关商品。

1.3.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';

@Entity()
export class Categories {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column('text')
description: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}

类别表的字段及其业务含义如下:

  • id:主键,唯一标识类别,类型为 UUID
  • name:类别名称,例如“电子产品”或“家具”
  • description:类别描述,进一步解释类别内容
  • created_atupdated_at:创建和更新时间,记录类别的管理信息

1.3.2. 关联关系分析

Categories 实体主要与 Products 实体建立了一对多的关联关系:

1
2
3
4
5
6
7
8
9
import { Products } from './products.entity';

@Entity()
export class Categories {
// ...

@OneToMany(() => Products, product => product.category)
products: Products[];
}
  1. 类别-商品关系 (@OneToMany)
    • 一个类别可以包含多个商品
    • 体现了分类组织的层次结构
    • 支持按类别查询和统计商品

1.4. 订单

订单是电商系统中最核心的业务实体之一,它记录了交易的全过程,连接了用户、商品、支付等多个业务环节。一个完善的订单系统需要处理订单状态流转、支付流程、商品库存等复杂的业务场景。

当用户下单购买一部智能手表,总价为 200 元,订单状态为“待支付”。在用户支付成功后,订单状态更新为“已支付”。

1.4.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany
} from 'typeorm';

@Entity()
export class Orders {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('decimal')
total_price: number;

@Column()
status: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}
  • id:订单的唯一标识符,使用 UUID 类型
  • total_price:订单总价,使用 decimal 类型确保精确计算
  • status:订单状态,用于追踪订单的处理流程
  • created_atupdated_at:记录订单的创建和更新时间

1.4.2. 关联关系分析

Orders 实体与其他实体建立了多个关联关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Users } from './users.entity';
import { OrderItems } from './order-items.entity';

@Entity()
export class Orders {
// ...

@ManyToOne(() => Users, user => user.orders)
user: Users;

@OneToMany(() => OrderItems, orderItem => orderItem.order)
orderItems: OrderItems[];
}

这些关联关系支持了完整的订单业务流程:

  1. 订单-用户关系 (@ManyToOne)
    • 每个订单必须属于一个用户
    • 支持用户订单历史查询
    • 便于进行用户消费分析
  2. 订单-订单项关系 (@OneToMany)
    • 一个订单可以包含多个商品
    • 通过 OrderItems 存储具体的商品信息和数量
    • 支持订单明细查询和统计

1.5. 订单项

订单项实体记录了订单中每个商品的具体购买信息,包括数量、单价和总价等。它不仅连接了订单和商品,还保存了购买时的价格快照,这对于订单历史记录和财务核算都很重要。

1.5.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';

@Entity()
export class OrderItems {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
quantity: number;

@Column('decimal')
price: number;

@Column('decimal')
total_price: number;

// ...
}
  • id:订单项的唯一标识符,使用 UUID 类型
  • quantity:购买数量
  • price:商品单价的快照,使用 decimal 类型
  • total_price:该项商品的总价,使用 decimal 类型

1.5.2. 关联关系分析

OrderItems 实体与其他实体建立了多个多对一的关联关系:

  1. 订单项-订单关系 (@ManyToOne)

    1
    2
    @ManyToOne(() => Orders, order => order.orderItems)
    order: Orders;
    • 多个订单项属于同一个订单
    • 通过这个关系可以获取订单的完整商品清单
    • 支持订单总价计算和商品统计
  2. 订单项-商品关系(@ManyToOne

    1
    2
    @ManyToOne(() => Products, product => product.orderItems)
    product: Products;
    • 记录该订单项对应的具体商品
    • 支持商品销售统计和分析
    • 可以追踪商品的购买历史

1.6. 购物车

每个用户都有一个购物车,用于暂存待购买的商品信息。

购物车实体记录了用户在线上选择和加入的商品,是下单前的临时存储。它与订单实体有着类似的结构,但服务于不同的业务场景。

1.6.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne, OneToMany } from 'typeorm';
import { Users } from './users.entity';
import { CartItems } from './cart-items.entity';

@Entity()
export class Carts {
@PrimaryGeneratedColumn('uuid')
id: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}

代码实现的特点:

  • 使用一对一关系与用户实体关联,确保每个用户只有一个购物车
  • 包含基本的时间戳字段,跟踪购物车的创建和更新

购物车表的字段及其业务含义如下:

  • id: 购物车的唯一标识符,使用 UUID 类型
  • created_atupdated_at: 记录购物车的创建和更新时间

1.6.2. 关联关系分析

Carts 实体与其他实体建立了以下关联关系:

  1. 购物车-用户关系(@OneToOne

    1
    2
    @OneToOne(() => Users, user => user.cart)
    user: Users;
  2. 购物车-购物车项关系(@OneToMany

    1
    2
    @OneToMany(() => CartItems, cartItem => cartItem.cart)
    cartItems: CartItems[];
    • 一个购物车可以包含多个购物车项
    • 通过这个关系可以获取购物车中的商品列表

1.7. 购物车项

购物车项实体记录了用户在购物车中选择的商品及其数量,它与订单项实体有着类似的结构。

用户在购物车中添加了两部智能手表,购物车项记录该商品 ID、数量为 2,以及关联的购物车 ID。

1.7.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';

@Entity()
export class CartItems {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
quantity: number;

// ...
}

CartItems 实体的特点:

  • 使用多对一关系连接购物车和商品实体
  • 记录了商品的购买数量,用于计算购物车总价

1.7.2. 关联关系分析

  1. 购物车项-购物车关系(@ManyToOne

    1
    2
    @ManyToOne(() => Carts, cart => cart.cartItems)
    cart: Carts;
  2. 购物车项-商品关系(@ManyToOne

    1
    2
    @ManyToOne(() => Products, product => product.cartItems)
    product: Products;

1.8. 支付

支付实体记录了用户在完成下单后的支付信息,包括支付金额、支付状态等。它与订单实体有着直接的关联关系。

1.8.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne } from 'typeorm';

@Entity()
export class Payments {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('decimal')
amount: number;

@Column()
status: string;

@CreateDateColumn()
created_at: Date;

// ...
}

Payments 实体的特点:

  • 使用多对一关系连接订单和用户实体
  • 记录了支付的金额和状态
  • 包含支付的创建时间戳

1.8.2. 关联关系分析

  1. 支付-订单关系(@ManyToOne

    1
    2
    @ManyToOne(() => Orders, order => order.id)
    order: Orders;
    • 一笔订单可以有多个支付记录
    • 通过这个关系可以获取支付所属的订单信息
  2. 支付-用户关系(@ManyToOne

    1
    2
    @ManyToOne(() => Users, user => user.payments)
    user: Users;
    • 一个用户可以有多笔支付记录
    • 通过这个关系可以获取支付所属的用户信息

1.9. 地址

地址实体记录了用户的收货地址信息,包括街道、城市、州/省、邮编和国家等详细信息。这些信息在用户下单时需要用到,也可以用于生成发货标签等。

1.9.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';

@Entity()
export class Addresses {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
street: string;

@Column()
city: string;

@Column()
state: string;

@Column()
postal_code: string;

@Column()
country: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

// ...
}

1.9.2. 关联关系分析

  1. 地址-用户关系(@ManyToOne

    1
    2
    @ManyToOne(() => Users, user => user.addresses)
    user: Users;
    • 一个用户可以有多个地址信息
    • 通过这个关系可以获取地址所属的用户信息

1.10. 库存日志

库存日志实体记录了商品库存的变动历史,包括每次库存增减的数量和时间。这些记录对于库存管理、库存盘点和异常排查都非常重要。

1.10.1. 代码实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne } from 'typeorm';

@Entity()
export class InventoryLogs {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
change: number;

@CreateDateColumn()
created_at: Date;

// ...
}

InventoryLogs 实体的特点:

  • 使用多对一关系与商品实体关联
  • 记录了库存变动数量(change),正数表示入库,负数表示出库
  • 包含时间戳,记录每次库存变动的具体时间点

1.10.2. 关联关系分析

  1. 库存日志-商品关系(@ManyToOne

    1
    2
    @ManyToOne(() => Products, product => product.inventoryLogs)
    product: Products;
    • 一个商品可以有多条库存变动记录
    • 通过这个关系可以追踪特定商品的库存变动历史

2. 创建数据库迁移文件

在开发应用程序时,数据库模式的更新是一项常见而重要的工作,尤其是在应用不断演进的过程中。

假设我们在开发一个应用,并需要更新数据库的表结构,比如添加新字段、修改字段类型等。如何在不丢失现有数据的情况下进行这些修改呢?

TypeORM 可以帮助我们轻松处理数据库操作。它内置了强大的迁移功能,使我们能够定义数据库结构变更并自动应用到数据库中。

2.1. 添加自定义命令

首先,我们在 package.json 中添加一些脚本来管理 TypeORM 的迁移操作。这些脚本将自动构建项目并运行相应的迁移命令,方便我们快速执行迁移操作:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
// ...
"typeorm": "typeorm-ts-node-commonjs -d src/config/data-source.ts",
"migration:initialize": "yarn build && yarn typeorm migration:generate src/migrations/InitialMigration",
"migration:generate": "yarn build && yarn typeorm migration:generate",
"migration:run": "yarn build && yarn typeorm migration:run",
"migration:revert": "yarn build && yarn typeorm migration:revert"
}
}

让我们一行一行地分析这些脚本的作用:

  • typeorm:指定 TypeORM 的配置文件路径(src/config/data-source.ts),它是所有 TypeORM 操作的基础
  • migration:initialize:生成 InitialMigration 迁移文件
  • migration:generate:生成新的迁移文件
    • 用法:yarn migration:generate src/migrations/xxx
  • migration:run:执行所有还未运行的迁移,应用到数据库中
  • migration:revert:撤销上一次迁移,通常用于回滚数据库结构到上一个状态

2.2. 配置 TypeORM 数据源

接下来,我们需要设置 TypeORM 的数据库配置。

在项目的 src/config/data-source.ts 文件中,我们使用 DataSourceOptions 来定义数据库连接的详细信息。

这里我们还使用了 @nestjs/config 模块来动态读取环境变量,这样在不同环境中可以使用不同的数据库配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ConfigService } from '@nestjs/config';
import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv';

config();

const configService = new ConfigService();

const dataSourceOptions: DataSourceOptions = {
type: configService.get<any>('DB_TYPE', 'mysql'),
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
synchronize: configService.get<string>('APP_ENV') === 'development',
entities: ['dist/entities/*.entity{.ts,.js}'],
migrations: ['dist/migrations/*{.ts,.js}'],
subscribers: []
};

export default new DataSource(dataSourceOptions);
  • type:指定数据库类型,这里我们通过环境变量 DB_TYPE 来设置,默认为 MySQL
  • hostportusernamepassworddatabase:这些参数是数据库连接的必要信息,都可以通过环境变量配置
  • synchronize:如果 APP_ENVdevelopment,则会启用同步模式,让 TypeORM 自动更新数据库表结构
    • 注意:生产环境下建议禁用此项
  • entitiesmigrations:指定实体和迁移文件的路径。TypeORM 会使用这些路径找到相关文件

2.3. 生成数据库迁移文件

当配置完成后,我们可以通过以下命令来生成用于初始化数据库的迁移文件:

1
yarn migration:initialize

这条命令会自动构建项目,并使用 TypeORM 生成一个迁移文件(位于 src/migrations,且默认命名为 数字数字数字-InitialMigration.ts)。

这个文件会包含创建、修改或删除表结构的 SQL 语句。例如,如果你添加了一个新的字段,生成的迁移文件就会包含一个 ALTER TABLE 语句。

2.4. 执行迁移

生成迁移文件后,使用以下命令将其应用到数据库:

1
yarn migration:run

此命令会检查并执行所有尚未在数据库中应用的迁移。这样可以确保你的数据库结构与最新的迁移文件一致。

2.5. 回滚迁移

在开发过程中,偶尔可能需要回滚上次迁移,例如在测试或调试时。使用以下命令可以撤销上一次迁移:

1
yarn migration:revert

TypeORM 会自动识别上次执行的迁移,并撤销相应的更改,恢复到之前的数据库结构。

3. 设置数据库索引

在开发应用程序时,尤其是当数据库中的数据量越来越大时,查询性能变得至关重要。

数据库索引是一种数据结构,它帮助数据库管理系统(DBMS)高效地检索数据。索引就像书籍中的目录,它可以快速定位到某个数据的位置,而不是扫描整个表。当我们对数据库表进行查询时,索引可以显著提高查询性能,尤其是在处理大量数据时。

在 MySQL 中,索引通常应用于那些需要频繁查询的字段。常见的索引类型有:

  • 主键索引:自动为表的主键字段创建
  • 唯一索引:确保每个值唯一
  • 普通索引:加速数据检索,但不强制唯一性
  • 全文索引:用于加速文本内容的检索

没有索引的情况下,数据库在执行查询时必须扫描整个表,逐行比较数据。这种方式在小型表中可能没什么问题,但在数据量较大时,会导致性能急剧下降。

假设我们的购物平台,数据库中存储了数百万条订单记录。当用户搜索特定订单时,如果没有适当的索引,系统可能会扫描整个订单表来找到匹配的记录。随着数据量增加,查询速度变得非常慢,甚至可能导致系统响应延迟。

使用索引后,数据库不再需要扫描整个表,而是通过索引快速定位到目标数据,从而显著提升查询速度。

在 NestJS 中,我们可以通过 TypeORM 为实体字段添加索引。TypeORM 提供了 @Index() 装饰器,允许我们在数据库表的字段上添加索引。

3.1. 用户

1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

Users 实体中,我们使用了 @Index() 装饰器来为 created_at 字段添加索引。

在大多数应用程序中,created_at 字段通常用于按照时间排序或进行时间范围查询。

例如,用户可能需要查看某一时间段内的所有注册用户,或者获取最近创建的用户列表。

假设在应用程序中,用户经常执行以下查询:

  • 查询某个时间段内注册的用户。
  • 按照注册时间排序显示用户列表。

如果没有为 created_at 字段添加索引,数据库会需要扫描整个用户表来执行这些查询,这样会导致性能问题。添加索引后,数据库可以通过索引快速查找和排序数据,显著提高查询速度,尤其是在数据量较大的情况下。

3.1.1. 关系字段为何不需要索引?

这时候就有人问了:“难道像 orderscart 这些字段,用户就不会查询了吗?如果经常用到,为什么不为它们加个索引呢?”答案是:确实不需要。

在 TypeORM 中,关系字段(比如 orderscart)通常是外键字段,数据库会自动为这些字段创建索引。例如,在 Users 实体中,orders 字段是通过 @OneToManyOrders 实体关联的。虽然我们没有显式地为 orders 字段添加索引,但在 Orders 表中,user 字段作为外键会自动被索引。

这意味着,当我们查询某个用户的所有订单时,数据库会直接利用 Orders 表中自动创建的 user 索引,来加速查询。而我们不需要额外为 orders 字段添加索引,TypeORM 和数据库已经为我们处理好了这部分的优化。

3.2. 商品

1
2
3
@Index()
@Column('decimal')
price: number;

price 字段通常会用于以下几种查询:

  • 按照价格范围查询产品,例如查找价格低于某个值的所有产品
  • 按照价格排序,例如将产品列表按价格从低到高或从高到低排序
1
2
3
@Index()
@Column()
stock: number;

stock 字段通常用于以下查询场景:

  • 查找库存数量为零或低于某个值的产品(例如“缺货”产品或“库存预警”产品)
  • 按照库存数量排序,帮助商家快速查看库存最少的产品
1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

所有的 created_at 字段都需要添加索引。

3.3. 支付

1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

3.4. 订单

1
2
3
@Index()
@Column()
status: string;

status 字段通常用于查询订单的状态,例如:

  • 查询某一状态的订单(如“待处理”、“已发货”、“已完成”等)
  • 按照订单状态统计订单数量
1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

3.5. 订单项

1
2
3
@Index()
@Column()
quantity: number;

quantity 字段通常在以下情况中用于查询:

  • 查询具有特定数量的订单项(例如查询某种商品的批量购买情况)
  • 按照数量对订单项进行筛选或排序

3.6. 类别

1
2
3
@Index()
@Column()
name: string;

name 字段是分类的唯一标识,用于以下情况:

  • 按类别名称搜索产品类别(例如,用户希望快速找到特定名称的类别)
  • 在名称字段上进行排序或进行自动完成的匹配查询
1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

3.7. 购物车

1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

3.8. 地址

1
2
3
@Index()
@Column()
postal_code: string;

在地址系统中,postal_code(邮政编码)字段可能用于以下场景:

  • 按邮政编码筛选地址。例如,在电商系统中,按邮政编码筛选用户地址,以确定是否支持配送
  • 邮政编码的批量统计或区域分析,例如确定特定邮政编码区域的用户密度
1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

3.9. 库存日志

1
2
3
@Index()
@CreateDateColumn()
created_at: Date;

接下来,我们可以使用以下命令生成数据库迁移文件:

1
yarn migration:generate src/migrations/CreateIndex

然后使用以下命令应用迁移并更新数据库:

1
yarn migration:run