乐闻世界logo
搜索文章和话题

TypeORM面试题手册

TypeORM 与 Prisma、Sequelize、MikroORM 等其他 ORM 框架的对比分析

选择合适的 ORM 框架对于项目成功至关重要。本文将详细对比 TypeORM 与其他主流 ORM 框架的异同,帮助开发者做出明智的选择。主流 ORM 框架对比1. TypeORM vs PrismaTypeORM 特点// TypeORM 使用装饰器和类@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[];}// 使用 Repository 模式const userRepository = dataSource.getRepository(User);const users = await userRepository.find({ relations: ['posts'] });优点:完全支持 TypeScript,类型安全支持 Active Record 和 Data Mapper 两种模式灵活的查询构建器强大的迁移系统支持多种数据库缺点:学习曲线较陡性能相对较低查询复杂度较高Prisma 特点// Prisma 使用 schema 文件// schema.prismamodel User { id Int @id @default(autoincrement()) name String posts Post[]}model Post { id Int @id @default(autoincrement()) title String author User @relation(fields: [authorId], references: [id]) authorId Int}// 使用生成的客户端const users = await prisma.user.findMany({ include: { posts: true }});优点:类型安全,自动生成类型查询语法简洁直观性能优秀强大的类型推断良好的开发体验缺点:不支持 Active Record 模式自定义查询能力有限迁移系统相对简单2. TypeORM vs SequelizeTypeORM 特点// TypeORM 装饰器方式@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string;}// 查询构建器const users = await dataSource .getRepository(User) .createQueryBuilder('user') .where('user.name LIKE :name', { name: '%John%' }) .getMany();优点:TypeScript 原生支持装饰器语法清晰强类型系统现代化设计缺点:相对较新,生态不如 Sequelize 成熟文档和社区资源较少Sequelize 特点// Sequelize 定义模型const User = sequelize.define('User', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING }, email: { type: DataTypes.STRING, unique: true }});// 查询const users = await User.findAll({ where: { name: { [Op.like]: '%John%' } }});优点:成熟稳定,生态丰富文档完善,社区活跃支持多种数据库功能全面缺点:TypeScript 支持需要额外配置类型安全性不如 TypeORMAPI 设计相对老旧3. TypeORM vs MikroORMTypeORM 特点// TypeORM 使用装饰器@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[];}// Data Mapper 模式const userRepository = dataSource.getRepository(User);const users = await userRepository.find({ relations: ['posts'] });优点:灵活的模式选择强大的查询能力良好的 TypeScript 支持缺点:性能不如 MikroORM单元测试(Unit of Work)实现不够完善MikroORM 特点// MikroORM 使用装饰器@Entity()export class User { @PrimaryKey() id!: number; @Property() name!: string; @OneToMany(() => Post, post => post.author) posts = new Collection<Post>(this);}// Unit of Work 模式const users = await em.find(User, {}, { populate: ['posts']});优点:性能优秀真正的 Unit of Work 模式身份映射(Identity Map)延迟加载(Lazy Loading)性能好更好的类型推断缺点:相对较新,生态较小学习曲线较陡文档相对较少4. TypeORM vs Mongoose (MongoDB)TypeORM (MongoDB)// TypeORM 支持 MongoDB@Entity()export class User { @ObjectIdColumn() id: string; @Column() name: string; @Column() email: string;}// 查询const users = await dataSource.getRepository(User).find();优点:统一的 API,支持多种数据库TypeScript 原生支持迁移系统缺点:MongoDB 支持不如 Mongoose 完善性能相对较低Mongoose// Mongoose Schemaconst userSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, unique: true, required: true }});const User = mongoose.model('User', userSchema);// 查询const users = await User.find();优点:MongoDB 生态最佳选择功能丰富,性能优秀中间件和插件系统强大文档完善缺点:TypeScript 支持需要额外配置不支持关系型数据库性能对比查询性能// 性能测试示例import { performance } from 'perf_hooks';async function benchmarkORMs() { const iterations = 1000; // TypeORM const typeormStart = performance.now(); for (let i = 0; i < iterations; i++) { await typeormRepository.find({ take: 10 }); } const typeormEnd = performance.now(); // Prisma const prismaStart = performance.now(); for (let i = 0; i < iterations; i++) { await prisma.user.findMany({ take: 10 }); } const prismaEnd = performance.now(); console.log(`TypeORM: ${typeormEnd - typeormStart}ms`); console.log(`Prisma: ${prismaEnd - prismaStart}ms`);}性能排名(从快到慢):MikroORMPrismaTypeORMSequelize内存使用// 内存使用监控function monitorMemoryUsage() { const used = process.memoryUsage(); console.log(`Memory Usage:`); console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`); console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`); console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);}内存使用排名(从低到高):PrismaMikroORMTypeORMSequelize功能对比表| 特性 | TypeORM | Prisma | Sequelize | MikroORM | Mongoose || ------------- | ------- | ------ | --------- | -------- | -------- || TypeScript 支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ || 性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ || 学习曲线 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ || 文档质量 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ || 社区活跃度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ || 迁移系统 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | N/A || 查询构建器 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ || 关系处理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | N/A || 数据库支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | MongoDB |选择建议选择 TypeORM 的场景需要 TypeScript 原生支持// 完全的类型安全@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string;}const user: User = await userRepository.findOne({ where: { id: 1 } });需要灵活的设计模式// Active Record 模式@Entity()export class User extends BaseEntity { static async findActive() { return this.find({ where: { isActive: true } }); }}// Data Mapper 模式const userRepository = dataSource.getRepository(User);const users = await userRepository.find({ where: { isActive: true } });需要强大的迁移系统// 复杂的迁移操作export class CreateUserTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable(/* ... */); await queryRunner.createForeignKey(/* ... */); await queryRunner.createIndex(/* ... */); }}需要支持多种数据库// 支持多种数据库类型const dataSource = new DataSource({ type: 'mysql', // 或 'postgres', 'sqlite', 'mssql', 'oracle' host: 'localhost', // ...});选择 Prisma 的场景追求最佳开发体验// 简洁的查询语法const users = await prisma.user.findMany({ where: { posts: { some: { published: true } } }, include: { posts: true }});需要最佳性能// Prisma 的性能通常优于 TypeORMconst users = await prisma.user.findMany({ take: 100});项目规模较小到中等// Prisma 适合中小型项目const user = await prisma.user.create({ data: { name: 'John', email: 'john@example.com' }});选择 Sequelize 的场景需要成熟的生态系统// 丰富的插件和中间件User.addHook('beforeCreate', (user, options) => { // 自定义逻辑});团队已经熟悉 Sequelize// Sequelize 的 API 设计相对传统const users = await User.findAll({ where: { status: 'active' }});需要支持旧版 Node.js// Sequelize 支持更旧的 Node.js 版本const sequelize = new Sequelize(/* ... */);选择 MikroORM 的场景需要最佳性能// MikroORM 的性能通常是最好的const users = await em.find(User, {}, { populate: ['posts']});需要真正的 Unit of Work 模式// Unit of Work 模式const user = em.create(User, { name: 'John' });em.persist(user);await em.flush(); // 批量提交需要高级特性// 身份映射和延迟加载const user = await em.findOne(User, 1);// 不会立即加载关联数据const posts = await user.posts.init(); // 按需加载选择 Mongoose 的场景使用 MongoDB 数据库// Mongoose 是 MongoDB 的最佳选择const user = await User.findOne({ email: 'john@example.com' });需要丰富的 MongoDB 特性// MongoDB 特有的查询const users = await User.find({ $or: [ { name: 'John' }, { email: 'john@example.com' } ]});迁移指南从 Sequelize 迁移到 TypeORM// Sequelize 模型const User = sequelize.define('User', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING }});// TypeORM 实体@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string;}从 Mongoose 迁移到 TypeORM (MongoDB)// Mongoose Schemaconst userSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, unique: true }});// TypeORM 实体 (MongoDB)@Entity()export class User { @ObjectIdColumn() id: string; @Column() name: string; @Column({ unique: true }) email: string;}总结选择 ORM 框架时需要考虑:项目需求: 功能需求、性能要求团队技能: 团队对框架的熟悉程度生态系统: 社区支持、插件生态学习曲线: 框架的学习难度长期维护: 框架的维护状态和更新频率TypeORM 是一个功能强大、类型安全的 ORM 框架,特别适合需要 TypeScript 支持和灵活设计模式的项目。但根据具体需求,其他框架可能更适合某些场景。
阅读 0·2月18日 22:21

TypeORM 如何处理事务?包括事务隔离级别、锁机制和分布式事务的详细说明

事务是数据库操作的核心概念,它确保一组数据库操作要么全部成功,要么全部失败。TypeORM 提供了多种事务处理方式,让开发者能够轻松管理数据库事务。事务基础概念什么是事务事务是数据库操作的逻辑单元,具有 ACID 特性:原子性 (Atomicity): 事务中的操作要么全部执行,要么全部不执行一致性 (Consistency): 事务执行前后数据库保持一致状态隔离性 (Isolation): 并发事务之间相互隔离持久性 (Durability): 事务提交后,修改永久保存事务的使用场景银行转账(从一个账户扣款,另一个账户入账)订单处理(创建订单、扣减库存、更新用户余额)多表关联操作需要数据一致性的复杂业务逻辑TypeORM 事务处理方式1. 使用 DataSource.transaction()这是最常用的事务处理方式,自动管理事务的提交和回滚。import { DataSource } from 'typeorm';async function transferFunds( dataSource: DataSource, fromUserId: number, toUserId: number, amount: number) { await dataSource.transaction(async transactionalEntityManager => { // 查询转出账户 const fromUser = await transactionalEntityManager.findOne(User, { where: { id: fromUserId }, lock: { mode: 'pessimistic_write' } }); if (!fromUser || fromUser.balance < amount) { throw new Error('Insufficient balance'); } // 查询转入账户 const toUser = await transactionalEntityManager.findOne(User, { where: { id: toUserId }, lock: { mode: 'pessimistic_write' } }); if (!toUser) { throw new Error('Recipient not found'); } // 扣减转出账户余额 fromUser.balance -= amount; await transactionalEntityManager.save(fromUser); // 增加转入账户余额 toUser.balance += amount; await transactionalEntityManager.save(toUser); // 记录交易日志 const transaction = transactionalEntityManager.create(Transaction, { fromUserId, toUserId, amount, type: 'transfer' }); await transactionalEntityManager.save(transaction); });}2. 使用 QueryRunnerQueryRunner 提供了更细粒度的事务控制。import { DataSource } from 'typeorm';async function transferWithQueryRunner( dataSource: DataSource, fromUserId: number, toUserId: number, amount: number) { const queryRunner = dataSource.createQueryRunner(); try { // 开启事务 await queryRunner.connect(); await queryRunner.startTransaction(); // 查询转出账户 const fromUser = await queryRunner.manager.findOne(User, { where: { id: fromUserId }, lock: { mode: 'pessimistic_write' } }); if (!fromUser || fromUser.balance < amount) { throw new Error('Insufficient balance'); } // 查询转入账户 const toUser = await queryRunner.manager.findOne(User, { where: { id: toUserId }, lock: { mode: 'pessimistic_write' } }); if (!toUser) { throw new Error('Recipient not found'); } // 执行转账操作 await queryRunner.manager.update(User, fromUserId, { balance: fromUser.balance - amount }); await queryRunner.manager.update(User, toUserId, { balance: toUser.balance + amount }); // 记录交易日志 await queryRunner.manager.insert(Transaction, { fromUserId, toUserId, amount, type: 'transfer' }); // 提交事务 await queryRunner.commitTransaction(); } catch (error) { // 回滚事务 await queryRunner.rollbackTransaction(); throw error; } finally { // 释放 QueryRunner await queryRunner.release(); }}3. 使用装饰器 Transaction在类方法上使用装饰器声明事务。import { Transaction, TransactionManager, EntityManager } from 'typeorm';class PaymentService { @Transaction() async processPayment( userId: number, amount: number, @TransactionManager() manager?: EntityManager ) { const user = await manager.findOne(User, { where: { id: userId }, lock: { mode: 'pessimistic_write' } }); if (!user || user.balance < amount) { throw new Error('Insufficient balance'); } user.balance -= amount; await manager.save(user); const payment = manager.create(Payment, { userId, amount, status: 'completed' }); await manager.save(payment); }}事务隔离级别设置隔离级别import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Transaction], // 设置默认隔离级别 extra: { connectionLimit: 10, },});// 在事务中设置隔离级别await dataSource.transaction(async transactionalEntityManager => { const queryRunner = transactionalEntityManager.queryRunner; // 设置隔离级别 await queryRunner.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); // 执行业务逻辑 // ...});隔离级别说明READ UNCOMMITTED (读未提交)可以读取未提交的数据可能出现脏读、不可重复读、幻读性能最好,但数据一致性最差READ COMMITTED (读已提交)只能读取已提交的数据避免脏读,但可能出现不可重复读、幻读大多数数据库的默认隔离级别REPEATABLE READ (可重复读)在同一事务中多次读取同一数据结果一致避免脏读、不可重复读,但可能出现幻读MySQL 的默认隔离级别SERIALIZABLE (串行化)最高隔离级别,完全避免并发问题性能最差,但数据一致性最好// 设置不同隔离级别的示例await dataSource.transaction(async transactionalEntityManager => { const queryRunner = transactionalEntityManager.queryRunner; // READ COMMITTED await queryRunner.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); // REPEATABLE READ await queryRunner.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); // SERIALIZABLE await queryRunner.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');});锁机制悲观锁悲观锁假设会发生并发冲突,在读取数据时就锁定。// SELECT ... FOR UPDATEconst user = await dataSource.transaction(async transactionalEntityManager => { const user = await transactionalEntityManager.findOne(User, { where: { id: userId }, lock: { mode: 'pessimistic_write' } }); // 此时该行被锁定,其他事务无法修改 user.balance -= amount; await transactionalEntityManager.save(user); return user;});// SELECT ... FOR SHAREconst user = await dataSource.transaction(async transactionalEntityManager => { const user = await transactionalEntityManager.findOne(User, { where: { id: userId }, lock: { mode: 'pessimistic_read' } }); // 其他事务可以读取但不能修改 return user;});乐观锁乐观锁假设不会发生并发冲突,通过版本号或时间戳检测冲突。@Entity()export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() stock: number; @VersionColumn() version: number;}// 使用乐观锁async function updateProductStock(productId: number, quantity: number) { const product = await dataSource.manager.findOne(Product, { where: { id: productId } }); if (!product || product.stock < quantity) { throw new Error('Insufficient stock'); } product.stock -= quantity; try { await dataSource.manager.save(product); } catch (error) { // 版本冲突,说明其他事务已经修改了数据 throw new Error('Concurrent modification detected'); }}嵌套事务使用 Savepointasync function complexOperation(dataSource: DataSource) { await dataSource.transaction(async transactionalEntityManager => { const queryRunner = transactionalEntityManager.queryRunner; try { // 主事务操作 await transactionalEntityManager.save(/* ... */); // 创建保存点 await queryRunner.query('SAVEPOINT savepoint1'); try { // 子事务操作 await transactionalEntityManager.save(/* ... */); // 如果成功,释放保存点 await queryRunner.query('RELEASE SAVEPOINT savepoint1'); } catch (error) { // 如果失败,回滚到保存点 await queryRunner.query('ROLLBACK TO SAVEPOINT savepoint1'); // 继续执行其他操作 } // 继续主事务操作 await transactionalEntityManager.save(/* ... */); } catch (error) { throw error; // 整个事务回滚 } });}事务超时和重试设置事务超时async function transactionWithTimeout( dataSource: DataSource, timeout: number = 30000) { const queryRunner = dataSource.createQueryRunner(); try { await queryRunner.connect(); await queryRunner.startTransaction(); // 设置事务超时(MySQL) await queryRunner.query(`SET max_execution_time = ${timeout}`); // 执行业务逻辑 // ... await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); }}事务重试机制async function transactionWithRetry( dataSource: DataSource, maxRetries: number = 3, operation: (manager: EntityManager) => Promise<void>) { let lastError: Error; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await dataSource.transaction(async transactionalEntityManager => { await operation(transactionalEntityManager); }); return; // 成功,退出重试循环 } catch (error) { lastError = error; // 如果是死锁错误,可以重试 if (error.code === 'ER_LOCK_DEADLOCK' && attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, 100 * attempt)); continue; } throw error; // 其他错误直接抛出 } } throw lastError;}// 使用示例await transactionWithRetry(dataSource, 3, async manager => { const user = await manager.findOne(User, { where: { id: 1 } }); user.balance += 100; await manager.save(user);});分布式事务两阶段提交 (2PC)TypeORM 本身不支持分布式事务,但可以通过以下方式实现:async function distributedTransaction( dataSource1: DataSource, dataSource2: DataSource) { const queryRunner1 = dataSource1.createQueryRunner(); const queryRunner2 = dataSource2.createQueryRunner(); try { await queryRunner1.connect(); await queryRunner2.connect(); await queryRunner1.startTransaction(); await queryRunner2.startTransaction(); // 第一阶段:准备 await queryRunner1.manager.save(/* ... */); await queryRunner2.manager.save(/* ... */); // 第二阶段:提交 await queryRunner1.commitTransaction(); await queryRunner2.commitTransaction(); } catch (error) { // 回滚所有事务 await queryRunner1.rollbackTransaction(); await queryRunner2.rollbackTransaction(); throw error; } finally { await queryRunner1.release(); await queryRunner2.release(); }}最佳实践1. 保持事务简短// ❌ 不好的做法:事务时间过长await dataSource.transaction(async manager => { const user = await manager.findOne(User, { where: { id: 1 } }); // 执行耗时操作(不应该在事务中) await sendEmail(user.email); await processPayment(user); user.balance += 100; await manager.save(user);});// ✅ 好的做法:只包含数据库操作const user = await dataSource.manager.findOne(User, { where: { id: 1 } });// 在事务外执行耗时操作await sendEmail(user.email);await processPayment(user);// 在事务中只执行数据库操作await dataSource.transaction(async manager => { user.balance += 100; await manager.save(user);});2. 正确处理异常await dataSource.transaction(async manager => { try { const user = await manager.findOne(User, { where: { id: 1 } }); if (!user) { throw new Error('User not found'); } user.balance += 100; await manager.save(user); } catch (error) { // 记录错误日志 console.error('Transaction failed:', error); // 抛出错误以触发回滚 throw error; }});3. 避免嵌套事务// ❌ 不好的做法:嵌套事务await dataSource.transaction(async manager1 => { await dataSource.transaction(async manager2 => { // 嵌套事务可能导致问题 });});// ✅ 好的做法:使用单一事务await dataSource.transaction(async manager => { // 所有操作在一个事务中});4. 使用适当的隔离级别// 根据业务需求选择合适的隔离级别await dataSource.transaction(async manager => { const queryRunner = manager.queryRunner; // 读多写少的场景使用 READ COMMITTED await queryRunner.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); // 需要强一致性的场景使用 SERIALIZABLE // await queryRunner.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'); // 执行业务逻辑});TypeORM 的事务处理功能强大而灵活,掌握事务的使用对于构建可靠的数据驱动应用至关重要。正确使用事务可以确保数据的一致性和完整性,避免并发问题。
阅读 0·2月18日 22:21

TypeORM 的事件系统如何工作?包括实体监听器和订阅者

TypeORM 的事件系统允许开发者在实体操作的生命周期中执行自定义逻辑,提供了强大的扩展能力。事件类型1. 实体生命周期事件TypeORM 提供了以下实体生命周期事件:BeforeInsert - 在实体插入之前触发AfterInsert - 在实体插入之后触发BeforeUpdate - 在实体更新之前触发AfterUpdate - 在实体更新之后触发BeforeRemove - 在实体删除之前触发AfterRemove - 在实体删除之后触发BeforeSoftRemove - 在实体软删除之前触发AfterSoftRemove - 在实体软删除之后触发BeforeRecover - 在实体恢复之前触发AfterRecover - 在实体恢复之后触发2. 订阅者事件订阅者可以监听所有实体的特定事件。使用实体监听器基本用法import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate, AfterInsert, AfterUpdate } from 'typeorm';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @Column({ default: 0 }) version: number; @BeforeInsert() beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); this.version = 1; } @BeforeUpdate() beforeUpdate() { this.updatedAt = new Date(); this.version++; } @AfterInsert() afterInsert() { console.log(`User ${this.name} inserted with ID ${this.id}`); } @AfterUpdate() afterUpdate() { console.log(`User ${this.name} updated to version ${this.version}`); }}复杂逻辑处理import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm';import { hash } from 'bcrypt';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column() password: string; @Column({ default: false }) emailVerified: boolean; @Column({ type: 'timestamp', nullable: true }) emailVerifiedAt: Date; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @BeforeInsert() async beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); // 加密密码 if (this.password) { this.password = await hash(this.password, 10); } // 验证邮箱格式 if (!this.validateEmail(this.email)) { throw new Error('Invalid email format'); } } @BeforeUpdate() async beforeUpdate() { this.updatedAt = new Date(); // 如果密码被修改,重新加密 if (this.password && this.isPasswordModified()) { this.password = await hash(this.password, 10); } // 如果邮箱被验证,记录验证时间 if (this.emailVerified && !this.emailVerifiedAt) { this.emailVerifiedAt = new Date(); } } private validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } private isPasswordModified(): boolean { // 实现密码修改检测逻辑 return true; }}使用订阅者基本订阅者import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';import { User } from '../entity/User';@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { // 指定监听的实体 listenTo() { return User; } // 插入前 beforeInsert(event: InsertEvent<User>) { console.log(`Before inserting user: ${event.entity.name}`); // 可以修改实体 event.entity.createdAt = new Date(); } // 插入后 afterInsert(event: InsertEvent<User>) { console.log(`After inserting user with ID: ${event.entity.id}`); // 发送欢迎邮件 this.sendWelcomeEmail(event.entity); } // 更新前 beforeUpdate(event: UpdateEvent<User>) { console.log(`Before updating user: ${event.entity.name}`); // 记录变更 this.logChanges(event); } // 更新后 afterUpdate(event: UpdateEvent<User>) { console.log(`After updating user: ${event.entity.name}`); // 发送通知 this.sendUpdateNotification(event.entity); } // 删除前 beforeRemove(event: RemoveEvent<User>) { console.log(`Before removing user: ${event.entity.name}`); // 检查是否可以删除 if (event.entity.posts && event.entity.posts.length > 0) { throw new Error('Cannot delete user with posts'); } } // 删除后 afterRemove(event: RemoveEvent<User>) { console.log(`After removing user: ${event.entity.name}`); // 清理相关数据 this.cleanupUserData(event.entity.id); } private sendWelcomeEmail(user: User) { // 发送欢迎邮件逻辑 console.log(`Sending welcome email to ${user.email}`); } private sendUpdateNotification(user: User) { // 发送更新通知逻辑 console.log(`Sending update notification to ${user.email}`); } private logChanges(event: UpdateEvent<User>) { // 记录变更逻辑 console.log('Changes:', event.updatedColumns); } private cleanupUserData(userId: number) { // 清理用户数据逻辑 console.log(`Cleaning up data for user ${userId}`); }}全局订阅者import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';@EventSubscriber()export class AuditSubscriber implements EntitySubscriberInterface { // 监听所有实体 listenTo() { return Object; } // 所有实体的插入操作 afterInsert(event: InsertEvent<any>) { console.log(`Entity ${event.metadata.name} inserted with ID ${event.entity.id}`); // 记录审计日志 this.logAudit({ action: 'INSERT', entity: event.metadata.name, entityId: event.entity.id, timestamp: new Date(), }); } private logAudit(log: any) { // 记录审计日志逻辑 console.log('Audit log:', log); }}注册订阅者在 DataSource 中注册import { DataSource } from 'typeorm';import { UserSubscriber } from './subscriber/UserSubscriber';import { AuditSubscriber } from './subscriber/AuditSubscriber';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, // 注册订阅者 subscribers: [UserSubscriber, AuditSubscriber],});动态注册订阅者import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});// 初始化后动态注册订阅者dataSource.initialize().then(() => { const userSubscriber = new UserSubscriber(); dataSource.subscribers.push(userSubscriber);});高级事件处理事务中的事件@EventSubscriber()export class TransactionSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 检查是否在事务中 if (event.queryRunner?.isTransactionActive) { console.log('Insert operation is part of a transaction'); } // 使用事务执行器 if (event.queryRunner) { event.queryRunner.manager.getRepository(AuditLog).save({ action: 'USER_INSERT', userId: event.entity.id, timestamp: new Date(), }); } }}异步事件处理@EventSubscriber()export class AsyncSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 异步发送邮件 await this.sendEmailAsync(event.entity); // 异步生成用户资料 await this.generateUserProfileAsync(event.entity); } private async sendEmailAsync(user: User) { // 模拟异步邮件发送 return new Promise((resolve) => { setTimeout(() => { console.log(`Email sent to ${user.email}`); resolve(null); }, 1000); }); } private async generateUserProfileAsync(user: User) { // 模拟异步用户资料生成 return new Promise((resolve) => { setTimeout(() => { console.log(`Profile generated for user ${user.id}`); resolve(null); }, 500); }); }}条件事件处理@EventSubscriber()export class ConditionalSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeUpdate(event: UpdateEvent<User>) { // 只在特定条件下执行 if (this.shouldProcessUpdate(event)) { this.processUpdate(event); } } private shouldProcessUpdate(event: UpdateEvent<User>): boolean { // 检查是否更新了特定字段 const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('email') || updatedFields.includes('password'); } private processUpdate(event: UpdateEvent<User>) { // 处理更新逻辑 console.log('Processing critical update:', event.entity); }}事件最佳实践1. 保持事件处理简单// ✅ 好的做法:事件处理简单直接@EventSubscriber()export class SimpleSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 简单的日志记录 console.log(`User created: ${event.entity.name}`); }}// ❌ 不好的做法:事件处理过于复杂@EventSubscriber()export class ComplexSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 复杂的业务逻辑 const user = event.entity; // 发送邮件 await this.sendEmail(user); // 创建用户资料 await this.createProfile(user); // 初始化用户设置 await this.initializeSettings(user); // 发送欢迎消息 await this.sendWelcomeMessage(user); // 记录统计 await this.recordStatistics(user); // 更新缓存 await this.updateCache(user); // 触发其他事件 await this.triggerEvents(user); }}2. 避免循环事件// ✅ 好的做法:避免循环事件@EventSubscriber()export class SafeSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 使用标志位避免循环 if (event.entity.processed) { return; } // 处理逻辑 await this.processUser(event.entity); // 标记为已处理 event.entity.processed = true; }}// ❌ 不好的做法:可能导致循环事件@EventSubscriber()export class CircularSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 更新用户,可能触发 afterUpdate 事件 await event.manager.save(User, { id: event.entity.id, processed: true, }); }}3. 错误处理// ✅ 好的做法:适当的错误处理@EventSubscriber()export class ErrorHandlingSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { try { await this.sendWelcomeEmail(event.entity); } catch (error) { console.error('Failed to send welcome email:', error); // 记录错误,但不影响主流程 await this.logError(error, event.entity); } } private async logError(error: any, user: User) { // 记录错误到数据库 await event.manager.getRepository(ErrorLog).save({ error: error.message, userId: user.id, timestamp: new Date(), }); }}4. 性能考虑// ✅ 好的做法:批量处理@EventSubscriber()export class BatchSubscriber implements EntitySubscriberInterface<User> { private batch: User[] = []; private timer: NodeJS.Timeout | null = null; listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 添加到批次 this.batch.push(event.entity); // 设置定时器 if (!this.timer) { this.timer = setTimeout(() => { this.processBatch(); }, 1000); // 1 秒后处理 } } private async processBatch() { if (this.batch.length === 0) { return; } const usersToProcess = [...this.batch]; this.batch = []; this.timer = null; // 批量处理 await this.sendBatchNotifications(usersToProcess); } private async sendBatchNotifications(users: User[]) { console.log(`Sending notifications to ${users.length} users`); // 批量发送通知逻辑 }}实际应用场景1. 审计日志@EventSubscriber()export class AuditLogSubscriber implements EntitySubscriberInterface { listenTo() { return Object; } afterInsert(event: InsertEvent<any>) { this.logAudit('INSERT', event.entity); } afterUpdate(event: UpdateEvent<any>) { this.logAudit('UPDATE', event.entity, event.updatedColumns); } afterRemove(event: RemoveEvent<any>) { this.logAudit('DELETE', event.entity); } private async logAudit(action: string, entity: any, columns?: any[]) { const auditLog = { action, entityName: entity.constructor.name, entityId: entity.id, changes: columns ? columns.map(col => col.propertyName) : null, timestamp: new Date(), }; await event.manager.getRepository(AuditLog).save(auditLog); }}2. 缓存失效@EventSubscriber()export class CacheInvalidationSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterUpdate(event: UpdateEvent<User>) { // 清除用户缓存 this.clearUserCache(event.entity.id); // 清除相关缓存 this.clearRelatedCache(event.entity.id); } afterRemove(event: RemoveEvent<User>) { // 清除所有相关缓存 this.clearAllUserCache(event.entity.id); } private clearUserCache(userId: number) { // 清除用户缓存逻辑 console.log(`Clearing cache for user ${userId}`); } private clearRelatedCache(userId: number) { // 清除相关缓存逻辑 console.log(`Clearing related cache for user ${userId}`); } private clearAllUserCache(userId: number) { // 清除所有用户缓存逻辑 console.log(`Clearing all cache for user ${userId}`); }}3. 通知系统@EventSubscriber()export class NotificationSubscriber implements EntitySubscriberInterface<Post> { listenTo() { return Post; } afterInsert(event: InsertEvent<Post>) { // 通知关注者 this.notifyFollowers(event.entity); // 通知作者 this.notifyAuthor(event.entity); } afterUpdate(event: UpdateEvent<Post>) { // 如果文章被发布,通知关注者 if (this.isPublished(event)) { this.notifyFollowers(event.entity); } } private isPublished(event: UpdateEvent<Post>): boolean { const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('status') && event.entity.status === 'published'; } private async notifyFollowers(post: Post) { // 通知关注者逻辑 console.log(`Notifying followers of post ${post.id}`); } private async notifyAuthor(post: Post) { // 通知作者逻辑 console.log(`Notifying author of post ${post.id}`); }}TypeORM 的事件系统提供了强大的扩展能力,合理使用事件可以简化业务逻辑,提高代码的可维护性。
阅读 0·2月18日 22:20

TypeORM 中如何定义和使用关系映射?包括一对一、一对多、多对多关系的详细配置

TypeORM 提供了四种主要的关系映射类型,每种关系都有其特定的使用场景和配置方式。理解这些关系映射对于构建复杂的数据模型至关重要。四种关系类型1. One-to-One (一对一)一对一关系表示两个实体之间存在唯一的对应关系。例如,一个用户只能有一个个人资料。@Entity()export class Profile { @PrimaryGeneratedColumn() id: number; @Column() gender: string; @Column() bio: string; @OneToOne(() => User, user => user.profile) @JoinColumn() user: User;}@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToOne(() => Profile, profile => profile.user, { cascade: true }) profile: Profile;}关键点:使用 @OneToOne() 装饰器定义关系在拥有方使用 @JoinColumn() 指定外键列cascade: true 允许级联操作(保存、删除等)2. One-to-Many / Many-to-One (一对多/多对一)一对多关系是最常见的关系类型。例如,一个用户可以发表多篇文章。@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() content: string; @ManyToOne(() => User, user => user.posts) author: User;}@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[];}关键点:@ManyToOne() 放在"多"的一方,包含外键@OneToMany() 放在"一"的一方,不需要 @JoinColumn()外键自动添加到"多"的一方表中3. Many-to-Many (多对多)多对多关系需要中间表来连接两个实体。例如,一篇文章可以有多个标签,一个标签也可以属于多篇文章。@Entity()export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToMany(() => Post, post => post.tags) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToMany(() => Tag, tag => tag.posts, { cascade: true }) @JoinTable() tags: Tag[];}关键点:使用 @JoinTable() 在关系的一方定义中间表中间表自动创建,包含两个外键可以自定义中间表名称和列名关系配置选项Eager 和 Lazy 加载@Entity()export class User { @OneToMany(() => Post, post => post.author, { eager: true // 立即加载关联数据 }) posts: Post[];}// 或者使用懒加载@Entity()export class User { @OneToMany(() => Post, post => post.author) posts: Promise<Post[]>;}级联操作 (Cascade)@OneToMany(() => Post, post => post.author, { cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover']})posts: Post[];级联操作选项:insert: 保存父实体时自动保存子实体update: 更新父实体时自动更新子实体remove: 删除父实体时自动删除子实体soft-remove: 软删除recover: 恢复软删除的实体OnDelete 和 OnUpdate@ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE', // 删除用户时级联删除文章 onUpdate: 'CASCADE' // 更新用户ID时级联更新文章})author: User;关系查询使用 FindOptions 查询关联数据const userRepository = dataSource.getRepository(User);// 加载关联数据const users = await userRepository.find({ relations: ['posts', 'profile']});// 条件查询关联数据const usersWithPosts = await userRepository.find({ relations: { posts: true }, where: { posts: { title: Like('%TypeORM%') } }});使用 QueryBuilderconst users = await dataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('user.profile', 'profile') .where('post.title = :title', { title: 'TypeORM Guide' }) .getMany();自定义关系自定义 JoinTable@ManyToMany(() => Tag, tag => tag.posts)@JoinTable({ name: 'post_tags', joinColumn: { name: 'postId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'tagId', referencedColumnName: 'id' }})tags: Tag[];自定义 JoinColumn@ManyToOne(() => User, user => user.posts)@JoinColumn({ name: 'author_id', referencedColumnName: 'id'})author: User;最佳实践合理选择关系类型: 根据业务需求选择最合适的关系类型避免过度使用 Eager 加载: 可能导致 N+1 查询问题谨慎使用级联删除: 确保不会意外删除重要数据使用索引优化查询: 为外键列添加索引考虑性能影响: 复杂关系查询可能影响性能TypeORM 的关系映射系统提供了强大而灵活的方式来处理实体之间的关系,掌握这些概念对于构建高效、可维护的应用程序至关重要。
阅读 0·2月18日 22:20

TypeORM 的 QueryBuilder 如何使用?包括复杂查询、关联查询、分页排序等高级功能

QueryBuilder 是 TypeORM 中最强大、最灵活的查询工具,它允许开发者构建复杂的 SQL 查询,同时保持类型安全和可读性。QueryBuilder 基础用法创建 QueryBuilderimport { DataSource } from 'typeorm';const dataSource = new DataSource(/* 配置 */);// 方式1: 通过 Repository 创建const userRepository = dataSource.getRepository(User);const queryBuilder = userRepository.createQueryBuilder('user');// 方式2: 通过 DataSource 创建const queryBuilder = dataSource.createQueryBuilder(User, 'user');基本查询操作// 查询所有用户const users = await dataSource .createQueryBuilder('user') .getMany();// 查询单个用户const user = await dataSource .createQueryBuilder('user') .where('user.id = :id', { id: 1 }) .getOne();// 查询并计数const count = await dataSource .createQueryBuilder('user') .getCount();条件查询Where 子句// 简单条件const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .getMany();// 多个条件 (AND)const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .andWhere('user.isActive = :isActive', { isActive: true }) .getMany();// OR 条件const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .orWhere('user.role = :role', { role: 'admin' }) .getMany();// 复杂条件组合const users = await dataSource .createQueryBuilder('user') .where( new Brackets(qb => { qb.where('user.age > :age', { age: 18 }) .orWhere('user.role = :role', { role: 'admin' }); }) ) .andWhere('user.isActive = :isActive', { isActive: true }) .getMany();操作符import { Like, Between, In, MoreThan, LessThan } from 'typeorm';// LIKE 查询const users = await dataSource .createQueryBuilder('user') .where('user.name LIKE :name', { name: '%John%' }) .getMany();// 或者使用 Like 操作符const users = await dataSource .createQueryBuilder('user') .where('user.name = :name', { name: Like('%John%') }) .getMany();// BETWEEN 查询const users = await dataSource .createQueryBuilder('user') .where('user.age = :age', { age: Between(18, 30) }) .getMany();// IN 查询const users = await dataSource .createQueryBuilder('user') .where('user.id IN :ids', { ids: [1, 2, 3] }) .getMany();// 或者使用 In 操作符const users = await dataSource .createQueryBuilder('user') .where('user.id = :ids', { ids: In([1, 2, 3]) }) .getMany();// 比较操作符const users = await dataSource .createQueryBuilder('user') .where('user.age = :age', { age: MoreThan(18) }) .andWhere('user.score = :score', { score: LessThan(100) }) .getMany();关联查询Left Join 和 Inner Join// Left Join (包含没有关联数据的记录)const users = await dataSource .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getMany();// Inner Join (只包含有关联数据的记录)const users = await dataSource .createQueryBuilder('user') .innerJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getMany();// 多层关联const users = await dataSource .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.comments', 'comment') .leftJoinAndSelect('comment.author', 'commentAuthor') .getMany();Join 条件const users = await dataSource .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.status = :status', { status: 'published' }) .addSelect(['post.title', 'post.createdAt']) .getMany();排序和分页排序// 单字段排序const users = await dataSource .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .getMany();// 多字段排序const users = await dataSource .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .addOrderBy('user.name', 'ASC') .getMany();// 随机排序 (MySQL)const users = await dataSource .createQueryBuilder('user') .orderBy('RAND()') .getMany();分页// 基本分页const page = 1;const pageSize = 10;const users = await dataSource .createQueryBuilder('user') .skip((page - 1) * pageSize) .take(pageSize) .getMany();// 获取总数和分页数据const [users, total] = await dataSource .createQueryBuilder('user') .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount();console.log(`Total: ${total}, Page: ${page}, PageSize: ${pageSize}`);聚合查询Group By 和 Having// 按角色分组统计用户数const result = await dataSource .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .groupBy('user.role') .getRawMany();// 使用 Having 过滤分组const result = await dataSource .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .groupBy('user.role') .having('COUNT(*) > :minCount', { minCount: 5 }) .getRawMany();聚合函数// 统计总数const count = await dataSource .createQueryBuilder('user') .select('COUNT(*)', 'count') .getRawOne();// 计算平均值const avgAge = await dataSource .createQueryBuilder('user') .select('AVG(user.age)', 'avgAge') .getRawOne();// 求和const totalScore = await dataSource .createQueryBuilder('user') .select('SUM(user.score)', 'totalScore') .getRawOne();// 最大值和最小值const result = await dataSource .createQueryBuilder('user') .select('MAX(user.age)', 'maxAge') .addSelect('MIN(user.age)', 'minAge') .getRawOne();子查询使用 SubQueryFactoryimport { SubQueryFactory } from 'typeorm';const users = await dataSource .createQueryBuilder('user') .where((qb: SelectQueryBuilder<User>) => { const subQuery = qb .subQuery() .select('post.userId') .from(Post, 'post') .where('post.title LIKE :title', { title: '%TypeORM%' }) .getQuery(); return 'user.id IN ' + subQuery; }) .setParameter('title', '%TypeORM%') .getMany();使用 EXISTSconst users = await dataSource .createQueryBuilder('user') .where((qb: SelectQueryBuilder<User>) => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.userId = user.id') .getQuery(); return 'EXISTS ' + subQuery; }) .getMany();更新和删除更新操作// 简单更新await dataSource .createQueryBuilder(User, 'user') .update() .set({ name: 'Updated Name' }) .where('id = :id', { id: 1 }) .execute();// 条件更新await dataSource .createQueryBuilder(User, 'user') .update() .set({ isActive: false }) .where('user.lastLoginAt < :date', { date: new Date('2024-01-01') }) .execute();// 基于子查询的更新await dataSource .createQueryBuilder(User, 'user') .update() .set({ score: () => 'score + 10' }) .where('user.id IN :ids', { ids: [1, 2, 3] }) .execute();删除操作// 简单删除await dataSource .createQueryBuilder(User, 'user') .delete() .where('id = :id', { id: 1 }) .execute();// 条件删除await dataSource .createQueryBuilder(User, 'user') .delete() .where('user.createdAt < :date', { date: new Date('2023-01-01') }) .andWhere('user.isActive = :isActive', { isActive: false }) .execute();高级特性原生 SQLconst users = await dataSource .createQueryBuilder(User, 'user') .where('user.id = :id', { id: 1 }) .andWhere('JSON_CONTAINS(user.preferences, :preferences)', { preferences: JSON.stringify({ theme: 'dark' }) }) .getMany();缓存const users = await dataSource .createQueryBuilder('user') .where('user.isActive = :isActive', { isActive: true }) .cache(60000) // 缓存 60 秒 .getMany();事务await dataSource.transaction(async transactionalEntityManager => { const queryRunner = transactionalEntityManager.queryRunner; await queryRunner.manager .createQueryBuilder(User, 'user') .insert() .values({ name: 'John', email: 'john@example.com' }) .execute(); await queryRunner.manager .createQueryBuilder(Post, 'post') .insert() .values({ title: 'New Post', authorId: 1 }) .execute();});性能优化建议避免 N+1 查询: 使用 leftJoinAndSelect 一次性加载关联数据只选择需要的字段: 使用 select() 明确指定需要的列合理使用索引: 为常用查询条件添加数据库索引使用缓存: 对不常变化的数据启用查询缓存限制返回结果: 使用 take() 和 skip() 实现分页监控查询性能: 使用 getQuery() 和 getSql() 查看生成的 SQLQueryBuilder 是 TypeORM 中最强大的查询工具,掌握它的使用可以让你构建出高效、灵活的数据库查询。
阅读 0·2月18日 22:19

TypeORM 的核心概念是什么?包括 Entity、Repository、DataSource 等主要组件的详细说明

TypeORM 是一个基于 TypeScript 和 JavaScript 的 ORM 框架,它采用 Active Record 和 Data Mapper 两种设计模式,让开发者能够使用面向对象的方式来操作关系型数据库。核心概念1. Entity (实体)Entity 是 TypeORM 的核心概念,它对应数据库中的表。每个 Entity 类都使用 @Entity() 装饰器标记,并映射到数据库表。@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date;}2. Column (列)Column 装饰器用于定义实体属性如何映射到数据库列:@Column(): 基本列定义@PrimaryGeneratedColumn(): 自增主键@CreateDateColumn(): 自动创建时间@UpdateDateColumn(): 自动更新时间@Generated(): 自动生成值3. Repository (仓储)Repository 是用于操作实体的主要接口,提供了 CRUD 操作:const userRepository = dataSource.getRepository(User);// 创建const user = userRepository.create({ name: 'John', email: 'john@example.com' });await userRepository.save(user);// 查询const users = await userRepository.find();const user = await userRepository.findOne({ where: { id: 1 } });// 更新await userRepository.update(1, { name: 'John Updated' });// 删除await userRepository.delete(1);4. DataSource (数据源)DataSource 是数据库连接的配置和管理中心:const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test', entities: [User], synchronize: true,});5. Relation (关系)TypeORM 支持四种主要的关系类型:One-to-One (一对一): 使用 @OneToOne() 装饰器One-to-Many (一对多): 使用 @OneToMany() 装饰器Many-to-One (多对一): 使用 @ManyToOne() 装饰器Many-to-Many (多对多): 使用 @ManyToMany() 装饰器@Entity()export class Profile { @OneToOne(() => User, user => user.profile) user: User;}@Entity()export class User { @OneToOne(() => Profile, profile => profile.user) profile: Profile; @OneToMany(() => Post, post => post.author) posts: Post[];}设计模式Active Record 模式在 Active Record 模式中,实体本身包含业务逻辑和数据访问方法:@Entity()export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; static async findByName(name: string) { return this.find({ where: { name } }); }}// 使用const users = await User.findByName('John');Data Mapper 模式在 Data Mapper 模式中,数据访问逻辑与实体分离,通过 Repository 操作:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string;}// 使用 Repositoryconst userRepository = dataSource.getRepository(User);const users = await userRepository.find({ where: { name: 'John' } });关键特性类型安全: 完全支持 TypeScript,提供编译时类型检查装饰器驱动: 使用装饰器简化配置迁移系统: 支持数据库迁移和版本控制查询构建器: 提供灵活的查询构建 API事务支持: 完整的事务管理缓存支持: 内置查询缓存机制多数据库支持: MySQL, PostgreSQL, SQLite, SQL Server, Oracle 等TypeORM 的这些核心概念使其成为 Node.js 生态中最流行的 ORM 框架之一,特别适合需要类型安全和面向对象编程风格的项目。
阅读 0·2月18日 22:19

TypeORM 如何在微服务架构中使用?包括数据一致性、分布式事务和服务间通信

在微服务架构中使用 TypeORM 需要考虑数据一致性、服务间通信、分布式事务等复杂问题。微服务中的数据管理1. 数据库分离策略// 用户服务 - user-service// config/database.tsimport { DataSource } from 'typeorm';export const userDataSource = new DataSource({ type: 'postgres', host: process.env.USER_DB_HOST || 'localhost', port: parseInt(process.env.USER_DB_PORT || '5432'), username: process.env.USER_DB_USER || 'user_service', password: process.env.USER_DB_PASSWORD || 'password', database: process.env.USER_DB_NAME || 'user_db', entities: [User, UserProfile, UserSettings], synchronize: false, logging: true,});// 订单服务 - order-serviceexport const orderDataSource = new DataSource({ type: 'postgres', host: process.env.ORDER_DB_HOST || 'localhost', port: parseInt(process.env.ORDER_DB_PORT || '5432'), username: process.env.ORDER_DB_USER || 'order_service', password: process.env.ORDER_DB_PASSWORD || 'password', database: process.env.ORDER_DB_NAME || 'order_db', entities: [Order, OrderItem, Payment], synchronize: false, logging: true,});// 产品服务 - product-serviceexport const productDataSource = new DataSource({ type: 'postgres', host: process.env.PRODUCT_DB_HOST || 'localhost', port: parseInt(process.env.PRODUCT_DB_PORT || '5432'), username: process.env.PRODUCT_DB_USER || 'product_service', password: process.env.PRODUCT_DB_PASSWORD || 'password', database: process.env.PRODUCT_DB_NAME || 'product_db', entities: [Product, Category, Inventory], synchronize: false, logging: true,});2. 服务间数据同步// 用户服务 - 同步用户数据到其他服务import { EventEmitter } from 'events';class UserSyncService extends EventEmitter { async syncUserCreated(user: User) { // 发送用户创建事件 this.emit('user.created', { userId: user.id, email: user.email, name: user.name, timestamp: new Date(), }); } async syncUserUpdated(user: User) { // 发送用户更新事件 this.emit('user.updated', { userId: user.id, email: user.email, name: user.name, timestamp: new Date(), }); } async syncUserDeleted(userId: number) { // 发送用户删除事件 this.emit('user.deleted', { userId, timestamp: new Date(), }); }}// 在用户服务中使用@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { private syncService: UserSyncService; constructor() { this.syncService = new UserSyncService(); } listenTo() { return User; } afterInsert(event: InsertEvent<User>) { this.syncService.syncUserCreated(event.entity); } afterUpdate(event: UpdateEvent<User>) { this.syncService.syncUserUpdated(event.entity); } afterRemove(event: RemoveEvent<User>) { this.syncService.syncUserDeleted(event.entity.id); }}分布式事务处理1. Saga 模式实现// 订单创建 Sagaclass OrderCreationSaga { private steps: SagaStep[] = []; constructor( private orderDataSource: DataSource, private userDataSource: DataSource, private productDataSource: DataSource ) { this.setupSteps(); } private setupSteps() { // 步骤 1: 验证用户 this.steps.push({ name: 'validateUser', execute: async (data: any) => { const userRepo = this.userDataSource.getRepository(User); const user = await userRepo.findOne({ where: { id: data.userId }, }); if (!user) { throw new Error('User not found'); } return { ...data, user }; }, compensate: async (data: any) => { // 验证步骤无需补偿 }, }); // 步骤 2: 检查库存 this.steps.push({ name: 'checkInventory', execute: async (data: any) => { const inventoryRepo = this.productDataSource.getRepository(Inventory); for (const item of data.items) { const inventory = await inventoryRepo.findOne({ where: { productId: item.productId }, }); if (!inventory || inventory.quantity < item.quantity) { throw new Error(`Insufficient inventory for product ${item.productId}`); } } return data; }, compensate: async (data: any) => { // 检查库存步骤无需补偿 }, }); // 步骤 3: 扣减库存 this.steps.push({ name: 'reserveInventory', execute: async (data: any) => { const inventoryRepo = this.productDataSource.getRepository(Inventory); for (const item of data.items) { await inventoryRepo.decrement( { productId: item.productId }, 'quantity', item.quantity ); } return data; }, compensate: async (data: any) => { // 补偿:恢复库存 const inventoryRepo = this.productDataSource.getRepository(Inventory); for (const item of data.items) { await inventoryRepo.increment( { productId: item.productId }, 'quantity', item.quantity ); } }, }); // 步骤 4: 创建订单 this.steps.push({ name: 'createOrder', execute: async (data: any) => { const orderRepo = this.orderDataSource.getRepository(Order); const order = orderRepo.create({ userId: data.userId, items: data.items, totalAmount: data.totalAmount, status: 'pending', }); const savedOrder = await orderRepo.save(order); return { ...data, orderId: savedOrder.id }; }, compensate: async (data: any) => { // 补偿:删除订单 const orderRepo = this.orderDataSource.getRepository(Order); await orderRepo.delete(data.orderId); }, }); // 步骤 5: 处理支付 this.steps.push({ name: 'processPayment', execute: async (data: any) => { const paymentRepo = this.orderDataSource.getRepository(Payment); const payment = paymentRepo.create({ orderId: data.orderId, amount: data.totalAmount, status: 'processing', }); await paymentRepo.save(payment); // 模拟支付处理 await this.processPaymentAsync(payment); return data; }, compensate: async (data: any) => { // 补偿:取消支付 const paymentRepo = this.orderDataSource.getRepository(Payment); await paymentRepo.update( { orderId: data.orderId }, { status: 'cancelled' } ); }, }); } async execute(data: any): Promise<any> { const executedSteps: SagaStep[] = []; try { for (const step of this.steps) { console.log(`Executing step: ${step.name}`); data = await step.execute(data); executedSteps.push(step); } // 所有步骤成功,更新订单状态 await this.orderDataSource.getRepository(Order).update( { id: data.orderId }, { status: 'completed' } ); return data; } catch (error) { console.error('Saga failed, compensating...', error); // 执行补偿操作 for (let i = executedSteps.length - 1; i >= 0; i--) { const step = executedSteps[i]; console.log(`Compensating step: ${step.name}`); try { await step.compensate(data); } catch (compensationError) { console.error(`Compensation failed for step ${step.name}:`, compensationError); } } throw error; } } private async processPaymentAsync(payment: Payment): Promise<void> { // 模拟支付处理 return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); }}interface SagaStep { name: string; execute: (data: any) => Promise<any>; compensate: (data: any) => Promise<void>;}2. 两阶段提交 (2PC)// 两阶段提交协调器class TwoPhaseCommitCoordinator { private participants: TwoPhaseCommitParticipant[] = []; registerParticipant(participant: TwoPhaseCommitParticipant) { this.participants.push(participant); } async execute(): Promise<void> { // 阶段 1: 准备 console.log('Phase 1: Prepare'); for (const participant of this.participants) { await participant.prepare(); } // 阶段 2: 提交 console.log('Phase 2: Commit'); for (const participant of this.participants) { await participant.commit(); } } async rollback(): Promise<void> { console.log('Rolling back'); for (const participant of this.participants) { await participant.rollback(); } }}interface TwoPhaseCommitParticipant { prepare(): Promise<void>; commit(): Promise<void>; rollback(): Promise<void>;}// 用户服务参与者class UserServiceParticipant implements TwoPhaseCommitParticipant { constructor(private dataSource: DataSource) {} async prepare(): Promise<void> { console.log('UserService: Preparing...'); // 准备用户数据 } async commit(): Promise<void> { console.log('UserService: Committing...'); // 提交用户数据 } async rollback(): Promise<void> { console.log('UserService: Rolling back...'); // 回滚用户数据 }}// 订单服务参与者class OrderServiceParticipant implements TwoPhaseCommitParticipant { constructor(private dataSource: DataSource) {} async prepare(): Promise<void> { console.log('OrderService: Preparing...'); // 准备订单数据 } async commit(): Promise<void> { console.log('OrderService: Committing...'); // 提交订单数据 } async rollback(): Promise<void> { console.log('OrderService: Rolling back...'); // 回滚订单数据 }}事件驱动架构1. 事件总线实现// 事件总线class EventBus { private eventQueue: any[] = []; private subscribers: Map<string, Function[]> = new Map(); publish(event: any) { console.log(`Publishing event: ${event.type}`); this.eventQueue.push(event); this.processEvent(event); } subscribe(eventType: string, handler: Function) { if (!this.subscribers.has(eventType)) { this.subscribers.set(eventType, []); } this.subscribers.get(eventType)!.push(handler); } private processEvent(event: any) { const handlers = this.subscribers.get(event.type); if (handlers) { handlers.forEach(handler => { try { handler(event); } catch (error) { console.error(`Error processing event ${event.type}:`, error); } }); } }}// 全局事件总线实例export const eventBus = new EventBus();// 在用户服务中发布事件@EventSubscriber()export class UserEventPublisher implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { eventBus.publish({ type: 'user.created', payload: { userId: event.entity.id, email: event.entity.email, name: event.entity.name, timestamp: new Date(), }, }); } afterUpdate(event: UpdateEvent<User>) { eventBus.publish({ type: 'user.updated', payload: { userId: event.entity.id, email: event.entity.email, name: event.entity.name, timestamp: new Date(), }, }); }}// 在订单服务中订阅用户事件eventBus.subscribe('user.created', async (event: any) => { console.log('OrderService received user.created event:', event); // 处理用户创建事件 await handleUserCreated(event.payload);});eventBus.subscribe('user.updated', async (event: any) => { console.log('OrderService received user.updated event:', event); // 处理用户更新事件 await handleUserUpdated(event.payload);});2. 消息队列集成// 使用 RabbitMQ 作为消息队列import amqp from 'amqplib';class MessageQueueService { private connection: any; private channel: any; async connect() { this.connection = await amqp.connect('amqp://localhost'); this.channel = await this.connection.createChannel(); } async publish(queue: string, message: any) { await this.channel.assertQueue(queue, { durable: true }); this.channel.sendToQueue( queue, Buffer.from(JSON.stringify(message)), { persistent: true } ); } async subscribe(queue: string, handler: Function) { await this.channel.assertQueue(queue, { durable: true }); this.channel.consume(queue, async (msg: any) => { try { const message = JSON.parse(msg.content.toString()); await handler(message); this.channel.ack(msg); } catch (error) { console.error('Error processing message:', error); this.channel.nack(msg, false, true); // 重新入队 } }); } async close() { await this.channel.close(); await this.connection.close(); }}// 在用户服务中发布消息const messageQueue = new MessageQueueService();await messageQueue.connect();@EventSubscriber()export class UserMessagePublisher implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { await messageQueue.publish('user.created', { userId: event.entity.id, email: event.entity.email, name: event.entity.name, timestamp: new Date(), }); }}// 在订单服务中订阅消息await messageQueue.subscribe('user.created', async (message: any) => { console.log('OrderService received user.created message:', message); await handleUserCreated(message);});数据一致性策略1. 最终一致性// 用户数据同步服务class UserDataSyncService { async syncUserToOrderService(user: User) { try { // 调用订单服务 API await fetch('http://order-service/api/users/sync', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ userId: user.id, email: user.email, name: user.name, }), }); } catch (error) { // 记录失败,稍后重试 console.error('Failed to sync user to order service:', error); await this.scheduleRetry(user); } } async scheduleRetry(user: User) { // 使用延迟队列重试 setTimeout(() => { this.syncUserToOrderService(user); }, 5000); // 5 秒后重试 }}// 在用户服务中使用@EventSubscriber()export class UserSyncSubscriber implements EntitySubscriberInterface<User> { private syncService: UserDataSyncService; constructor() { this.syncService = new UserDataSyncService(); } listenTo() { return User; } afterInsert(event: InsertEvent<User>) { this.syncService.syncUserToOrderService(event.entity); } afterUpdate(event: UpdateEvent<User>) { this.syncService.syncUserToOrderService(event.entity); }}2. 幂等性处理// 幂等性处理器class IdempotencyHandler { private processedIds: Set<string> = new Set(); async processWithIdempotency( id: string, operation: () => Promise<void> ): Promise<void> { if (this.processedIds.has(id)) { console.log(`Operation ${id} already processed, skipping`); return; } await operation(); this.processedIds.add(id); } // 使用数据库存储已处理的 ID async processWithDatabaseIdempotency( id: string, operation: () => Promise<void> ): Promise<void> { const idempotencyRepo = this.dataSource.getRepository(IdempotencyKey); const existing = await idempotencyRepo.findOne({ where: { key: id }, }); if (existing) { console.log(`Operation ${id} already processed, skipping`); return; } await operation(); await idempotencyRepo.save({ key: id, processedAt: new Date(), }); }}// 在消息处理中使用const idempotencyHandler = new IdempotencyHandler();await messageQueue.subscribe('user.created', async (message: any) => { await idempotencyHandler.processWithDatabaseIdempotency( `user.created.${message.userId}`, async () => { await handleUserCreated(message); } );});服务发现与负载均衡1. 服务注册// 服务注册中心class ServiceRegistry { private services: Map<string, ServiceInstance[]> = new Map(); register(serviceName: string, instance: ServiceInstance) { if (!this.services.has(serviceName)) { this.services.set(serviceName, []); } this.services.get(serviceName)!.push(instance); console.log(`Registered ${serviceName} instance:`, instance); } unregister(serviceName: string, instance: ServiceInstance) { const instances = this.services.get(serviceName); if (instances) { const index = instances.indexOf(instance); if (index > -1) { instances.splice(index, 1); console.log(`Unregistered ${serviceName} instance:`, instance); } } } discover(serviceName: string): ServiceInstance | null { const instances = this.services.get(serviceName); if (!instances || instances.length === 0) { return null; } // 轮询负载均衡 const index = Math.floor(Math.random() * instances.length); return instances[index]; }}interface ServiceInstance { host: string; port: number; healthCheckUrl: string;}// 在服务启动时注册const serviceRegistry = new ServiceRegistry();serviceRegistry.register('user-service', { host: 'localhost', port: 3001, healthCheckUrl: 'http://localhost:3001/health',});serviceRegistry.register('order-service', { host: 'localhost', port: 3002, healthCheckUrl: 'http://localhost:3002/health',});2. 健康检查// 健康检查服务class HealthCheckService { async checkService(instance: ServiceInstance): Promise<boolean> { try { const response = await fetch(instance.healthCheckUrl); return response.ok; } catch (error) { console.error(`Health check failed for ${instance.host}:${instance.port}:`, error); return false; } } async startHealthChecks(registry: ServiceRegistry) { setInterval(async () => { const services = ['user-service', 'order-service', 'product-service']; for (const serviceName of services) { const instance = registry.discover(serviceName); if (instance) { const isHealthy = await this.checkService(instance); if (!isHealthy) { console.log(`Service ${serviceName} is unhealthy, removing from registry`); registry.unregister(serviceName, instance); } } } }, 30000); // 每 30 秒检查一次 }}// 启动健康检查const healthCheckService = new HealthCheckService();healthCheckService.startHealthChecks(serviceRegistry);TypeORM 在微服务架构中的应用需要仔细设计数据管理、事务处理和通信机制,以确保系统的可靠性和可扩展性。
阅读 0·2月18日 22:18

TypeORM 如何进行性能优化?包括查询优化、索引使用和批量操作

性能优化是数据库操作中的关键环节,TypeORM 提供了多种优化策略和技巧。查询优化1. 选择性加载字段// ❌ 不好的做法:加载所有字段const users = await userRepository.find();// ✅ 好的做法:只加载需要的字段const users = await userRepository.find({ select: ['id', 'name', 'email'],});// 使用 QueryBuilder 选择字段const users = await userRepository .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany();// 排除特定字段const users = await userRepository.find({ select: { id: true, name: true, email: true, password: false, // 排除密码字段 },});2. 使用索引import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';@Entity()@Index(['email']) // 单列索引@Index(['name', 'email']) // 复合索引@Index(['createdAt']) // 普通索引@Index(['status'], { unique: true }) // 唯一索引@Index(['name'], { fulltext: true }) // 全文索引export class User { @PrimaryGeneratedColumn() id: number; @Column() @Index() // 列级索引 name: string; @Column() @Index({ unique: true }) email: string; @Column() status: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'text' }) bio: string;}// 在查询中使用索引const users = await userRepository.find({ where: { email: 'john@example.com', // 使用索引 },});// 使用复合索引const users = await userRepository.find({ where: { name: 'John', email: 'john@example.com', },});3. 分页优化// 使用 skip 和 take 分页const users = await userRepository.find({ skip: 0, take: 10,});// 使用 QueryBuilder 分页const users = await userRepository .createQueryBuilder('user') .skip(0) .take(10) .getMany();// 使用游标分页(性能更好)const users = await userRepository .createQueryBuilder('user') .where('user.id > :cursor', { cursor: lastId }) .orderBy('user.id', 'ASC') .take(10) .getMany();// 使用 LIMIT 和 OFFSETconst users = await userRepository .createQueryBuilder('user') .limit(10) .offset(0) .getMany();关联优化1. 懒加载 vs 急切加载// ❌ 不好的做法:N+1 查询问题const users = await userRepository.find();for (const user of users) { const posts = await user.posts; // 每次都查询数据库}// ✅ 好的做法:使用急切加载const users = await userRepository.find({ relations: ['posts'],});// 使用 QueryBuilder 急切加载const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();// 选择性加载关联const users = await userRepository.find({ relations: { posts: true, profile: false, // 不加载 profile },});// 使用 join 加载关联const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .addSelect(['post.id', 'post.title']) .getMany();2. 关联查询优化// 使用 innerJoin 代替 leftJoin(如果确定有关联数据)const users = await userRepository .createQueryBuilder('user') .innerJoin('user.posts', 'post') .where('post.published = :published', { published: true }) .getMany();// 使用 join 条件过滤const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.published = :published', { published: true }) .getMany();// 使用子查询优化const users = await userRepository .createQueryBuilder('user') .where((qb) => { const subQuery = qb .subQuery() .select('post.userId') .from(Post, 'post') .where('post.published = :published', { published: true }) .getQuery(); return 'user.id IN ' + subQuery; }) .setParameter('published', true) .getMany();批量操作优化1. 批量插入// ❌ 不好的做法:循环插入for (const userData of usersData) { const user = userRepository.create(userData); await userRepository.save(user);}// ✅ 好的做法:批量插入const users = usersData.map(userData => userRepository.create(userData));await userRepository.save(users);// 使用 QueryBuilder 批量插入await userRepository .createQueryBuilder() .insert() .into(User) .values(usersData) .execute();// 使用 bulk insertawait userRepository.insert(usersData);2. 批量更新// ❌ 不好的做法:循环更新for (const user of users) { await userRepository.update(user.id, { status: 'active' });}// ✅ 好的做法:批量更新await userRepository.update( { status: 'inactive' }, { status: 'active' });// 使用 QueryBuilder 批量更新await userRepository .createQueryBuilder() .update(User) .set({ status: 'active' }) .where('status = :status', { status: 'inactive' }) .execute();// 使用 CASE WHEN 批量更新await userRepository .createQueryBuilder() .update(User) .set({ status: () => 'CASE WHEN age > 18 THEN "active" ELSE "inactive" END', }) .execute();3. 批量删除// ❌ 不好的做法:循环删除for (const user of users) { await userRepository.delete(user.id);}// ✅ 好的做法:批量删除await userRepository.delete({ status: 'deleted' });// 使用 QueryBuilder 批量删除await userRepository .createQueryBuilder() .delete() .from(User) .where('status = :status', { status: 'deleted' }) .execute();// 使用软删除批量删除await userRepository.softDelete({ status: 'deleted' });事务优化1. 事务范围控制// ❌ 不好的做法:大事务await dataSource.transaction(async (manager) => { // 执行大量操作 for (let i = 0; i < 1000; i++) { await manager.save(User, { name: `User ${i}` }); }});// ✅ 好的做法:分批处理const batchSize = 100;for (let i = 0; i < 1000; i += batchSize) { await dataSource.transaction(async (manager) => { const users = []; for (let j = i; j < i + batchSize && j < 1000; j++) { users.push({ name: `User ${j}` }); } await manager.save(User, users); });}2. 使用 QueryRunner 优化// 使用 QueryRunner 获得更好的性能控制const queryRunner = dataSource.createQueryRunner();try { await queryRunner.connect(); await queryRunner.startTransaction(); // 执行多个操作 await queryRunner.manager.save(User, user1); await queryRunner.manager.save(User, user2); await queryRunner.commitTransaction();} catch (error) { await queryRunner.rollbackTransaction(); throw error;} finally { await queryRunner.release();}缓存优化1. 查询缓存// 启用查询缓存const users = await userRepository.find({ cache: true,});// 自定义缓存时间const users = await userRepository.find({ cache: { id: 'users_list', milliseconds: 60000, // 60 秒 },});// 使用 QueryBuilder 缓存const users = await userRepository .createQueryBuilder('user') .cache(true) .getMany();// 清除缓存await dataSource.queryResultCache?.remove(['users_list']);2. 应用级缓存// 使用内存缓存const cache = new Map<string, any>();async function getUserWithCache(userId: number) { const cacheKey = `user:${userId}`; // 尝试从缓存获取 if (cache.has(cacheKey)) { return cache.get(cacheKey); } // 从数据库获取 const user = await userRepository.findOne({ where: { id: userId }, }); // 存入缓存 cache.set(cacheKey, user); return user;}数据库连接优化1. 连接池配置const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, // 连接池配置 extra: { connectionLimit: 20, // 连接池大小 acquireTimeout: 60000, // 获取连接超时时间 timeout: 60000, // 查询超时时间 waitForConnections: true, // 等待可用连接 queueLimit: 100, // 队列限制 },});2. 读写分离// 主数据库(写操作)const writeDataSource = new DataSource({ type: 'mysql', name: 'write', host: 'write-db.example.com', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});// 从数据库(读操作)const readDataSource = new DataSource({ type: 'mysql', name: 'read', host: 'read-db.example.com', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});// 使用读写分离class UserRepository { async findById(id: number) { // 从从数据库读取 const repository = readDataSource.getRepository(User); return await repository.findOne({ where: { id } }); } async save(user: User) { // 写入主数据库 const repository = writeDataSource.getRepository(User); return await repository.save(user); }}监控和调优1. 查询日志分析// 启用查询日志const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: ['query', 'error', 'slow'], // 记录查询、错误和慢查询 maxQueryExecutionTime: 1000, // 慢查询阈值(毫秒)});// 自定义日志记录const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logger: new QueryLogger(),});class QueryLogger implements Logger { logQuery(query: string, parameters?: any[]) { console.log('Query:', query); console.log('Parameters:', parameters); } logQueryError(error: string, query: string, parameters?: any[]) { console.error('Query Error:', error); console.error('Query:', query); } logQuerySlow(time: number, query: string, parameters?: any[]) { console.warn('Slow Query:', query); console.warn('Execution Time:', time); }}2. 性能监控// 查询性能监控class QueryPerformanceMonitor { private queries: Map<string, number[]> = new Map(); trackQuery(query: string, executionTime: number) { if (!this.queries.has(query)) { this.queries.set(query, []); } this.queries.get(query)!.push(executionTime); } getAverageExecutionTime(query: string): number { const times = this.queries.get(query); if (!times || times.length === 0) { return 0; } const sum = times.reduce((a, b) => a + b, 0); return sum / times.length; } getSlowQueries(threshold: number = 1000): string[] { const slowQueries: string[] = []; for (const [query, times] of this.queries.entries()) { const avgTime = this.getAverageExecutionTime(query); if (avgTime > threshold) { slowQueries.push(query); } } return slowQueries; }}// 使用性能监控const monitor = new QueryPerformanceMonitor();const startTime = Date.now();const users = await userRepository.find();const executionTime = Date.now() - startTime;monitor.trackQuery('SELECT * FROM user', executionTime);最佳实践1. 避免 N+1 查询// ❌ 不好的做法:N+1 查询const users = await userRepository.find();for (const user of users) { const posts = await user.posts; // N+1 查询}// ✅ 好的做法:使用急切加载const users = await userRepository.find({ relations: ['posts'],});// ✅ 好的做法:使用 joinconst users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();2. 使用适当的数据类型// ✅ 好的做法:使用适当的数据类型@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 255 }) // 限制长度 name: string; @Column({ type: 'int' }) // 使用整数 age: number; @Column({ type: 'decimal', precision: 10, scale: 2 }) // 使用精确小数 balance: number; @Column({ type: 'boolean' }) // 使用布尔值 isActive: boolean; @Column({ type: 'enum', enum: ['active', 'inactive'] }) // 使用枚举 status: string; @Column({ type: 'json' }) // 使用 JSON metadata: any;}3. 定期维护// 定期清理数据async function cleanupOldData() { const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); await userRepository.delete({ createdAt: LessThan(thirtyDaysAgo), status: 'deleted', });}// 定期重建索引async function rebuildIndexes() { await dataSource.query('ANALYZE TABLE user'); await dataSource.query('OPTIMIZE TABLE user');}// 定期更新统计信息async function updateStatistics() { await dataSource.query('ANALYZE TABLE user');}TypeORM 的性能优化需要综合考虑查询、关联、批量操作、事务等多个方面,合理使用优化技巧可以显著提高应用性能。
阅读 0·2月18日 22:18

TypeORM 如何进行测试?包括单元测试、集成测试和 Mock 策略

测试是保证代码质量的重要环节,TypeORM 提供了多种测试策略和工具,让开发者能够轻松编写和运行测试。测试环境配置使用内存数据库import { DataSource } from 'typeorm';import { User } from '../entity/User';// 使用 SQLite 内存数据库进行测试export const testDataSource = new DataSource({ type: 'sqlite', database: ':memory:', // 内存数据库 entities: [User], synchronize: true, // 自动同步表结构 logging: false, dropSchema: true, // 每次测试前清空数据库});// 测试套件设置beforeAll(async () => { await testDataSource.initialize();});afterAll(async () => { await testDataSource.destroy();});// 每个测试前清空数据库beforeEach(async () => { await testDataSource.synchronize(true);});使用测试数据库// 使用独立的测试数据库export const testDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'test_user', password: 'test_password', database: 'test_db', // 测试数据库 entities: [User, Post], synchronize: true, logging: false,});// 在 package.json 中配置测试脚本{ "scripts": { "test": "NODE_ENV=test jest", "test:watch": "NODE_ENV=test jest --watch", "test:coverage": "NODE_ENV=test jest --coverage" }}单元测试测试实体import { User } from '../entity/User';describe('User Entity', () => { it('should create a user with valid data', () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; expect(user.name).toBe('John Doe'); expect(user.email).toBe('john@example.com'); expect(user.age).toBe(25); }); it('should validate email format', () => { const user = new User(); user.email = 'invalid-email'; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; expect(emailRegex.test(user.email)).toBe(false); }); it('should validate age range', () => { const user = new User(); user.age = 15; expect(user.age).toBeLessThan(18); });});测试 Repositoryimport { DataSource } from 'typeorm';import { User } from '../entity/User';import { testDataSource } from './testDataSource';describe('User Repository', () => { let userRepository: any; beforeAll(async () => { await testDataSource.initialize(); userRepository = testDataSource.getRepository(User); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { // 清空数据库 await userRepository.clear(); }); it('should create a user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); expect(savedUser.id).toBeDefined(); expect(savedUser.name).toBe('John Doe'); expect(savedUser.email).toBe('john@example.com'); }); it('should find user by id', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); const foundUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(foundUser).toBeDefined(); expect(foundUser.name).toBe('John Doe'); }); it('should find users by email', async () => { await userRepository.save({ name: 'John Doe', email: 'john@example.com', age: 25, }); const users = await userRepository.find({ where: { email: 'john@example.com' }, }); expect(users.length).toBe(1); expect(users[0].name).toBe('John Doe'); }); it('should update user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); savedUser.name = 'Jane Doe'; await userRepository.save(savedUser); const updatedUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(updatedUser.name).toBe('Jane Doe'); }); it('should delete user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); await userRepository.delete(savedUser.id); const deletedUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(deletedUser).toBeNull(); });});集成测试测试服务层import { UserService } from '../service/UserService';import { testDataSource } from './testDataSource';describe('UserService Integration Tests', () => { let userService: UserService; beforeAll(async () => { await testDataSource.initialize(); userService = new UserService(testDataSource); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { // 清空数据库 const userRepository = testDataSource.getRepository(User); await userRepository.clear(); }); it('should create user with validation', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; const user = await userService.createUser(userData); expect(user.id).toBeDefined(); expect(user.name).toBe('John Doe'); }); it('should throw error for invalid email', async () => { const userData = { name: 'John Doe', email: 'invalid-email', age: 25, }; await expect(userService.createUser(userData)).rejects.toThrow( 'Invalid email format' ); }); it('should find user by email', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; await userService.createUser(userData); const user = await userService.findByEmail('john@example.com'); expect(user).toBeDefined(); expect(user.name).toBe('John Doe'); });});测试复杂查询describe('Complex Query Tests', () => { let userRepository: any; let postRepository: any; beforeAll(async () => { await testDataSource.initialize(); userRepository = testDataSource.getRepository(User); postRepository = testDataSource.getRepository(Post); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { await userRepository.clear(); await postRepository.clear(); }); it('should find users with posts', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', }); const savedUser = await userRepository.save(user); await postRepository.save({ title: 'Post 1', author: savedUser, }); await postRepository.save({ title: 'Post 2', author: savedUser, }); const usersWithPosts = await userRepository.find({ relations: ['posts'], where: { id: savedUser.id }, }); expect(usersWithPosts[0].posts.length).toBe(2); }); it('should use query builder for complex queries', async () => { const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.age > :age', { age: 18 }) .orderBy('user.createdAt', 'DESC') .getMany(); expect(Array.isArray(users)).toBe(true); });});Mock 和 StubMock Repositoryimport { Repository } from 'typeorm';import { User } from '../entity/User';describe('UserService with Mock Repository', () => { let userService: UserService; let userRepositoryMock: jest.Mocked<Repository<User>>; beforeEach(() => { // 创建 Mock Repository userRepositoryMock = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), delete: jest.fn(), } as any; userService = new UserService(userRepositoryMock); }); it('should create user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; userRepositoryMock.create.mockReturnValue(userData); userRepositoryMock.save.mockResolvedValue({ id: 1, ...userData, }); const user = await userService.createUser(userData); expect(userRepositoryMock.create).toHaveBeenCalledWith(userData); expect(userRepositoryMock.save).toHaveBeenCalled(); expect(user.id).toBe(1); }); it('should find user by email', async () => { const user = { id: 1, name: 'John Doe', email: 'john@example.com', age: 25, }; userRepositoryMock.findOne.mockResolvedValue(user); const foundUser = await userService.findByEmail('john@example.com'); expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ where: { email: 'john@example.com' }, }); expect(foundUser).toEqual(user); });});Mock DataSourceimport { DataSource } from 'typeorm';describe('Service with Mock DataSource', () => { let dataSourceMock: jest.Mocked<DataSource>; let service: MyService; beforeEach(() => { dataSourceMock = { getRepository: jest.fn(), createQueryBuilder: jest.fn(), transaction: jest.fn(), } as any; service = new MyService(dataSourceMock); }); it('should use repository from data source', async () => { const repositoryMock = { find: jest.fn().mockResolvedValue([]), }; dataSourceMock.getRepository.mockReturnValue(repositoryMock); await service.findAll(); expect(dataSourceMock.getRepository).toHaveBeenCalledWith(User); expect(repositoryMock.find).toHaveBeenCalled(); });});事务测试测试事务回滚describe('Transaction Tests', () => { let dataSource: DataSource; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User, Account], synchronize: true, }); await dataSource.initialize(); }); afterAll(async () => { await dataSource.destroy(); }); it('should rollback transaction on error', async () => { const userRepository = dataSource.getRepository(User); const accountRepository = dataSource.getRepository(Account); // 创建初始用户 const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', }); await userRepository.save(user); const initialBalance = 1000; // 创建账户 const account = accountRepository.create({ userId: user.id, balance: initialBalance, }); await accountRepository.save(account); // 测试事务回滚 await expect( dataSource.transaction(async (manager) => { const userRepo = manager.getRepository(User); const accountRepo = manager.getRepository(Account); // 扣款 const account = await accountRepo.findOne({ where: { userId: user.id }, }); account.balance -= 500; await accountRepo.save(account); // 模拟错误 throw new Error('Transaction failed'); }) ).rejects.toThrow('Transaction failed'); // 验证事务已回滚 const finalAccount = await accountRepository.findOne({ where: { userId: user.id }, }); expect(finalAccount.balance).toBe(initialBalance); }); it('should commit transaction on success', async () => { const userRepository = dataSource.getRepository(User); const accountRepository = dataSource.getRepository(Account); const user = userRepository.create({ name: 'Jane Doe', email: 'jane@example.com', }); await userRepository.save(user); const initialBalance = 1000; const account = accountRepository.create({ userId: user.id, balance: initialBalance, }); await accountRepository.save(account); // 测试事务提交 await dataSource.transaction(async (manager) => { const accountRepo = manager.getRepository(Account); const account = await accountRepo.findOne({ where: { userId: user.id }, }); account.balance -= 500; await accountRepo.save(account); }); // 验证事务已提交 const finalAccount = await accountRepository.findOne({ where: { userId: user.id }, }); expect(finalAccount.balance).toBe(500); });});测试工具和库使用 Jest// jest.config.jsmodule.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], transform: { '^.+\\.ts$': 'ts-jest', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, },};// package.json{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }}使用 Supertest (API 测试)import request from 'supertest';import { app } from '../app';import { testDataSource } from './testDataSource';describe('User API Tests', () => { let server: any; beforeAll(async () => { await testDataSource.initialize(); server = app.listen(0); // 随机端口 }); afterAll(async () => { await testDataSource.destroy(); server.close(); }); beforeEach(async () => { const userRepository = testDataSource.getRepository(User); await userRepository.clear(); }); it('should create user via POST', async () => { const response = await request(server) .post('/api/users') .send({ name: 'John Doe', email: 'john@example.com', age: 25, }) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe('John Doe'); }); it('should get user by ID', async () => { const userRepository = testDataSource.getRepository(User); const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); const response = await request(server) .get(`/api/users/${savedUser.id}`) .expect(200); expect(response.body.id).toBe(savedUser.id); expect(response.body.name).toBe('John Doe'); }); it('should return 404 for non-existent user', async () => { const response = await request(server) .get('/api/users/999') .expect(404); expect(response.body).toHaveProperty('error'); });});测试最佳实践1. 隔离测试// ✅ 好的做法:每个测试独立describe('User Repository', () => { beforeEach(async () => { // 每个测试前清空数据库 await userRepository.clear(); }); it('test 1', async () => { // 独立的测试 }); it('test 2', async () => { // 不依赖 test 1 });});// ❌ 不好的做法:测试相互依赖describe('User Repository', () => { let userId: number; it('test 1', async () => { const user = await userRepository.save({ name: 'John' }); userId = user.id; }); it('test 2', async () => { // 依赖 test 1 的结果 const user = await userRepository.findOne({ where: { id: userId } }); });});2. 使用测试数据工厂// factory/UserFactory.tsexport class UserFactory { static create(overrides: Partial<User> = {}): User { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; Object.assign(user, overrides); return user; } static createMany(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, () => this.create(overrides)); }}// 在测试中使用describe('User Repository', () => { it('should create user', async () => { const user = UserFactory.create({ name: 'Jane Doe', email: 'jane@example.com', }); const savedUser = await userRepository.save(user); expect(savedUser.name).toBe('Jane Doe'); }); it('should create multiple users', async () => { const users = UserFactory.createMany(5); const savedUsers = await userRepository.save(users); expect(savedUsers.length).toBe(5); });});3. 测试边界情况describe('User Repository Edge Cases', () => { it('should handle empty results', async () => { const users = await userRepository.find({ where: { name: 'NonExistent' }, }); expect(users).toEqual([]); }); it('should handle null values', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: null, // 允许 null }); const savedUser = await userRepository.save(user); expect(savedUser.age).toBeNull(); }); it('should handle duplicate emails', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; await userRepository.save(userRepository.create(userData)); await expect( userRepository.save(userRepository.create(userData)) ).rejects.toThrow(); });});4. 使用测试覆盖率// 运行测试并生成覆盖率报告npm run test:coverage// 检查覆盖率报告// coverage/lcov-report/index.html// 设置覆盖率阈值// jest.config.jscoverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, },}TypeORM 的测试策略提供了全面的测试支持,合理使用测试工具和最佳实践可以确保代码质量和可靠性。
阅读 0·2月18日 22:17

TypeORM 的缓存机制如何工作?包括查询缓存、Redis 缓存和缓存策略

缓存是提高应用性能的重要手段,TypeORM 提供了多种缓存策略和机制,让开发者能够灵活地管理数据缓存。缓存类型1. 查询缓存TypeORM 提供了内置的查询缓存功能,可以缓存查询结果。import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, // 启用查询缓存 cache: { type: 'database', // 或 'redis', 'ioredis' duration: 30000, // 缓存持续时间(毫秒) options: { // 数据库缓存选项 tableName: 'query_cache', }, },});2. Redis 缓存使用 Redis 作为缓存存储:import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, cache: { type: 'redis', // 或 'ioredis' duration: 30000, options: { host: 'localhost', port: 6379, password: 'password', db: 0, }, },});3. 数据库缓存使用数据库表作为缓存存储:const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, cache: { type: 'database', duration: 30000, options: { tableName: 'query_cache', }, },});查询缓存使用基本查询缓存import { DataSource } from 'typeorm';// 启用缓存的查询const users = await dataSource .getRepository(User) .find({ cache: true, // 启用缓存 });// 自定义缓存时间const users = await dataSource .getRepository(User) .find({ cache: { id: 'users_list', // 缓存 ID milliseconds: 60000, // 缓存 60 秒 }, });QueryBuilder 缓存// 使用 QueryBuilder 启用缓存const users = await dataSource .getRepository(User) .createQueryBuilder('user') .cache(true) // 启用缓存 .getMany();// 自定义缓存配置const users = await dataSource .getRepository(User) .createQueryBuilder('user') .cache('user_list', 60000) // 缓存 ID 和时间 .where('user.age > :age', { age: 18 }) .getMany();关联查询缓存// 缓存关联查询const usersWithPosts = await dataSource .getRepository(User) .find({ relations: ['posts'], cache: true, });// 使用 QueryBuilder 缓存关联查询const usersWithPosts = await dataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .cache('users_with_posts', 30000) .getMany();缓存管理清除缓存// 清除特定查询缓存await dataSource .getRepository(User) .query('CLEAR CACHE');// 清除所有缓存await dataSource.queryResultCache?.clear();// 清除特定缓存 IDawait dataSource.queryResultCache?.remove(['users_list']);缓存失效策略// 在更新数据后清除相关缓存async function updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // 更新用户 await userRepository.update(userId, userData); // 清除相关缓存 await dataSource.queryResultCache?.remove([ 'users_list', 'user_details', ]);}// 使用事务和缓存清除async function updateUserWithCache(userId: number, userData: Partial<User>) { await dataSource.transaction(async (manager) => { const userRepository = manager.getRepository(User); // 更新用户 await userRepository.update(userId, userData); // 清除缓存 await manager.queryResultCache?.remove([ 'users_list', ]); });}实体缓存实体级缓存import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; // 实体缓存配置 @Column({ type: 'timestamp' }) cachedAt: Date;}// 使用实体缓存async function getUserWithCache(userId: number) { const cacheKey = `user:${userId}`; // 尝试从缓存获取 const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // 从数据库获取 const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // 存入缓存 await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user;}自定义缓存装饰器import { Cacheable } from './decorators/Cacheable';@Entity()export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; @Cacheable({ ttl: 30000 }) // 缓存 30 秒 async getDetails(): Promise<Product> { return this; }}// 使用自定义缓存装饰器const product = await productRepository.findOne({ where: { id: 1 } });const details = await product.getDetails();缓存策略读写分离缓存// 读操作使用缓存async function getUser(userId: number) { const cacheKey = `user:${userId}`; // 尝试从缓存读取 const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // 从数据库读取 const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // 存入缓存 await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user;}// 写操作清除缓存async function updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // 更新数据库 await userRepository.update(userId, userData); // 清除缓存 const cacheKey = `user:${userId}`; await dataSource.queryResultCache?.remove([cacheKey]);}分层缓存// 一级缓存:内存缓存const memoryCache = new Map<string, any>();// 二级缓存:Redis 缓存const redisCache = dataSource.queryResultCache;async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // 尝试一级缓存 if (memoryCache.has(cacheKey)) { return memoryCache.get(cacheKey); } // 尝试二级缓存 const cached = await redisCache?.get(cacheKey); if (cached) { const product = JSON.parse(cached); memoryCache.set(cacheKey, product); return product; } // 从数据库获取 const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // 存入二级缓存 await redisCache?.store(cacheKey, JSON.stringify(product), 60000); // 存入一级缓存 memoryCache.set(cacheKey, product); return product;}缓存预热// 应用启动时预热缓存async function warmupCache() { // 预热热门用户 const popularUsers = await dataSource .getRepository(User) .createQueryBuilder('user') .where('user.followersCount > :count', { count: 1000 }) .limit(100) .getMany(); for (const user of popularUsers) { const cacheKey = `user:${user.id}`; await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 300000 // 5 分钟 ); } // 预热热门文章 const popularPosts = await dataSource .getRepository(Post) .createQueryBuilder('post') .where('post.views > :views', { views: 1000 }) .limit(100) .getMany(); for (const post of popularPosts) { const cacheKey = `post:${post.id}`; await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(post), 300000 ); }}// 在应用启动时调用warmupCache();缓存最佳实践1. 选择合适的缓存策略// ✅ 好的做法:为不同类型的数据使用不同的缓存策略// 静态数据:长时间缓存const staticData = await dataSource .getRepository(Category) .find({ cache: { milliseconds: 3600000, // 1 小时 }, });// 动态数据:短时间缓存const dynamicData = await dataSource .getRepository(Post) .find({ cache: { milliseconds: 30000, // 30 秒 }, });// 实时数据:不缓存const realTimeData = await dataSource .getRepository(OnlineUser) .find({ cache: false, });2. 缓存键命名规范// ✅ 好的做法:使用清晰的缓存键命名const cacheKeys = { user: (id: number) => `user:${id}`, userList: (page: number) => `user_list:page:${page}`, userPosts: (userId: number) => `user:${userId}:posts`, popularPosts: () => `posts:popular`,};// 使用示例await dataSource.queryResultCache?.store( cacheKeys.user(1), JSON.stringify(user), 60000);3. 缓存失效时机// ✅ 好的做法:在数据变更时及时清除缓存class UserService { async updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // 更新数据 await userRepository.update(userId, userData); // 清除相关缓存 await this.clearUserCache(userId); } async clearUserCache(userId: number) { const cacheKeys = [ `user:${userId}`, `user:${userId}:posts`, `user:${userId}:comments`, ]; await dataSource.queryResultCache?.remove(cacheKeys); }}4. 监控缓存性能// 缓存性能监控class CacheMonitor { private hits = 0; private misses = 0; async get<T>(key: string): Promise<T | null> { const cached = await dataSource.queryResultCache?.get(key); if (cached) { this.hits++; return JSON.parse(cached); } this.misses++; return null; } getHitRate(): number { const total = this.hits + this.misses; return total > 0 ? this.hits / total : 0; } getStats() { return { hits: this.hits, misses: this.misses, hitRate: this.getHitRate(), }; }}// 使用监控const monitor = new CacheMonitor();async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // 尝试从缓存获取 const cached = await monitor.get<Product>(cacheKey); if (cached) { return cached; } // 从数据库获取 const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // 存入缓存 await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), 60000 ); return product;}// 定期输出缓存统计setInterval(() => { console.log('Cache Stats:', monitor.getStats());}, 60000);高级缓存技巧1. 缓存穿透防护// 使用空值缓存防止缓存穿透async function getUser(userId: number) { const cacheKey = `user:${userId}`; // 尝试从缓存获取 const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { const user = JSON.parse(cached); // 检查是否为空值标记 if (user === null) { return null; } return user; } // 从数据库获取 const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // 存入缓存(包括空值) await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user;}2. 缓存雪崩防护// 使用随机缓存时间防止缓存雪崩async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // 尝试从缓存获取 const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // 从数据库获取 const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // 使用随机缓存时间(30-60 秒) const cacheTime = 30000 + Math.random() * 30000; // 存入缓存 await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), cacheTime ); return product;}3. 缓存击穿防护// 使用互斥锁防止缓存击穿const locks = new Map<string, Promise<any>>();async function getHotProduct(productId: number) { const cacheKey = `product:${productId}`; // 尝试从缓存获取 const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // 检查是否有锁 if (locks.has(cacheKey)) { return await locks.get(cacheKey); } // 创建锁 const promise = (async () => { try { // 从数据库获取 const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // 存入缓存 await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), 60000 ); return product; } finally { // 释放锁 locks.delete(cacheKey); } })(); locks.set(cacheKey, promise); return await promise;}TypeORM 的缓存机制提供了强大的性能优化能力,合理使用缓存可以显著提高应用性能。
阅读 0·2月18日 22:17