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

面试题手册

TypeORM 如何进行性能优化?包括查询优化、缓存策略、批量操作等最佳实践

性能优化是 TypeORM 开发中的重要环节,合理的优化策略可以显著提升应用响应速度和数据库效率。本文将详细介绍 TypeORM 的各种性能优化技巧和最佳实践。查询优化1. 避免 N+1 查询问题N+1 查询是常见的性能问题,指执行 1 次主查询后,又对每个结果执行 N 次关联查询。// ❌ 不好的做法:N+1 查询const users = await userRepository.find();for (const user of users) { const posts = await postRepository.find({ where: { authorId: user.id } }); user.posts = posts;}// ✅ 好的做法:使用关联查询const users = await userRepository.find({ relations: ['posts']});// ✅ 更好的做法:使用 QueryBuilderconst users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();2. 选择性加载字段只查询需要的字段,减少数据传输量。// ❌ 查询所有字段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();3. 使用索引优化查询为常用查询条件添加数据库索引。@Entity()@Index(['email']) // 单列索引@Index(['status', 'createdAt']) // 复合索引export class User { @PrimaryGeneratedColumn() id: number; @Column() @Index() // 装饰器方式添加索引 email: string; @Column() status: string; @Column() createdAt: Date;}// 或者使用迁移创建索引public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createIndex( 'user', new TableIndex({ name: 'IDX_USER_EMAIL', columnNames: ['email'], isUnique: true, }) );}4. 使用 LIMIT 和 OFFSET 实现分页// 基本分页const page = 1;const pageSize = 10;const users = await userRepository.find({ skip: (page - 1) * pageSize, take: pageSize, order: { createdAt: 'DESC' }});// 使用 QueryBuilder 分页const users = await userRepository .createQueryBuilder('user') .skip((page - 1) * pageSize) .take(pageSize) .orderBy('user.createdAt', 'DESC') .getMany();// 获取总数和分页数据const [users, total] = await userRepository.findAndCount({ skip: (page - 1) * pageSize, take: pageSize,});5. 使用游标分页对于大数据集,游标分页比 OFFSET 分页更高效。// 基于游标的分页async function getUsersWithCursor(cursor: number, limit: number = 10) { const query = userRepository .createQueryBuilder('user') .orderBy('user.id', 'ASC') .limit(limit + 1); // 多取一条判断是否有下一页 if (cursor) { query.where('user.id > :cursor', { cursor }); } const users = await query.getMany(); const hasNextPage = users.length > limit; if (hasNextPage) { users.pop(); // 移除多取的一条 } return { data: users, nextCursor: hasNextPage ? users[users.length - 1].id : null, hasNextPage };}缓存优化1. 启用查询缓存// 在 DataSource 配置中启用缓存const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], cache: { type: 'redis', options: { host: 'localhost', port: 6379, }, duration: 30000, // 缓存 30 秒 },});// 在查询中使用缓存const users = await userRepository.find({ cache: { id: 'users_list', milliseconds: 60000, // 缓存 60 秒 }});// 使用 QueryBuilder 缓存const users = await userRepository .createQueryBuilder('user') .cache(60000) // 缓存 60 秒 .getMany();2. 实体缓存@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() @Cache() // 启用实体缓存 email: string;}// 查询时自动使用缓存const user = await userRepository.findOne({ where: { id: 1 }, cache: true});3. 自定义缓存策略class CacheService { private cache = new Map<string, { data: any, expires: number }>(); async get<T>(key: string): Promise<T | null> { const item = this.cache.get(key); if (!item) return null; if (Date.now() > item.expires) { this.cache.delete(key); return null; } return item.data; } async set<T>(key: string, data: T, ttl: number = 60000): Promise<void> { this.cache.set(key, { data, expires: Date.now() + ttl }); } async invalidate(key: string): Promise<void> { this.cache.delete(key); }}// 使用自定义缓存const cacheService = new CacheService();async function getUserWithCache(userId: number) { const cacheKey = `user:${userId}`; let user = await cacheService.get<User>(cacheKey); if (!user) { user = await userRepository.findOne({ where: { id: userId } }); await cacheService.set(cacheKey, user, 60000); // 缓存 60 秒 } return user;}批量操作优化1. 批量插入// ❌ 不好的做法:逐个插入for (const userData of usersData) { const user = userRepository.create(userData); await userRepository.save(user);}// ✅ 好的做法:批量插入const users = userRepository.create(usersData);await userRepository.save(users);// ✅ 更好的做法:使用 QueryBuilderawait userRepository .createQueryBuilder() .insert() .into(User) .values(usersData) .execute();// ✅ 最佳做法:使用原生 SQL 批量插入const values = usersData.map(u => `('${u.name}', '${u.email}')`).join(',');await userRepository.query( `INSERT INTO user (name, email) VALUES ${values}`);2. 批量更新// ❌ 不好的做法:逐个更新for (const user of users) { user.status = 'active'; await userRepository.save(user);}// ✅ 好的做法:使用 QueryBuilder 批量更新await userRepository .createQueryBuilder() .update(User) .set({ status: 'active' }) .where('id IN :ids', { ids: users.map(u => u.id) }) .execute();// ✅ 更好的做法:使用原生 SQLconst ids = users.map(u => u.id).join(',');await userRepository.query( `UPDATE user SET status = 'active' WHERE id IN (${ids})`);3. 批量删除// ❌ 不好的做法:逐个删除for (const user of users) { await userRepository.delete(user.id);}// ✅ 好的做法:使用 QueryBuilder 批量删除await userRepository .createQueryBuilder() .delete() .from(User) .where('id IN :ids', { ids: users.map(u => u.id) }) .execute();// ✅ 更好的做法:使用原生 SQLconst ids = users.map(u => u.id).join(',');await userRepository.query( `DELETE FROM user WHERE id IN (${ids})`);连接池优化1. 配置连接池const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], extra: { connectionLimit: 10, // 最大连接数 acquireTimeout: 60000, // 获取连接超时时间 timeout: 60000, // 查询超时时间 waitForConnections: true, // 等待可用连接 queueLimit: 0, // 队列限制,0 表示无限制 },});2. 监控连接池状态class ConnectionPoolMonitor { constructor(private dataSource: DataSource) {} getPoolStats() { const pool = this.dataSource.driver.master; return { totalConnections: pool.pool._allConnections.length, freeConnections: pool.pool._freeConnections.length, queuedRequests: pool.pool._connectionQueue.length, }; } logPoolStats() { const stats = this.getPoolStats(); console.log('Connection Pool Stats:', stats); }}// 定期监控连接池const monitor = new ConnectionPoolMonitor(dataSource);setInterval(() => monitor.logPoolStats(), 60000);懒加载优化1. 使用懒加载@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Promise<Post[]>; // 使用 Promise 实现懒加载}// 查询时不会加载关联数据const user = await userRepository.findOne({ where: { id: 1 } });// 需要时才加载const posts = await user.posts;2. 控制懒加载深度// 设置最大懒加载深度const dataSource = new DataSource({ type: 'mysql', // ... 其他配置 maxQueryExecutionTime: 1000, // 最大查询执行时间});查询分析和监控1. 启用查询日志const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], logging: true, // 启用日志 maxQueryExecutionTime: 1000, // 记录超过 1 秒的查询});2. 使用查询分析器class QueryAnalyzer { private queries: Map<string, { count: number, totalTime: number }> = new Map(); logQuery(query: string, duration: number) { const key = query; if (!this.queries.has(key)) { this.queries.set(key, { count: 0, totalTime: 0 }); } const stats = this.queries.get(key); stats.count++; stats.totalTime += duration; } getSlowQueries(threshold: number = 1000) { const slowQueries: Array<{ query: string, avgTime: number, count: number }> = []; for (const [query, stats] of this.queries.entries()) { const avgTime = stats.totalTime / stats.count; if (avgTime > threshold) { slowQueries.push({ query, avgTime, count: stats.count }); } } return slowQueries.sort((a, b) => b.avgTime - a.avgTime); }}3. 使用 EXPLAIN 分析查询async function analyzeQuery(query: string) { const result = await dataSource.query(`EXPLAIN ${query}`); console.log('Query Analysis:', result); // 检查是否使用了索引 const usingIndex = result.some(row => row.type === 'ref' || row.type === 'eq_ref'); if (!usingIndex) { console.warn('Query not using index:', query); }}// 使用示例analyzeQuery('SELECT * FROM user WHERE email = "test@example.com"');数据库优化1. 优化表结构@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 100 }) // 指定合适的长度 name: string; @Column({ type: 'varchar', length: 255, unique: true }) email: string; @Column({ type: 'tinyint', default: 1 }) // 使用 tinyint 代替 boolean isActive: boolean; @Column({ type: 'int', unsigned: true }) // 使用 unsigned 节省空间 age: number; @CreateDateColumn({ type: 'timestamp' }) createdAt: Date;}2. 使用合适的数据类型// ✅ 好的做法:使用合适的数据类型@Column({ type: 'tinyint' })status: number; // 0-255@Column({ type: 'smallint' })score: number; // -32768 到 32767@Column({ type: 'int' })count: number; // -2147483648 到 2147483647@Column({ type: 'bigint' })largeNumber: number; // 大整数@Column({ type: 'decimal', precision: 10, scale: 2 })price: number; // 金额使用 decimal3. 定期维护数据库// 定期优化表async function optimizeTables() { const tables = ['user', 'post', 'comment']; for (const table of tables) { await dataSource.query(`OPTIMIZE TABLE ${table}`); }}// 定期分析表async function analyzeTables() { const tables = ['user', 'post', 'comment']; for (const table of tables) { await dataSource.query(`ANALYZE TABLE ${table}`); }}性能测试和基准测试1. 查询性能测试import { performance } from 'perf_hooks';async function benchmarkQuery( name: string, query: () => Promise<any>, iterations: number = 100) { const times: number[] = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); await query(); const end = performance.now(); times.push(end - start); } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; const minTime = Math.min(...times); const maxTime = Math.max(...times); console.log(`${name}:`); console.log(` Average: ${avgTime.toFixed(2)}ms`); console.log(` Min: ${minTime.toFixed(2)}ms`); console.log(` Max: ${maxTime.toFixed(2)}ms`);}// 使用示例await benchmarkQuery('Find users with relations', async () => { await userRepository.find({ relations: ['posts'] });});最佳实践总结避免 N+1 查询: 使用关联查询或 QueryBuilder选择性加载字段: 只查询需要的字段使用索引: 为常用查询条件添加索引实现分页: 使用 LIMIT 和 OFFSET 或游标分页启用缓存: 对不常变化的数据使用缓存批量操作: 使用批量插入、更新、删除优化连接池: 合理配置连接池参数监控性能: 启用日志,分析慢查询优化表结构: 使用合适的数据类型和长度定期维护: 优化和分析数据库表通过以上优化策略,可以显著提升 TypeORM 应用的性能,提供更好的用户体验。
阅读 0·2月18日 22:12

TypeORM 的实体继承如何实现?包括单表继承、类表继承和具体表继承

实体继承是面向对象编程的重要特性,TypeORM 提供了多种继承模式,让开发者能够更好地组织和管理实体类。继承模式概述TypeORM 支持三种主要的继承模式:单表继承 (Single Table Inheritance)类表继承 (Class Table Inheritance)具体表继承 (Concrete Table Inheritance)单表继承 (Single Table Inheritance)基本概念单表继承将所有子类的数据存储在同一个表中,使用鉴别列(discriminator column)来区分不同的子类。实现示例import { Entity, PrimaryGeneratedColumn, Column, ChildEntity, DiscriminatorColumn, DiscriminatorValue} from 'typeorm';@Entity()@DiscriminatorColumn({ name: 'type' })export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column({ type: 'text' }) body: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date;}@ChildEntity()@DiscriminatorValue('article')export class Article extends Content { @Column() author: string; @Column() category: string;}@ChildEntity()@DiscriminatorValue('video')export class Video extends Content { @Column() url: string; @Column() duration: number;}@ChildEntity()@DiscriminatorValue('podcast')export class Podcast extends Content { @Column() audioUrl: string; @Column() episodeNumber: number;}使用示例// 查询所有内容const allContent = await dataSource.getRepository(Content).find();// 查询特定类型的内容const articles = await dataSource.getRepository(Article).find();const videos = await dataSource.getRepository(Video).find();// 创建不同类型的内容const article = new Article();article.title = 'TypeORM Tutorial';article.body = 'Content...';article.author = 'John Doe';article.category = 'Programming';const video = new Video();video.title = 'TypeORM Video Guide';video.body = 'Video content...';video.url = 'https://example.com/video';video.duration = 600;await dataSource.getRepository(Content).save([article, video]);优缺点优点:查询性能好,所有数据在一个表中简单的关联关系适合子类字段差异不大的情况缺点:表可能有很多 NULL 列不适合子类字段差异很大的情况添加新子类需要修改表结构类表继承 (Class Table Inheritance)基本概念类表继承为每个类创建单独的表,子类表通过外键关联到父类表。实现示例import { Entity, PrimaryGeneratedColumn, Column, ChildEntity, TableInheritance} from 'typeorm';@Entity()@TableInheritance({ column: { type: 'varchar', name: 'type' } })export class Person { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column({ type: 'date' }) birthDate: Date;}@ChildEntity()export class Employee extends Person { @Column() position: string; @Column() department: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) salary: number;}@ChildEntity()export class Customer extends Person { @Column() companyName: string; @Column() industry: string; @Column({ type: 'int' }) loyaltyPoints: number;}使用示例// 查询所有人员const allPeople = await dataSource.getRepository(Person).find();// 查询特定类型的人员const employees = await dataSource.getRepository(Employee).find();const customers = await dataSource.getRepository(Customer).find();// 创建不同类型的人员const employee = new Employee();employee.name = 'John Doe';employee.email = 'john@example.com';employee.birthDate = new Date('1990-01-01');employee.position = 'Developer';employee.department = 'IT';employee.salary = 75000;const customer = new Customer();customer.name = 'Jane Smith';customer.email = 'jane@example.com';customer.birthDate = new Date('1985-05-15');customer.companyName = 'Acme Corp';customer.industry = 'Technology';customer.loyaltyPoints = 1000;await dataSource.getRepository(Person).save([employee, customer]);优缺点优点:数据规范化,避免 NULL 列每个子类有独立的表适合子类字段差异大的情况缺点:查询需要 JOIN,性能可能较差复杂的关联关系添加新子类需要创建新表具体表继承 (Concrete Table Inheritance)基本概念具体表继承为每个具体子类创建独立的表,不创建父类表。实现示例import { Entity, PrimaryGeneratedColumn, Column, AbstractEntity} from 'typeorm';@AbstractEntity()export abstract class Vehicle { @PrimaryGeneratedColumn() id: number; @Column() brand: string; @Column() model: string; @Column({ type: 'int' }) year: number; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number;}@Entity()export class Car extends Vehicle { @Column() fuelType: string; @Column({ type: 'int' }) numberOfDoors: number; @Column({ type: 'boolean' }) isConvertible: boolean;}@Entity()export class Motorcycle extends Vehicle { @Column() engineType: string; @Column({ type: 'int' }) displacement: number; @Column({ type: 'boolean' }) hasSidecar: boolean;}使用示例// 查询特定类型的车辆const cars = await dataSource.getRepository(Car).find();const motorcycles = await dataSource.getRepository(Motorcycle).find();// 创建不同类型的车辆const car = new Car();car.brand = 'Toyota';car.model = 'Camry';car.year = 2024;car.price = 35000;car.fuelType = 'Hybrid';car.numberOfDoors = 4;car.isConvertible = false;const motorcycle = new Motorcycle();motorcycle.brand = 'Harley-Davidson';motorcycle.model = 'Street 750';motorcycle.year = 2024;motorcycle.price = 8000;motorcycle.engineType = 'V-Twin';motorcycle.displacement = 750;motorcycle.hasSidecar = false;await Promise.all([ dataSource.getRepository(Car).save(car), dataSource.getRepository(Motorcycle).save(motorcycle)]);优缺点优点:每个表都是完整的,没有 NULL 列查询性能好适合子类完全独立的情况缺点:父类字段在所有子类表中重复难以查询所有子类的数据不适合需要统一查询父类的情况继承与关系在继承中使用关系@Entity()@DiscriminatorColumn({ name: 'type' })export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @OneToMany(() => Comment, comment => comment.content) comments: Comment[];}@ChildEntity()@DiscriminatorValue('article')export class Article extends Content { @Column() author: string; @Column() category: string;}@Entity()export class Comment { @PrimaryGeneratedColumn() id: number; @Column() text: string; @ManyToOne(() => Content, content => content.comments) content: Content;}// 使用示例const article = await dataSource.getRepository(Article).findOne({ where: { id: 1 }, relations: ['comments']});多态关系@Entity()export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() entityType: string; // 'article' or 'video' @Column() entityId: number;}@Entity()@DiscriminatorColumn({ name: 'type' })export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @OneToMany(() => Tag, tag => tag.entityId = this.id && tag.entityType = 'article' ) tags: Tag[];}@ChildEntity()@DiscriminatorValue('article')export class Article extends Content { @Column() author: string;}@ChildEntity()@DiscriminatorValue('video')export class Video extends Content { @Column() url: string;}继承与查询查询所有子类// 单表继承:查询所有内容const allContent = await dataSource.getRepository(Content).find();// 类表继承:查询所有人员const allPeople = await dataSource.getRepository(Person).find({ relations: true // 加载子类数据});// 具体表继承:需要分别查询const cars = await dataSource.getRepository(Car).find();const motorcycles = await dataSource.getRepository(Motorcycle).find();按类型过滤// 使用鉴别器值过滤const articles = await dataSource.getRepository(Content).find({ where: { type: 'article' as any }});// 或者使用子类 Repositoryconst articles = await dataSource.getRepository(Article).find();复杂查询// 查询特定类型的关联数据const articlesWithComments = await dataSource .getRepository(Article) .createQueryBuilder('article') .leftJoinAndSelect('article.comments', 'comment') .where('article.category = :category', { category: 'Technology' }) .getMany();// 使用 QueryBuilder 按类型查询const contents = await dataSource .getRepository(Content) .createQueryBuilder('content') .where('content.type IN (:...types)', { types: ['article', 'video'] }) .getMany();继承与迁移单表继承迁移import { MigrationInterface, QueryRunner, Table } from 'typeorm';export class CreateContentTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'content', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'title', type: 'varchar', }, { name: 'body', type: 'text', }, { name: 'type', // 鉴别列 type: 'varchar', default: "'article'", }, { name: 'author', // Article 特有字段 type: 'varchar', isNullable: true, }, { name: 'category', // Article 特有字段 type: 'varchar', isNullable: true, }, { name: 'url', // Video 特有字段 type: 'varchar', isNullable: true, }, { name: 'duration', // Video 特有字段 type: 'int', isNullable: true, }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('content'); }}类表继承迁移export class CreatePersonTables1234567890124 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 创建父类表 await queryRunner.createTable( new Table({ name: 'person', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, { name: 'email', type: 'varchar', }, { name: 'birthDate', type: 'date', }, { name: 'type', // 鉴别列 type: 'varchar', }, ], }), true ); // 创建子类表 await queryRunner.createTable( new Table({ name: 'employee', columns: [ { name: 'id', type: 'int', isPrimary: true, }, { name: 'position', type: 'varchar', }, { name: 'department', type: 'varchar', }, { name: 'salary', type: 'decimal', precision: 10, scale: 2, }, ], foreignKeys: [ { columnNames: ['id'], referencedColumnNames: ['id'], referencedTableName: 'person', onDelete: 'CASCADE', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('employee'); await queryRunner.dropTable('person'); }}最佳实践选择合适的继承模式// ✅ 单表继承:子类字段差异小@Entity()@DiscriminatorColumn({ name: 'type' })export class User { @PrimaryGeneratedColumn() id: number; @Column() username: string; @Column() email: string;}@ChildEntity()@DiscriminatorValue('admin')export class Admin extends User { @Column() permissions: string;}// ✅ 类表继承:子类字段差异大@Entity()@TableInheritance({ column: { type: 'varchar', name: 'type' } })export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number;}@ChildEntity()export class PhysicalProduct extends Product { @Column() weight: number; @Column() dimensions: string;}@ChildEntity()export class DigitalProduct extends Product { @Column() downloadUrl: string; @Column() fileSize: number;}// ✅ 具体表继承:子类完全独立@AbstractEntity()export abstract class Notification { @PrimaryGeneratedColumn() id: number; @Column() message: string; @Column({ type: 'timestamp' }) createdAt: Date;}@Entity()export class EmailNotification extends Notification { @Column() recipientEmail: string; @Column() subject: string;}@Entity()export class SMSNotification extends Notification { @Column() phoneNumber: string; @Column() sender: string;}避免深层继承// ❌ 不好的做法:深层继承@Entity()@DiscriminatorColumn({ name: 'type' })export class Animal { @PrimaryGeneratedColumn() id: number; @Column() name: string;}@ChildEntity()@DiscriminatorValue('mammal')export class Mammal extends Animal { @Column() furColor: string;}@ChildEntity()@DiscriminatorValue('dog')export class Dog extends Mammal { @Column() breed: string;}// ✅ 好的做法:扁平继承结构@Entity()@DiscriminatorColumn({ name: 'type' })export class Animal { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ nullable: true }) furColor: string; @Column({ nullable: true }) breed: string;}@ChildEntity()@DiscriminatorValue('dog')export class Dog extends Animal { // Dog 特有的属性}@ChildEntity()@DiscriminatorValue('cat')export class Cat extends Animal { // Cat 特有的属性}TypeORM 的实体继承功能提供了强大的面向对象编程支持,合理使用继承可以提高代码的可维护性和可扩展性。
阅读 0·2月18日 22:10

TypeORM 如何实现软删除?包括软删除的配置、操作和最佳实践

软删除是一种数据删除策略,它不会真正从数据库中删除记录,而是通过标记记录为"已删除"状态来隐藏它们。TypeORM 提供了内置的软删除支持,使得实现软删除变得简单而强大。软删除基础概念什么是软删除软删除是一种数据保留策略,当删除记录时:不会真正从数据库中删除记录只是将记录标记为"已删除"状态查询时默认不包含已删除的记录可以随时恢复已删除的记录软删除 vs 硬删除| 特性 | 软删除 | 硬删除 || ---- | --------- | -------- || 数据保留 | 保留在数据库中 | 从数据库中删除 || 可恢复性 | 可以恢复 | 无法恢复 || 存储空间 | 占用存储空间 | 释放存储空间 || 查询性能 | 需要过滤已删除记录 | 查询性能更好 || 审计追踪 | 保留删除历史 | 无法追踪删除历史 |TypeORM 软删除实现基本软删除配置import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; // 软删除列:当记录被软删除时,此列会被设置为当前时间 @DeleteDateColumn() deletedAt: Date | null;}使用软删除import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: true,});// 软删除用户await dataSource.manager.softRemove(user);// 或者使用 Repositoryawait userRepository.softRemove(user);// 恢复已删除的用户await dataSource.manager.recover(user);// 或者使用 Repositoryawait userRepository.recover(user);// 硬删除用户(真正删除)await dataSource.manager.remove(user);// 或者使用 Repositoryawait userRepository.remove(user);软删除操作软删除记录// 方式1:使用 softRemoveconst user = await userRepository.findOne({ where: { id: 1 } });await userRepository.softRemove(user);// 方式2:使用 softDeleteawait userRepository.softDelete(1);// 方式3:使用 QueryBuilderawait userRepository .createQueryBuilder() .softDelete() .where('id = :id', { id: 1 }) .execute();// 批量软删除await userRepository.softDelete([1, 2, 3]);恢复已删除记录// 方式1:使用 recoverconst user = await userRepository.findOne({ where: { id: 1 }, withDeleted: true // 包含已删除的记录});await userRepository.recover(user);// 方式2:使用 restoreawait userRepository.restore(1);// 方式3:使用 QueryBuilderawait userRepository .createQueryBuilder() .restore() .where('id = :id', { id: 1 }) .execute();// 批量恢复await userRepository.restore([1, 2, 3]);查询已删除记录// 查询所有记录(包括已删除的)const allUsers = await userRepository.find({ withDeleted: true});// 只查询已删除的记录const deletedUsers = await userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) }});// 使用 QueryBuilder 查询已删除记录const deletedUsers = await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt IS NOT NULL') .getMany();软删除与关系级联软删除@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date | null; @OneToMany(() => Post, post => post.author, { cascade: true, // 级联操作 }) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @DeleteDateColumn() deletedAt: Date | null; @ManyToOne(() => User, user => user.posts) author: User;}// 软删除用户时,级联软删除所有文章const user = await userRepository.findOne({ where: { id: 1 }, relations: ['posts']});await userRepository.softRemove(user);查询关联数据时处理软删除// 查询用户及其未删除的文章const users = await userRepository.find({ relations: ['posts'], where: { deletedAt: IsNull() // 只查询未删除的用户 }});// 使用 QueryBuilder 查询const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.deletedAt IS NULL') .where('user.deletedAt IS NULL') .getMany();高级软删除用法自定义软删除列@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; // 使用自定义列名和类型 @DeleteDateColumn({ name: 'deleted_at', type: 'timestamp', nullable: true }) deletedAt: Date | null; // 或者使用布尔标记 @Column({ name: 'is_deleted', type: 'boolean', default: false }) isDeleted: boolean; @Column({ name: 'deleted_by', type: 'int', nullable: true }) deletedBy: number | null;}软删除审计@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date | null; @Column({ nullable: true }) deletedBy: number | null; // 删除操作者 ID @Column({ type: 'text', nullable: true }) deleteReason: string | null; // 删除原因}// 软删除时记录审计信息async function softDeleteWithAudit(userId: number, deletedBy: number, reason: string) { const user = await userRepository.findOne({ where: { id: userId } }); if (!user) { throw new Error('User not found'); } user.deletedAt = new Date(); user.deletedBy = deletedBy; user.deleteReason = reason; await userRepository.save(user);}软删除订阅者import { EntitySubscriberInterface, EventSubscriber, SoftRemoveEvent } from 'typeorm';@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async beforeSoftRemove(event: SoftRemoveEvent<User>) { console.log('Before soft remove user:', event.entity); // 检查是否可以软删除 if (event.entity.hasActiveOrders()) { throw new Error('Cannot delete user with active orders'); } } async afterSoftRemove(event: SoftRemoveEvent<User>) { console.log('After soft remove user:', event.entity); // 记录审计日志 await this.logAudit('SOFT_DELETE', event.entity); // 发送通知 await this.sendNotification(event.entity); } async beforeRecover(event: any) { console.log('Before recover user:', event.entity); // 检查是否可以恢复 if (event.entity.emailConflict()) { throw new Error('Cannot recover user due to email conflict'); } } async afterRecover(event: any) { console.log('After recover user:', event.entity); // 记录恢复日志 await this.logAudit('RECOVER', event.entity); } private async logAudit(action: string, user: User) { // 记录审计日志的逻辑 console.log(`Audit log: ${action} user ${user.id}`); } private async sendNotification(user: User) { // 发送通知的逻辑 console.log(`Sending notification for user ${user.id}`); }}软删除与查询构建器在 QueryBuilder 中使用软删除// 查询未删除的记录(默认行为)const users = await userRepository .createQueryBuilder('user') .getMany();// 查询所有记录(包括已删除的)const allUsers = await userRepository .createQueryBuilder('user') .withDeleted() .getMany();// 只查询已删除的记录const deletedUsers = await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt IS NOT NULL') .getMany();// 软删除记录await userRepository .createQueryBuilder('user') .softDelete() .where('user.id = :id', { id: 1 }) .execute();// 恢复记录await userRepository .createQueryBuilder('user') .restore() .where('user.id = :id', { id: 1 }) .execute();复杂查询中的软删除处理// 查询用户及其未删除的文章const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.deletedAt IS NULL') .where('user.deletedAt IS NULL') .getMany();// 统计未删除和已删除的用户数量const [activeCount, deletedCount] = await Promise.all([ userRepository.count({ where: { deletedAt: IsNull() } }), userRepository.count({ withDeleted: true, where: { deletedAt: Not(IsNull()) } })]);console.log(`Active users: ${activeCount}, Deleted users: ${deletedCount}`);软删除最佳实践1. 合理使用软删除// ✅ 好的做法:对重要数据使用软删除@Entity()export class User { @DeleteDateColumn() deletedAt: Date | null;}@Entity()export class Order { @DeleteDateColumn() deletedAt: Date | null;}// ✅ 好的做法:对临时数据使用硬删除@Entity()export class TempFile { // 不使用软删除,直接硬删除}// ❌ 不好的做法:对所有数据都使用软删除// 这会导致数据库膨胀和查询性能下降2. 定期清理已删除记录// 定期清理超过一定时间的已删除记录async function cleanupOldDeletedRecords() { const retentionDays = 90; // 保留 90 天 const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); // 真正删除超过保留期的记录 await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt < :cutoffDate', { cutoffDate }) .delete() .execute();}// 使用定时任务定期执行// setInterval(cleanupOldDeletedRecords, 24 * 60 * 60 * 1000); // 每天执行一次3. 处理唯一约束冲突@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @DeleteDateColumn() deletedAt: Date | null;}// 软删除用户后,可能需要恢复async function recoverUser(userId: number) { const user = await userRepository.findOne({ where: { id: userId }, withDeleted: true }); if (!user) { throw new Error('User not found'); } // 检查邮箱是否已被使用 const existingUser = await userRepository.findOne({ where: { email: user.email } }); if (existingUser) { throw new Error('Email already in use'); } await userRepository.recover(user);}4. 软删除与外键约束@Entity()export class User { @PrimaryGeneratedColumn() id: number; @DeleteDateColumn() deletedAt: Date | null; @OneToMany(() => Post, post => post.author) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @DeleteDateColumn() deletedAt: Date | null; @ManyToOne(() => User, user => user.posts) @JoinColumn({ onDelete: 'SET NULL' }) // 软删除时设置为 NULL author: User | null;}5. 软删除与索引@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() @Index() // 为软删除列添加索引,提高查询性能 deletedAt: Date | null;}// 或者使用复合索引@Entity()@Index(['deletedAt', 'createdAt']) // 复合索引export class User { @DeleteDateColumn() deletedAt: Date | null; @CreateDateColumn() createdAt: Date;}软删除性能优化1. 使用索引优化查询@Entity()export class User { @DeleteDateColumn() @Index('IDX_USER_DELETED_AT') // 为软删除列添加索引 deletedAt: Date | null;}// 查询未删除的用户时会使用索引const users = await userRepository.find({ where: { deletedAt: IsNull() }});2. 避免过度使用 withDeleted// ❌ 不好的做法:总是查询所有记录const users = await userRepository.find({ withDeleted: true });// ✅ 好的做法:只在需要时查询已删除记录const users = await userRepository.find();const deletedUsers = await userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) }});3. 使用 QueryBuilder 优化复杂查询// ✅ 好的做法:使用 QueryBuilder 优化查询const users = await userRepository .createQueryBuilder('user') .where('user.deletedAt IS NULL') .orderBy('user.createdAt', 'DESC') .limit(10) .getMany();TypeORM 的软删除功能提供了强大的数据管理能力,合理使用软删除可以保护重要数据,提供审计追踪,同时保持应用的灵活性。但需要注意性能影响和定期清理策略。
阅读 0·2月18日 22:09

TypeORM 如何支持多种数据库?包括 MySQL、PostgreSQL、MongoDB 等的配置和使用

TypeORM 支持多种数据库类型,包括 MySQL、PostgreSQL、SQLite、SQL Server、Oracle 和 MongoDB。本文将详细介绍 TypeORM 如何配置和使用多个数据库。支持的数据库类型关系型数据库MySQL / MariaDBimport { DataSource } from 'typeorm';const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});PostgreSQLconst postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});SQLiteconst sqliteDataSource = new DataSource({ type: 'sqlite', database: './myapp.db', entities: [User, Post], synchronize: false, logging: true,});SQL Serverconst mssqlDataSource = new DataSource({ type: 'mssql', host: 'localhost', port: 1433, username: 'sa', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});Oracleconst oracleDataSource = new DataSource({ type: 'oracle', host: 'localhost', port: 1521, username: 'system', password: 'password', serviceName: 'ORCL', entities: [User, Post], synchronize: false, logging: true,});NoSQL 数据库MongoDBconst mongoDataSource = new DataSource({ type: 'mongodb', host: 'localhost', port: 27017, database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});多数据库配置配置多个 DataSourceimport { DataSource } from 'typeorm';// 主数据库 (MySQL)const primaryDataSource = new DataSource({ type: 'mysql', name: 'primary', // 数据源名称 host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'primary_db', entities: [User, Post], synchronize: false, logging: true,});// 从数据库 (PostgreSQL)const secondaryDataSource = new DataSource({ type: 'postgres', name: 'secondary', // 数据源名称 host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'secondary_db', entities: [Comment, Like], synchronize: false, logging: true,});// 缓存数据库 (Redis)const cacheDataSource = new DataSource({ type: 'mongodb', name: 'cache', host: 'localhost', port: 27017, database: 'cache_db', entities: [Cache], synchronize: false, logging: true,});初始化多个数据源async function initializeDataSources() { try { // 初始化所有数据源 await Promise.all([ primaryDataSource.initialize(), secondaryDataSource.initialize(), cacheDataSource.initialize(), ]); console.log('All data sources initialized successfully'); } catch (error) { console.error('Error initializing data sources:', error); throw error; }}// 使用示例initializeDataSources().then(() => { // 应用程序逻辑});跨数据库操作在不同数据库间操作// 从 MySQL 读取用户async function getUserFromPrimary(userId: number) { const userRepository = primaryDataSource.getRepository(User); return await userRepository.findOne({ where: { id: userId } });}// 从 PostgreSQL 读取评论async function getCommentsFromSecondary(postId: number) { const commentRepository = secondaryDataSource.getRepository(Comment); return await commentRepository.find({ where: { postId } });}// 写入 MongoDB 缓存async function cacheUserData(userId: number, data: any) { const cacheRepository = cacheDataSource.getRepository(Cache); const cache = cacheRepository.create({ key: `user:${userId}`, value: JSON.stringify(data), expiresAt: new Date(Date.now() + 3600000), // 1小时后过期 }); return await cacheRepository.save(cache);}// 组合使用async function getUserWithCache(userId: number) { // 先尝试从缓存读取 const cacheRepository = cacheDataSource.getRepository(Cache); const cached = await cacheRepository.findOne({ where: { key: `user:${userId}` } }); if (cached && cached.expiresAt > new Date()) { return JSON.parse(cached.value); } // 从主数据库读取 const user = await getUserFromPrimary(userId); // 写入缓存 await cacheUserData(userId, user); return user;}跨数据库事务// 使用分布式事务模式async function transferDataBetweenDatabases( userId: number, postId: number) { const primaryQueryRunner = primaryDataSource.createQueryRunner(); const secondaryQueryRunner = secondaryDataSource.createQueryRunner(); try { // 开始事务 await primaryQueryRunner.connect(); await primaryQueryRunner.startTransaction(); await secondaryQueryRunner.connect(); await secondaryQueryRunner.startTransaction(); // 在主数据库操作 const userRepository = primaryQueryRunner.manager.getRepository(User); const user = await userRepository.findOne({ where: { id: userId } }); // 在从数据库操作 const commentRepository = secondaryQueryRunner.manager.getRepository(Comment); const comment = commentRepository.create({ userId, postId, content: 'Comment from user', }); await commentRepository.save(comment); // 提交事务 await primaryQueryRunner.commitTransaction(); await secondaryQueryRunner.commitTransaction(); } catch (error) { // 回滚事务 await primaryQueryRunner.rollbackTransaction(); await secondaryQueryRunner.rollbackTransaction(); throw error; } finally { // 释放资源 await primaryQueryRunner.release(); await secondaryQueryRunner.release(); }}数据库特定配置MySQL 特定配置const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, // MySQL 特定配置 extra: { connectionLimit: 10, // 连接池大小 acquireTimeout: 60000, // 获取连接超时时间 timeout: 60000, // 查询超时时间 waitForConnections: true, // 等待可用连接 queueLimit: 0, // 队列限制 charset: 'utf8mb4', // 字符集 timezone: '+00:00', // 时区 ssl: { rejectUnauthorized: false } }, // MySQL 驱动选项 driverOptions: { flags: ['+FOUND_ROWS'], namedPlaceholders: true, },});PostgreSQL 特定配置const postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, // PostgreSQL 特定配置 extra: { max: 10, // 最大连接数 idleTimeoutMillis: 30000, // 空闲连接超时 connectionTimeoutMillis: 2000, // 连接超时 }, // PostgreSQL 驱动选项 driverOptions: { application_name: 'MyApp', statement_timeout: 10000, query_timeout: 10000, },});MongoDB 特定配置const mongoDataSource = new DataSource({ type: 'mongodb', host: 'localhost', port: 27017, database: 'myapp', entities: [User], synchronize: false, logging: true, // MongoDB 特定配置 url: 'mongodb://localhost:27017/myapp', useNewUrlParser: true, useUnifiedTopology: true, // MongoDB 驱动选项 driverOptions: { maxPoolSize: 10, minPoolSize: 2, maxIdleTimeMS: 60000, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, },});数据库迁移管理多数据库迁移// 主数据库迁移export class CreateUserTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('user'); }}// 从数据库迁移export class CreateCommentTable1234567890124 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'comment', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'content', type: 'text', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('comment'); }}运行特定数据库的迁移// 运行主数据库迁移async function runPrimaryMigrations() { await primaryDataSource.runMigrations();}// 运行从数据库迁移async function runSecondaryMigrations() { await secondaryDataSource.runMigrations();}// 运行所有数据库迁移async function runAllMigrations() { await Promise.all([ primaryDataSource.runMigrations(), secondaryDataSource.runMigrations(), cacheDataSource.runMigrations(), ]);}数据库切换策略环境变量配置// config/database.tsimport { DataSource } from 'typeorm';export function createDataSource(): DataSource { const dbType = process.env.DB_TYPE || 'mysql'; const commonConfig = { entities: [User, Post], synchronize: false, logging: true, }; switch (dbType) { case 'mysql': return new DataSource({ type: 'mysql', host: process.env.MYSQL_HOST || 'localhost', port: parseInt(process.env.MYSQL_PORT || '3306'), username: process.env.MYSQL_USER || 'root', password: process.env.MYSQL_PASSWORD || 'password', database: process.env.MYSQL_DATABASE || 'myapp', ...commonConfig, }); case 'postgres': return new DataSource({ type: 'postgres', host: process.env.POSTGRES_HOST || 'localhost', port: parseInt(process.env.POSTGRES_PORT || '5432'), username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'password', database: process.env.POSTGRES_DATABASE || 'myapp', ...commonConfig, }); case 'sqlite': return new DataSource({ type: 'sqlite', database: process.env.SQLITE_DATABASE || './myapp.db', ...commonConfig, }); default: throw new Error(`Unsupported database type: ${dbType}`); }}// 使用const dataSource = createDataSource();await dataSource.initialize();读写分离配置// 主数据库(写操作)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); } async findAll() { // 从从数据库读取 const repository = readDataSource.getRepository(User); return await repository.find(); }}性能优化连接池配置// MySQL 连接池优化const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, extra: { connectionLimit: 20, // 根据应用负载调整 acquireTimeout: 60000, timeout: 60000, waitForConnections: true, queueLimit: 100, },});// PostgreSQL 连接池优化const postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, extra: { max: 20, // 最大连接数 min: 2, // 最小连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, },});查询优化// 使用合适的数据库类型async function getBestDatabaseForOperation(operation: string) { switch (operation) { case 'read-heavy': // 使用 PostgreSQL 适合读密集型操作 return postgresDataSource; case 'write-heavy': // 使用 MySQL 适合写密集型操作 return mysqlDataSource; case 'cache': // 使用 MongoDB 适合缓存 return mongoDataSource; case 'analytics': // 使用 PostgreSQL 的窗口函数适合分析 return postgresDataSource; default: return mysqlDataSource; }}// 使用示例const dataSource = await getBestDatabaseForOperation('read-heavy');const repository = dataSource.getRepository(User);const users = await repository.find();TypeORM 的多数据库支持提供了强大的灵活性,让开发者可以根据不同的需求选择最合适的数据库类型,并在同一个应用中同时使用多个数据库。
阅读 0·2月18日 21:08

TypeORM 如何使用验证器?包括 class-validator 的集成和自定义验证器的实现

数据验证是应用程序开发中的重要环节,TypeORM 可以与各种验证器库集成,确保数据的完整性和一致性。本文将详细介绍 TypeORM 中如何使用验证器进行数据验证。验证器基础概念什么是验证器验证器是用于验证数据是否符合特定规则的机制,包括:字段类型验证字段格式验证字段长度验证自定义业务规则验证跨字段验证常用验证器库class-validator: 最流行的 TypeScript 验证器库class-transformer: 用于对象转换和验证joi: 强大的对象模式验证库zod: TypeScript 优先的模式验证库使用 class-validator安装依赖npm install class-validator class-transformernpm install --save-dev @types/class-transformer基本验证示例import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate} from 'typeorm';import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsInt, Min, Max, IsOptional, IsDateString, IsEnum, ValidateIf, ValidateNested} from 'class-validator';import { Type } from 'class-transformer';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty({ message: 'Name cannot be empty' }) @IsString({ message: 'Name must be a string' }) @MinLength(2, { message: 'Name must be at least 2 characters' }) @MaxLength(100, { message: 'Name must not exceed 100 characters' }) name: string; @Column({ unique: true }) @IsEmail({}, { message: 'Invalid email format' }) @IsNotEmpty({ message: 'Email cannot be empty' }) email: string; @Column({ nullable: true }) @IsOptional() @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(100, { message: 'Password must not exceed 100 characters' }) password?: string; @Column({ type: 'int', nullable: true }) @IsOptional() @IsInt({ message: 'Age must be an integer' }) @Min(18, { message: 'Age must be at least 18' }) @Max(120, { message: 'Age must not exceed 120' }) age?: number; @Column({ type: 'enum', enum: ['active', 'inactive', 'suspended'], default: 'active' }) @IsEnum(['active', 'inactive', 'suspended'], { message: 'Invalid status' }) status: string; @Column({ type: 'date', nullable: true }) @IsOptional() @IsDateString({}, { message: 'Invalid date format' }) birthDate?: Date; @BeforeInsert() @BeforeUpdate() async validate() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Validation failed: ${JSON.stringify(errors)}`); } }}高级验证自定义验证器import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions } from 'class-validator';// 自定义验证器:检查用户名是否唯一@ValidatorConstraint({ name: 'isUsernameUnique', async: true })export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface { async validate(username: string) { // 这里应该查询数据库检查用户名是否唯一 // 示例代码 const userExists = await checkUsernameExists(username); return !userExists; } defaultMessage(args: ValidationArguments) { return 'Username already exists'; }}// 自定义装饰器export function IsUsernameUnique(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsUsernameUniqueConstraint, }); };}// 使用自定义验证器@Entity()export class User { @Column({ unique: true }) @IsUsernameUnique({ message: 'Username already exists' }) username: string;}条件验证import { ValidateIf } from 'class-validator';@Entity()export class User { @Column() @IsNotEmpty() accountType: 'personal' | 'business'; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Company name is required for business accounts' }) companyName?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Tax ID is required for business accounts' }) taxId?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'personal') @IsNotEmpty({ message: 'Personal ID is required for personal accounts' }) personalId?: string;}嵌套对象验证import { ValidateNested, Type } from 'class-transformer';import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';class Address { @IsNotEmpty() @IsString() street: string; @IsNotEmpty() @IsString() city: string; @IsNotEmpty() @IsString() zipCode: string;}@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty() name: string; @Column({ type: 'json', nullable: true }) @ValidateIf(o => o.hasAddress) @ValidateNested() @Type(() => Address) address?: Address; @Column({ default: false }) hasAddress: boolean;}跨字段验证import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';// 自定义验证器:确认密码匹配@ValidatorConstraint({ name: 'isPasswordMatching', async: false })export class IsPasswordMatchingConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments) { const object = args.object as any; return password === object.password; } defaultMessage(args: ValidationArguments) { return 'Passwords do not match'; }}export function IsPasswordMatching(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsPasswordMatchingConstraint, }); };}@Entity()export class User { @Column() @IsNotEmpty() @MinLength(8) password: string; @Column({ nullable: true }) @IsPasswordMatching({ message: 'Passwords do not match' }) confirmPassword?: string;}验证错误处理验证并获取错误import { validate, ValidationError } from 'class-validator';async function createUser(userData: Partial<User>) { const user = new User(); Object.assign(user, userData); const errors = await validate(user); if (errors.length > 0) { // 格式化错误信息 const formattedErrors = this.formatValidationErrors(errors); throw new Error(`Validation failed: ${JSON.stringify(formattedErrors)}`); } // 保存用户 return await userRepository.save(user);}function formatValidationErrors(errors: ValidationError[]): any { const result: any = {}; errors.forEach(error => { const constraints = error.constraints || {}; result[error.property] = Object.values(constraints).join(', '); if (error.children && error.children.length > 0) { result[error.property] = { ...result[error.property], ...this.formatValidationErrors(error.children) }; } }); return result;}// 使用示例try { const user = await createUser({ name: '', email: 'invalid-email', age: 15 });} catch (error) { console.error(error.message); // 输出: Validation failed: {"name":"Name cannot be empty","email":"Invalid email format","age":"Age must be at least 18"}}自定义验证中间件import { validate } from 'class-validator';import { plainToClass } from 'class-transformer';export function validationMiddleware<T extends object>( type: new () => T) { return async (req: any, res: any, next: any) => { const dto = plainToClass(type, req.body); const errors = await validate(dto); if (errors.length > 0) { const formattedErrors = formatValidationErrors(errors); return res.status(400).json({ error: 'Validation failed', details: formattedErrors }); } req.body = dto; next(); };}// 在 Express 中使用import express from 'express';const app = express();app.post('/users', validationMiddleware(User), async (req, res) => { const user = await userRepository.save(req.body); res.json(user); });验证器装饰器详解字符串验证@Entity()export class User { @Column() @IsString() @IsNotEmpty() @MinLength(2) @MaxLength(100) @IsAlphanumeric() name: string; @Column() @IsEmail() @IsLowercase() email: string; @Column() @IsUrl() website?: string; @Column() @IsPhoneNumber(null) // 需要安装 class-validator-phone-number phone?: string;}数字验证@Entity()export class Product { @Column({ type: 'decimal', precision: 10, scale: 2 }) @IsNumber() @Min(0) @Max(999999.99) price: number; @Column({ type: 'int' }) @IsInt() @IsPositive() stock: number; @Column({ type: 'int' }) @IsInt() @IsDivisibleBy(10) quantity: number;}日期验证@Entity()export class Event { @Column({ type: 'date' }) @IsDateString() @IsBefore('endDate') startDate: Date; @Column({ type: 'date' }) @IsDateString() @IsAfter('startDate') endDate: Date; @Column({ type: 'date' }) @IsDateString() @IsFuture() registrationDeadline?: Date;}数组和对象验证@Entity()export class User { @Column({ type: 'simple-array' }) @IsArray() @ArrayNotEmpty() @ArrayMinSize(1) @ArrayMaxSize(10) @IsString({ each: true }) tags: string[]; @Column({ type: 'json', nullable: true }) @IsObject() @IsNotEmptyObject() metadata?: Record<string, any>; @Column({ type: 'simple-array', nullable: true }) @IsArray() @ArrayUnique() @IsEmail({ each: true }) additionalEmails?: string[];}验证最佳实践1. 分层验证// 实体层验证:数据库级别的验证@Entity()export class User { @Column() @IsNotEmpty() @IsString() name: string; @BeforeInsert() @BeforeUpdate() async validateEntity() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Entity validation failed: ${JSON.stringify(errors)}`); } }}// DTO 层验证:API 请求级别的验证class CreateUserDto { @IsNotEmpty() @IsString() @MinLength(2) @MaxLength(100) name: string; @IsNotEmpty() @IsEmail() email: string; @IsNotEmpty() @MinLength(8) password: string;}// 在服务层使用 DTO 验证async function createUser(dto: CreateUserDto) { const errors = await validate(dto); if (errors.length > 0) { throw new ValidationException(errors); } const user = new User(); Object.assign(user, dto); return await userRepository.save(user);}2. 异步验证@ValidatorConstraint({ name: 'isEmailUnique', async: true })export class IsEmailUniqueConstraint implements ValidatorConstraintInterface { async validate(email: string) { const user = await userRepository.findOne({ where: { email } }); return !user; } defaultMessage() { return 'Email already exists'; }}@Entity()export class User { @Column({ unique: true }) @IsEmailUnique() email: string;}3. 国际化错误消息import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';@ValidatorConstraint({ name: 'customValidator', async: false })export class CustomValidatorConstraint implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { return true; } defaultMessage(args: ValidationArguments) { // 根据语言环境返回不同的错误消息 const locale = args.object['locale'] || 'en'; const messages = { en: 'Custom validation failed', zh: '自定义验证失败', ja: 'カスタム検証に失敗しました' }; return messages[locale] || messages.en; }}4. 性能优化// 避免在验证器中执行耗时操作@ValidatorConstraint({ name: 'isUnique', async: true })export class IsUniqueConstraint implements ValidatorConstraintInterface { private cache = new Map<string, boolean>(); async validate(value: any, args: ValidationArguments) { const cacheKey = `${args.targetName}.${args.property}.${value}`; // 检查缓存 if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // 执行验证 const result = await this.checkUniqueness(value, args); // 缓存结果 this.cache.set(cacheKey, result); return result; } private async checkUniqueness(value: any, args: ValidationArguments): Promise<boolean> { // 实际的唯一性检查逻辑 return true; }}5. 测试验证器import { validate } from 'class-validator';describe('User Validation', () => { it('should validate valid user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; const errors = await validate(user); expect(errors.length).toBe(0); }); it('should fail validation for invalid email', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'invalid-email'; user.age = 25; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('isEmail'); }); it('should fail validation for underage user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 15; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('min'); });});验证器与其他库集成与 Joi 集成import * as Joi from 'joi';const userSchema = Joi.object({ name: Joi.string().min(2).max(100).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18).max(120).optional(), password: Joi.string().min(8).required()});@Entity()export class User { @BeforeInsert() @BeforeUpdate() async validateWithJoi() { const { error } = userSchema.validate(this); if (error) { throw new Error(`Validation failed: ${error.details[0].message}`); } }}与 Zod 集成import { z } from 'zod';const userSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(18).max(120).optional(), password: z.string().min(8)});@Entity()export class User { @BeforeInsert() @BeforeUpdate() async validateWithZod() { const result = userSchema.safeParse(this); if (!result.success) { throw new Error(`Validation failed: ${JSON.stringify(result.error.errors)}`); } }}TypeORM 的验证器功能提供了强大的数据验证能力,合理使用验证器可以确保数据的完整性和一致性,提高应用程序的健壮性。
阅读 0·2月18日 19:12

TypeORM 的高级查询技巧有哪些?包括复杂条件、子查询、窗口函数等

TypeORM 提供了强大的查询功能,掌握高级查询技巧可以让你构建出高效、灵活的数据库查询。本文将详细介绍 TypeORM 的各种高级查询技巧。复杂条件查询使用 Brackets 构建复杂条件import { Brackets } from 'typeorm';// 构建复杂的 OR 条件const users = await userRepository .createQueryBuilder('user') .where('user.status = :status', { status: 'active' }) .andWhere( new Brackets(qb => { qb.where('user.name LIKE :name', { name: '%John%' }) .orWhere('user.email LIKE :email', { email: '%john%' }); }) ) .getMany();// 等价 SQL: WHERE user.status = 'active' AND (user.name LIKE '%John%' OR user.email LIKE '%john%')// 多层嵌套条件const users = await userRepository .createQueryBuilder('user') .where( new Brackets(qb => { qb.where('user.age > :minAge', { minAge: 18 }) .andWhere( new Brackets(qb2 => { qb2.where('user.status = :status1', { status1: 'active' }) .orWhere('user.status = :status2', { status2: 'pending' }); }) ); }) ) .getMany();使用 Raw SQL 条件// 使用原始 SQL 表达式const users = await userRepository .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .andWhere('DATE(user.createdAt) > :date', { date: '2024-01-01' }) .getMany();// 使用函数const users = await userRepository .createQueryBuilder('user') .where('LOWER(user.name) = LOWER(:name)', { name: 'JOHN' }) .getMany();// 使用 CASE WHENconst users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'CASE WHEN user.age >= 18 THEN :adult ELSE :minor END as ageGroup', ]) .setParameter('adult', 'Adult') .setParameter('minor', 'Minor') .getRawMany();子查询使用 EXISTS 子查询// 查询有文章的用户const users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `EXISTS ${subQuery}`; }) .getMany();// 等价 SQL: SELECT * FROM user WHERE EXISTS (SELECT 1 FROM post WHERE post.authorId = user.id)// 查询没有文章的用户const users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `NOT EXISTS ${subQuery}`; }) .getMany();使用 IN 子查询// 查询文章数量大于 5 的用户const users = await userRepository .createQueryBuilder('user') .where('user.id IN ' + userRepository .createQueryBuilder('user2') .select('user2.id') .from(Post, 'post') .where('post.authorId = user2.id') .groupBy('user2.id') .having('COUNT(post.id) > :count', { count: 5 }) .getQuery() ) .getMany();// 等价 SQL: SELECT * FROM user WHERE user.id IN (SELECT user2.id FROM post WHERE post.authorId = user2.id GROUP BY user2.id HAVING COUNT(post.id) > 5)使用子查询进行计算// 查询每个用户的文章数量const users = await userRepository .createQueryBuilder('user') .leftJoin( (subQuery) => { return subQuery .select('post.authorId', 'authorId') .addSelect('COUNT(*)', 'postCount') .from(Post, 'post') .groupBy('post.authorId'); }, 'postStats', 'postStats.authorId = user.id' ) .addSelect('postStats.postCount', 'postCount') .getMany();// 或者使用子查询表达式const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', `( SELECT COUNT(*) FROM post WHERE post.authorId = user.id ) as postCount`, ]) .getRawMany();聚合和分组基本聚合函数// 统计用户数量const count = await userRepository.count();// 统计活跃用户数量const activeCount = await userRepository.count({ where: { status: 'active' }});// 计算平均年龄const result = await userRepository .createQueryBuilder('user') .select('AVG(user.age)', 'avgAge') .getRawOne();// 计算总和const result = await userRepository .createQueryBuilder('user') .select('SUM(user.score)', 'totalScore') .getRawOne();// 最大值和最小值const result = await userRepository .createQueryBuilder('user') .select('MAX(user.age)', 'maxAge') .addSelect('MIN(user.age)', 'minAge') .getRawOne();GROUP BY 和 HAVING// 按状态分组统计用户数量const result = await userRepository .createQueryBuilder('user') .select('user.status', 'status') .addSelect('COUNT(*)', 'count') .groupBy('user.status') .getRawMany();// 按年龄分组统计const result = await userRepository .createQueryBuilder('user') .select([ 'CASE ' + ' WHEN user.age < 18 THEN :minor ' + ' WHEN user.age < 30 THEN :young ' + ' WHEN user.age < 50 THEN :middle ' + ' ELSE :senior ' + 'END as ageGroup', 'COUNT(*) as count', ]) .setParameter('minor', 'Minor') .setParameter('young', 'Young') .setParameter('middle', 'Middle') .setParameter('senior', 'Senior') .groupBy('ageGroup') .getRawMany();// 使用 HAVING 过滤分组const result = await userRepository .createQueryBuilder('user') .select('user.status', 'status') .addSelect('COUNT(*)', 'count') .groupBy('user.status') .having('COUNT(*) > :minCount', { minCount: 10 }) .getRawMany();// 多列分组const result = await userRepository .createQueryBuilder('user') .select(['user.status', 'user.city']) .addSelect('COUNT(*)', 'count') .groupBy('user.status') .addGroupBy('user.city') .getRawMany();窗口函数使用窗口函数// ROW_NUMBER() - 为每行分配一个唯一的序号const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'ROW_NUMBER() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// RANK() - 相同分数的用户获得相同的排名const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'RANK() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// DENSE_RANK() - 紧密排名,不跳过排名const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'DENSE_RANK() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// LAG() 和 LEAD() - 访问前一行和后一行的数据const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'LAG(user.score) OVER (ORDER BY user.id) as prevScore', 'LEAD(user.score) OVER (ORDER BY user.id) as nextScore', ]) .getRawMany();// SUM() OVER() - 计算累计和const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'SUM(user.score) OVER (ORDER BY user.id) as cumulativeScore', ]) .getRawMany();连接查询多表连接// 左连接多个表const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.comments', 'comment') .leftJoinAndSelect('comment.author', 'commentAuthor') .where('user.id = :id', { id: 1 }) .getOne();// 自连接const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.manager', 'manager') .where('user.managerId IS NOT NULL') .getMany();// 条件连接const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.status = :status', { status: 'published' }) .addSelect(['post.id', 'post.title']) .where('user.id = :id', { id: 1 }) .getOne();复杂连接查询// 查询每个用户的最新文章const users = await userRepository .createQueryBuilder('user') .leftJoin( (subQuery) => { return subQuery .select('p1.authorId') .addSelect('p1.id', 'postId') .addSelect('p1.title', 'postTitle') .addSelect('p1.createdAt', 'postCreatedAt') .from(Post, 'p1') .where( `p1.createdAt = ( SELECT MAX(p2.createdAt) FROM post p2 WHERE p2.authorId = p1.authorId )` ); }, 'latestPost', 'latestPost.authorId = user.id' ) .addSelect('latestPost.postTitle', 'latestPostTitle') .addSelect('latestPost.postCreatedAt', 'latestPostCreatedAt') .getMany();排序技巧自定义排序// 使用表达式排序const users = await userRepository .createQueryBuilder('user') .orderBy('CASE user.status WHEN :active THEN 1 WHEN :pending THEN 2 ELSE 3 END', 'ASC') .setParameter('active', 'active') .setParameter('pending', 'pending') .getMany();// 使用函数排序const users = await userRepository .createQueryBuilder('user') .orderBy('LENGTH(user.name)', 'DESC') .getMany();// 多条件排序const users = await userRepository .createQueryBuilder('user') .orderBy('user.status', 'ASC') .addOrderBy('user.createdAt', 'DESC') .addOrderBy('user.name', 'ASC') .getMany();// 随机排序(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('RAND()') .getMany();// 随机排序(PostgreSQL)const users = await userRepository .createQueryBuilder('user') .orderBy('RANDOM()') .getMany();NULL 值排序// NULL 值排在最后(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('user.age IS NULL', 'ASC') .addOrderBy('user.age', 'ASC') .getMany();// NULL 值排在最前(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('user.age IS NULL', 'DESC') .addOrderBy('user.age', 'ASC') .getMany();// 使用 COALESCE 处理 NULLconst users = await userRepository .createQueryBuilder('user') .orderBy('COALESCE(user.age, 0)', 'ASC') .getMany();分页优化基于游标的分页// 使用游标分页提高性能async function getUsersWithCursor(cursor: number, limit: number = 10) { const query = userRepository .createQueryBuilder('user') .orderBy('user.id', 'ASC') .limit(limit + 1); // 多取一条判断是否有下一页 if (cursor) { query.where('user.id > :cursor', { cursor }); } const users = await query.getMany(); const hasNextPage = users.length > limit; if (hasNextPage) { users.pop(); // 移除多取的一条 } return { data: users, nextCursor: hasNextPage ? users[users.length - 1].id : null, hasNextPage };}// 使用示例const page1 = await getUsersWithCursor(null, 10);const page2 = await getUsersWithCursor(page1.nextCursor, 10);基于时间戳的分页// 使用时间戳分页async function getUsersByTimestamp( lastTimestamp: Date | null, limit: number = 10) { const query = userRepository .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .limit(limit + 1); if (lastTimestamp) { query.where('user.createdAt < :timestamp', { timestamp: lastTimestamp }); } const users = await query.getMany(); const hasMore = users.length > limit; if (hasMore) { users.pop(); } return { data: users, nextTimestamp: hasMore ? users[users.length - 1].createdAt : null, hasMore };}性能优化技巧使用索引提示// 强制使用特定索引(MySQL)const users = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' }) .setHint('FORCE INDEX (idx_user_email)') .getOne();// 使用索引提示(PostgreSQL)const users = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' }) .setHint('SET enable_seqscan = off') .getOne();避免全表扫描// ❌ 不好的做法:可能导致全表扫描const users = await userRepository .createQueryBuilder('user') .where('LOWER(user.name) = :name', { name: 'john' }) .getMany();// ✅ 好的做法:使用索引友好的查询const users = await userRepository .createQueryBuilder('user') .where('user.name = :name', { name: 'John' }) .getMany();使用 EXISTS 代替 IN// ❌ 不好的做法:使用 IN 可能导致性能问题const users = await userRepository .createQueryBuilder('user') .where('user.id IN :ids', { ids: [1, 2, 3, 4, 5] }) .getMany();// ✅ 好的做法:使用 EXISTSconst users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `EXISTS ${subQuery}`; }) .getMany();查询调试查看生成的 SQL// 获取生成的 SQLconst query = userRepository .createQueryBuilder('user') .where('user.id = :id', { id: 1 });const sql = query.getSql();console.log('Generated SQL:', sql);// 执行查询const user = await query.getOne();// 使用日志记录所有查询const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], logging: true, // 启用日志 maxQueryExecutionTime: 1000, // 记录超过 1 秒的查询});分析查询性能// 使用 EXPLAIN 分析查询async function analyzeQuery(query: string) { const result = await dataSource.query(`EXPLAIN ${query}`); console.log('Query Analysis:', result); // 检查是否使用了索引 const usingIndex = result.some(row => row.type === 'ref' || row.type === 'eq_ref' ); if (!usingIndex) { console.warn('Query not using index'); }}// 使用示例const query = userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' });await analyzeQuery(query.getSql());TypeORM 的高级查询技巧提供了强大而灵活的数据库操作能力,掌握这些技巧可以让你构建出高效、复杂的数据库查询。
阅读 0·2月18日 19:09

TypeORM 的订阅者 (Subscriber) 是什么?如何使用订阅者监听实体事件

订阅者是 TypeORM 中用于监听和响应实体事件的强大机制。它允许开发者在实体生命周期的关键时刻执行自定义逻辑,类似于数据库触发器,但更加灵活和类型安全。订阅者基础概念什么是订阅者订阅者是一个类,它监听特定实体的生命周期事件,并在这些事件发生时执行相应的逻辑。订阅者可以监听的事件包括:beforeInsert: 插入前afterInsert: 插入后beforeUpdate: 更新前afterUpdate: 更新后beforeRemove: 删除前afterRemove: 删除后beforeSoftRemove: 软删除前afterSoftRemove: 软删除后beforeRecover: 恢复前afterRecover: 恢复后订阅者 vs 监听器订阅者: 监听所有实体实例的事件,适合全局逻辑监听器: 在实体内部定义,只监听该实体的事件,适合实体特定的逻辑创建订阅者基本订阅者示例import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm';import { User } from '../entity/User';@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { // 指定要监听的实体 listenTo() { return User; } // 插入前 beforeInsert(event: InsertEvent<User>) { console.log('Before insert user:', event.entity); // 自动生成用户名 if (!event.entity.username) { event.entity.username = event.entity.email.split('@')[0]; } // 自动设置创建时间 if (!event.entity.createdAt) { event.entity.createdAt = new Date(); } } // 插入后 afterInsert(event: InsertEvent<User>) { console.log('After insert user:', event.entity); // 发送欢迎邮件 this.sendWelcomeEmail(event.entity); // 记录审计日志 this.logAudit('INSERT', event.entity); } // 更新前 beforeUpdate(event: UpdateEvent<User>) { console.log('Before update user:', event.entity); // 自动更新修改时间 if (event.entity) { event.entity.updatedAt = new Date(); } } // 更新后 afterUpdate(event: UpdateEvent<User>) { console.log('After update user:', event.entity); // 记录变更历史 this.recordChangeHistory(event); } // 删除前 beforeRemove(event: any) { console.log('Before remove user:', event.entity); // 检查是否可以删除 if (event.entity.hasActiveOrders()) { throw new Error('Cannot delete user with active orders'); } } // 删除后 afterRemove(event: any) { console.log('After remove user:', event.entity); // 清理关联数据 this.cleanupRelatedData(event.entity.id); } private sendWelcomeEmail(user: User) { // 发送欢迎邮件的逻辑 console.log(`Sending welcome email to ${user.email}`); } private logAudit(action: string, user: User) { // 记录审计日志的逻辑 console.log(`Audit log: ${action} user ${user.id}`); } private recordChangeHistory(event: UpdateEvent<User>) { // 记录变更历史的逻辑 console.log('Recording change history:', event.databaseEntity, event.entity); } private cleanupRelatedData(userId: number) { // 清理关联数据的逻辑 console.log(`Cleaning up data for user ${userId}`); }}注册订阅者在 DataSource 中注册import { DataSource } from 'typeorm';import { UserSubscriber } from './subscriber/UserSubscriber';export const AppDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post, Comment], subscribers: [UserSubscriber], // 注册订阅者 synchronize: false, logging: true,});动态注册订阅者import { DataSource } from 'typeorm';const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post, Comment], synchronize: false, logging: true,});// 初始化后动态添加订阅者await dataSource.initialize();const userSubscriber = new UserSubscriber();dataSource.subscribers.push(userSubscriber);高级订阅者用法数据验证@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeInsert(event: InsertEvent<User>) { this.validateUser(event.entity); } beforeUpdate(event: UpdateEvent<User>) { if (event.entity) { this.validateUser(event.entity); } } private validateUser(user: User) { // 验证邮箱格式 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(user.email)) { throw new Error('Invalid email format'); } // 验证年龄 if (user.age && (user.age < 18 || user.age > 120)) { throw new Error('Age must be between 18 and 120'); } // 验证用户名长度 if (user.username && user.username.length < 3) { throw new Error('Username must be at least 3 characters'); } }}自动填充字段@EventSubscriber()export class BaseEntitySubscriber implements EntitySubscriberInterface { listenTo() { return Object; // 监听所有实体 } beforeInsert(event: InsertEvent<any>) { const entity = event.entity; const now = new Date(); // 自动设置创建时间 if (entity.hasOwnProperty('createdAt') && !entity.createdAt) { entity.createdAt = now; } // 自动设置更新时间 if (entity.hasOwnProperty('updatedAt') && !entity.updatedAt) { entity.updatedAt = now; } // 自动设置创建者 if (entity.hasOwnProperty('createdBy') && !entity.createdBy) { entity.createdBy = this.getCurrentUserId(); } } beforeUpdate(event: UpdateEvent<any>) { const entity = event.entity; if (entity) { // 自动更新更新时间 if (entity.hasOwnProperty('updatedAt')) { entity.updatedAt = new Date(); } // 自动设置更新者 if (entity.hasOwnProperty('updatedBy')) { entity.updatedBy = this.getCurrentUserId(); } } } private getCurrentUserId(): number { // 获取当前用户 ID 的逻辑 return 1; // 示例 }}审计日志@EventSubscriber()export class AuditLogSubscriber implements EntitySubscriberInterface { listenTo() { return Object; // 监听所有实体 } async afterInsert(event: InsertEvent<any>) { await this.createAuditLog('INSERT', event.entity); } async afterUpdate(event: UpdateEvent<any>) { await this.createAuditLog('UPDATE', event.entity, event.databaseEntity); } async afterRemove(event: any) { await this.createAuditLog('DELETE', event.entity); } private async createAuditLog( action: string, entity: any, oldEntity?: any ) { const auditLog = { action, entityType: entity.constructor.name, entityId: entity.id, userId: this.getCurrentUserId(), timestamp: new Date(), changes: oldEntity ? this.getChanges(oldEntity, entity) : null, ipAddress: this.getCurrentIpAddress(), }; // 保存审计日志 console.log('Creating audit log:', auditLog); // await this.auditLogRepository.save(auditLog); } private getChanges(oldEntity: any, newEntity: any): any { const changes: any = {}; for (const key in newEntity) { if (oldEntity[key] !== newEntity[key]) { changes[key] = { old: oldEntity[key], new: newEntity[key], }; } } return changes; } private getCurrentUserId(): number { return 1; // 示例 } private getCurrentIpAddress(): string { return '127.0.0.1'; // 示例 }}缓存失效@EventSubscriber()export class CacheInvalidationSubscriber implements EntitySubscriberInterface { private cacheService: CacheService; constructor() { this.cacheService = new CacheService(); } listenTo() { return Object; // 监听所有实体 } async afterInsert(event: InsertEvent<any>) { await this.invalidateCache(event.entity); } async afterUpdate(event: UpdateEvent<any>) { if (event.entity) { await this.invalidateCache(event.entity); } } async afterRemove(event: any) { await this.invalidateCache(event.entity); } private async invalidateCache(entity: any) { const entityType = entity.constructor.name.toLowerCase(); const entityId = entity.id; // 使单个实体的缓存失效 await this.cacheService.delete(`${entityType}:${entityId}`); // 使列表缓存失效 await this.cacheService.delete(`${entityType}:list:*`); console.log(`Cache invalidated for ${entityType}:${entityId}`); }}通知和事件@EventSubscriber()export class NotificationSubscriber implements EntitySubscriberInterface { private notificationService: NotificationService; constructor() { this.notificationService = new NotificationService(); } listenTo() { return Object; // 监听所有实体 } async afterInsert(event: InsertEvent<any>) { await this.handleInsertEvent(event); } async afterUpdate(event: UpdateEvent<any>) { await this.handleUpdateEvent(event); } private async handleInsertEvent(event: InsertEvent<any>) { const entity = event.entity; // 根据实体类型发送不同的通知 switch (entity.constructor.name) { case 'Order': await this.notificationService.sendOrderCreatedNotification(entity); break; case 'Comment': await this.notificationService.sendCommentNotification(entity); break; case 'Message': await this.notificationService.sendMessageNotification(entity); break; } } private async handleUpdateEvent(event: UpdateEvent<any>) { const entity = event.entity; if (!entity) return; // 根据实体类型和变更发送通知 switch (entity.constructor.name) { case 'Order': if (entity.status !== event.databaseEntity.status) { await this.notificationService.sendOrderStatusChangedNotification(entity); } break; case 'User': if (entity.email !== event.databaseEntity.email) { await this.notificationService.sendEmailChangedNotification(entity); } break; } }}订阅者最佳实践1. 单一职责原则每个订阅者应该只负责一个特定的功能领域。// ✅ 好的做法:每个订阅者负责一个功能@EventSubscriber()export class UserValidationSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeInsert(event: InsertEvent<User>) { /* 验证逻辑 */ }}@EventSubscriber()export class UserAuditSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { /* 审计逻辑 */ }}// ❌ 不好的做法:一个订阅者负责多个功能@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeInsert(event: InsertEvent<User>) { /* 验证逻辑 */ } afterInsert(event: InsertEvent<User>) { /* 审计逻辑 */ } afterUpdate(event: UpdateEvent<User>) { /* 通知逻辑 */ }}2. 避免循环依赖订阅者不应该触发会导致其他订阅者无限循环的操作。@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> { constructor( private userRepository: Repository<User> ) {} listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // ❌ 不好的做法:可能导致循环 // await this.userRepository.save(event.entity); // ✅ 好的做法:使用 EntityManager 避免触发订阅者 await this.userRepository.manager.save(User, event.entity); }}3. 错误处理在订阅者中妥善处理错误,避免影响主操作。@EventSubscriber()export class SafeSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { try { await this.sendNotification(event.entity); } catch (error) { // 记录错误但不影响主操作 console.error('Failed to send notification:', error); // 可以将错误发送到错误监控系统 } } private async sendNotification(user: User) { // 发送通知的逻辑 }}4. 性能考虑避免在订阅者中执行耗时操作。@EventSubscriber()export class PerformanceAwareSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // ❌ 不好的做法:同步执行耗时操作 // await this.sendEmail(event.entity); // await this.generateReport(event.entity); // ✅ 好的做法:异步执行耗时操作 setImmediate(() => { this.sendEmail(event.entity).catch(console.error); this.generateReport(event.entity).catch(console.error); }); // 或者使用消息队列 // await this.queueService.add('send-email', { userId: event.entity.id }); } private async sendEmail(user: User) { // 发送邮件的逻辑 } private async generateReport(user: User) { // 生成报告的逻辑 }}5. 测试订阅者为订阅者编写单元测试。import { InsertEvent } from 'typeorm';import { UserSubscriber } from './UserSubscriber';import { User } from '../entity/User';describe('UserSubscriber', () => { let subscriber: UserSubscriber; beforeEach(() => { subscriber = new UserSubscriber(); }); it('should auto-generate username before insert', () => { const user = new User(); user.email = 'test@example.com'; const event: InsertEvent<User> = { entity: user, metadata: {} as any, queryRunner: {} as any, manager: {} as any, }; subscriber.beforeInsert(event); expect(user.username).toBe('test'); }); it('should set createdAt before insert', () => { const user = new User(); user.email = 'test@example.com'; const event: InsertEvent<User> = { entity: user, metadata: {} as any, queryRunner: {} as any, manager: {} as any, }; subscriber.beforeInsert(event); expect(user.createdAt).toBeInstanceOf(Date); });});订阅者 vs 监听器监听器示例@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; @BeforeInsert() beforeInsert() { console.log('Before insert in entity'); this.createdAt = new Date(); } @AfterInsert() afterInsert() { console.log('After insert in entity'); } @BeforeUpdate() beforeUpdate() { console.log('Before update in entity'); this.updatedAt = new Date(); } @AfterUpdate() afterUpdate() { console.log('After update in entity'); }}选择订阅者还是监听器使用订阅者的情况:需要监听多个实体的事件需要注入其他服务需要实现跨实体的业务逻辑需要保持实体类的简洁使用监听器的情况:逻辑只与单个实体相关逻辑简单,不需要外部依赖希望逻辑与实体紧密关联TypeORM 的订阅者机制提供了强大而灵活的事件处理能力,合理使用订阅者可以实现复杂的业务逻辑,同时保持代码的整洁和可维护性。
阅读 0·2月17日 23:52

TypeORM 的迁移系统如何工作?如何创建、运行和管理数据库迁移

迁移系统是 TypeORM 中用于管理数据库结构变更的重要工具,它允许开发者以版本化的方式追踪和应用数据库结构的变化,确保团队协作时数据库结构的一致性。迁移基础概念什么是迁移迁移是数据库结构变更的脚本,用于:创建或删除表添加或删除列修改列类型创建或删除索引添加或删除外键约束每个迁移都有唯一的版本号和时间戳,确保迁移可以按顺序执行。迁移文件结构import { MigrationInterface, QueryRunner, Table } from 'typeorm';export class CreateUserTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 执行迁移:创建表、添加列等 await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, { name: 'email', type: 'varchar', isUnique: true, }, { name: 'createdAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { // 回滚迁移:删除表、移除列等 await queryRunner.dropTable('user'); }}创建迁移使用 CLI 创建迁移# 创建新迁移npm run typeorm migration:generate -- -n CreateUserTable# 或者使用 npxnpx typeorm migration:generate -n CreateUserTable# 创建空迁移npm run typeorm migration:create -- -n CreateUserTable配置 DataSourceimport { DataSource } from 'typeorm';export const AppDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: ['src/entity/**/*.ts'], migrations: ['src/migration/**/*.ts'], subscribers: ['src/subscriber/**/*.ts'], synchronize: false, // 生产环境必须设为 false logging: true,});运行迁移使用 CLI 运行迁移# 运行所有待执行的迁移npm run typeorm migration:run# 回滚最后一次迁移npm run typeorm migration:revert# 显示迁移状态npm run typeorm migration:show# 清空数据库(慎用)npm run typeorm schema:drop在代码中运行迁移import { DataSource } from 'typeorm';async function runMigrations() { const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: ['src/entity/**/*.ts'], migrations: ['src/migration/**/*.ts'], }); await dataSource.initialize(); // 运行所有待执行的迁移 await dataSource.runMigrations(); // 回滚最后一次迁移 // await dataSource.undoLastMigration(); await dataSource.destroy();}runMigrations().catch(console.error);迁移操作示例创建表public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', length: '100', }, { name: 'email', type: 'varchar', length: '255', isUnique: true, }, { name: 'age', type: 'int', nullable: true, }, { name: 'isActive', type: 'boolean', default: true, }, { name: 'createdAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', }, { name: 'updatedAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', }, ], indices: [ { name: 'IDX_USER_EMAIL', columnNames: ['email'], }, ], }), true );}添加列public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn( 'user', new TableColumn({ name: 'avatar', type: 'varchar', length: '255', isNullable: true, }) );}public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'avatar');}修改列public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn( 'user', 'name', new TableColumn({ name: 'name', type: 'varchar', length: '200', // 修改长度 }) );}public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn( 'user', 'name', new TableColumn({ name: 'name', type: 'varchar', length: '100', }) );}创建索引public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createIndex( 'user', new TableIndex({ name: 'IDX_USER_EMAIL', columnNames: ['email'], isUnique: true, }) );}public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropIndex('user', 'IDX_USER_EMAIL');}添加外键public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createForeignKey( 'post', new TableForeignKey({ columnNames: ['authorId'], referencedColumnNames: ['id'], referencedTableName: 'user', onDelete: 'CASCADE', }) );}public async down(queryRunner: QueryRunner): Promise<void> { const table = await queryRunner.getTable('post'); const foreignKey = table.foreignKeys.find( fk => fk.columnNames.indexOf('authorId') !== -1 ); await queryRunner.dropForeignKey('post', foreignKey);}执行原生 SQLpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(` CREATE TRIGGER update_user_timestamp BEFORE UPDATE ON user FOR EACH ROW SET NEW.updatedAt = CURRENT_TIMESTAMP `);}public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`DROP TRIGGER update_user_timestamp`);}数据迁移迁移现有数据public async up(queryRunner: QueryRunner): Promise<void> { // 添加新列 await queryRunner.addColumn( 'user', new TableColumn({ name: 'fullName', type: 'varchar', length: '200', isNullable: true, }) ); // 迁移数据 await queryRunner.query(` UPDATE user SET fullName = CONCAT(firstName, ' ', lastName) `); // 删除旧列 await queryRunner.dropColumn('user', 'firstName'); await queryRunner.dropColumn('user', 'lastName');}批量插入数据public async up(queryRunner: QueryRunner): Promise<void> { const users = [ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }, ]; for (const user of users) { await queryRunner.query( `INSERT INTO user (name, email) VALUES (?, ?)`, [user.name, user.email] ); }}迁移最佳实践1. 版本控制// 迁移文件命名格式: {timestamp}-{name}.ts// 例如: 1234567890123-CreateUserTable.tsexport class CreateUserTable1234567890123 implements MigrationInterface { // 迁移内容}2. 可逆性确保每个迁移都可以完全回滚:export class AddUserAvatar1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn('user', new TableColumn({ name: 'avatar', type: 'varchar', isNullable: true, })); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'avatar'); }}3. 事务支持public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.startTransaction(); try { await queryRunner.createTable(/* ... */); await queryRunner.addColumn(/* ... */); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; }}4. 环境区分export class AddProductionIndex1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 只在生产环境执行 if (process.env.NODE_ENV === 'production') { await queryRunner.createIndex(/* ... */); } } public async down(queryRunner: QueryRunner): Promise<void> { if (process.env.NODE_ENV === 'production') { await queryRunner.dropIndex(/* ... */); } }}常见问题1. 迁移冲突当多个开发者同时创建迁移时可能出现冲突:# 解决方法:重新生成迁移npm run typeorm migration:generate -- -n FixMigrationConflict2. 数据丢失风险在修改列类型或删除列前备份数据:public async up(queryRunner: QueryRunner): Promise<void> { // 备份数据 await queryRunner.query(`CREATE TABLE user_backup AS SELECT * FROM user`); // 执行迁移 await queryRunner.changeColumn(/* ... */);}3. 性能问题对于大数据表的迁移,考虑分批处理:public async up(queryRunner: QueryRunner): Promise<void> { const batchSize = 1000; let offset = 0; while (true) { const users = await queryRunner.query( `SELECT id FROM user LIMIT ${batchSize} OFFSET ${offset}` ); if (users.length === 0) break; for (const user of users) { await queryRunner.query(`UPDATE user SET ... WHERE id = ?`, [user.id]); } offset += batchSize; }}生产环境建议禁用 synchronize: 生产环境必须设置 synchronize: false备份策略: 执行迁移前备份数据库测试环境: 先在测试环境验证迁移回滚计划: 准备好回滚方案监控日志: 监控迁移执行日志分步执行: 对于大型迁移,分步骤执行TypeORM 的迁移系统提供了强大而灵活的数据库结构管理能力,掌握迁移系统的使用对于维护大型应用的数据库结构至关重要。
阅读 0·2月17日 23:49

Next.js 与微前端架构如何结合使用?

Next.js 与微前端架构的结合是构建大型企业级应用的重要方案。微前端架构允许将大型应用拆分为多个独立开发、部署和维护的小型前端应用。微前端架构概述1. 微前端核心概念微前端是一种架构风格,将前端应用分解为更小、更简单的块,这些块可以由不同的团队独立开发和部署。核心优势:独立开发和部署技术栈无关增量升级团队自治代码隔离Next.js 微前端实现方案1. Module Federation(模块联邦)// next.config.js - 主应用配置const NextFederationPlugin = require('@module-federation/nextjs-mf');module.exports = { webpack(config, options) { const { isServer } = options; config.plugins.push( new NextFederationPlugin({ name: 'main_app', filename: 'static/chunks/remoteEntry.js', remotes: { productApp: 'product_app@https://product.example.com/_next/static/chunks/remoteEntry.js', cartApp: 'cart_app@https://cart.example.com/_next/static/chunks/remoteEntry.js', userApp: 'user_app@https://user.example.com/_next/static/chunks/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: false, }, 'react-dom': { singleton: true, requiredVersion: false, }, next: { singleton: true, requiredVersion: false, }, }, extraOptions: { automaticAsyncBoundary: true, }, }) ); return config; },};// next.config.js - 子应用配置(productApp)const NextFederationPlugin = require('@module-federation/nextjs-mf');module.exports = { webpack(config, options) { const { isServer } = options; config.plugins.push( new NextFederationPlugin({ name: 'product_app', filename: 'static/chunks/remoteEntry.js', exposes: { './ProductList': './components/ProductList', './ProductDetail': './components/ProductDetail', './ProductSearch': './components/ProductSearch', }, shared: { react: { singleton: true, requiredVersion: false, }, 'react-dom': { singleton: true, requiredVersion: false, }, next: { singleton: true, requiredVersion: false, }, }, }) ); return config; },};// 主应用中使用远程组件// app/products/page.js'use client';import dynamic from 'next/dynamic';const ProductList = dynamic(() => import('productApp/ProductList'), { loading: () => <div>Loading products...</div>, ssr: false,});const ProductSearch = dynamic(() => import('productApp/ProductSearch'), { loading: () => <div>Loading search...</div>, ssr: false,});export default function ProductsPage() { return ( <div> <h1>Products</h1> <ProductSearch /> <ProductList /> </div> );}2. iframe 方案// components/IframeWrapper.js'use client';import { useState, useEffect, useRef } from 'react';export default function IframeWrapper({ src, title, onMessage }) { const iframeRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { const iframe = iframeRef.current; const handleMessage = (event) => { // 验证消息来源 if (event.origin !== new URL(src).origin) return; onMessage?.(event.data); }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; }, [src, onMessage]); const handleLoad = () => { setIsLoaded(true); }; const sendMessage = (message) => { if (iframeRef.current && iframeRef.current.contentWindow) { iframeRef.current.contentWindow.postMessage(message, new URL(src).origin); } }; return ( <div className="iframe-container"> {!isLoaded && <div className="loading">Loading...</div>} <iframe ref={iframeRef} src={src} title={title} onLoad={handleLoad} style={{ border: 'none', width: '100%', height: '100%', display: isLoaded ? 'block' : 'none' }} allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" /> </div> );}// 使用 iframe 集成子应用// app/dashboard/page.js'use client';import IframeWrapper from '@/components/IframeWrapper';export default function DashboardPage() { const handleMessage = (data) => { console.log('Message from iframe:', data); if (data.type === 'NAVIGATION') { // 处理导航事件 } else if (data.type === 'AUTH') { // 处理认证事件 } }; return ( <div className="dashboard"> <nav> <a href="/">Home</a> <a href="/dashboard">Dashboard</a> </nav> <main> <IframeWrapper src="https://cart.example.com" title="Shopping Cart" onMessage={handleMessage} /> </main> </div> );}3. Web Components 方案// components/MicroFrontendWrapper.js'use client';import { useEffect, useRef } from 'react';export default function MicroFrontendWrapper({ name, host, history, onNavigate, onUnmount}) { const ref = useRef(null); useEffect(() => { const scriptId = `micro-frontend-script-${name}`; const renderMicroFrontend = () => { window[name] = { mount: (container, history) => { console.log(`Mounting ${name}`); // 调用子应用的 mount 方法 }, unmount: (container) => { console.log(`Unmounting ${name}`); onUnmount?.(); }, }; if (window[name] && window[name].mount) { window[name].mount(ref.current, history); } }; const loadScript = () => { if (document.getElementById(scriptId)) { renderMicroFrontend(); return; } const script = document.createElement('script'); script.id = scriptId; script.src = `${host}/main.js`; script.onload = renderMicroFrontend; document.head.appendChild(script); }; loadScript(); return () => { if (window[name] && window[name].unmount) { window[name].unmount(ref.current); } }; }, [name, host, history, onUnmount]); return <div ref={ref} />;}// 使用 Web Components 集成// app/micro/page.js'use client';import MicroFrontendWrapper from '@/components/MicroFrontendWrapper';export default function MicroFrontendPage() { const handleNavigate = (location) => { console.log('Navigate to:', location); window.history.pushState({}, '', location); }; const handleUnmount = () => { console.log('Micro frontend unmounted'); }; return ( <div> <h1>Micro Frontend Integration</h1> <MicroFrontendWrapper name="productApp" host="https://product.example.com" history={window.history} onNavigate={handleNavigate} onUnmount={handleUnmount} /> </div> );}4. 单体仓库(Monorepo)方案// 使用 Turborepo 管理 monorepo// turbo.json{ "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^lint"] }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] } }}// pnpm-workspace.yamlpackages: - 'apps/*' - 'packages/*'// 目录结构// apps/// main-app/ # 主应用// product-app/ # 产品子应用// cart-app/ # 购物车子应用// user-app/ # 用户子应用// packages/// ui/ # 共享 UI 组件// utils/ # 共享工具函数// types/ # 共享类型定义// config/ # 共享配置// apps/main-app/package.json{ "name": "main-app", "dependencies": { "next": "^14.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "@workspace/ui": "workspace:*", "@workspace/utils": "workspace:*" }}// apps/product-app/package.json{ "name": "product-app", "dependencies": { "next": "^14.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "@workspace/ui": "workspace:*", "@workspace/utils": "workspace:*" }}状态管理和通信1. 跨应用状态管理// packages/shared-state/src/store.jsimport { createStore } from 'zustand/vanilla';export const createSharedStore = (initialState) => { return createStore((set, get) => ({ ...initialState, update: (key, value) => set({ [key]: value }), reset: () => set(initialState), }));};// 创建共享状态export const userStore = createSharedStore({ user: null, isAuthenticated: false, cart: [],});export const productStore = createSharedStore({ products: [], filters: {}, sortBy: 'name',});// 主应用中使用// app/layout.js'use client';import { userStore } from '@workspace/shared-state';import { useEffect } from 'react';export default function RootLayout({ children }) { useEffect(() => { // 监听用户状态变化 const unsubscribe = userStore.subscribe((state) => { console.log('User state changed:', state); // 通知其他应用 window.postMessage({ type: 'USER_STATE_CHANGE', state }, '*'); }); return () => unsubscribe(); }, []); return ( <html lang="en"> <body>{children}</body> </html> );}// 子应用中使用// product-app/components/UserInfo.js'use client';import { userStore } from '@workspace/shared-state';import { useEffect, useState } from 'react';export default function UserInfo() { const [user, setUser] = useState(null); useEffect(() => { // 订阅用户状态 const unsubscribe = userStore.subscribe((state) => { setUser(state.user); }); return () => unsubscribe(); }, []); if (!user) { return <div>Please login</div>; } return <div>Welcome, {user.name}</div>;}2. 事件总线通信// packages/event-bus/src/index.jsclass EventBus { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } off(event, callback) { if (!this.events[event]) return; this.events[event] = this.events[event].filter(cb => cb !== callback); } emit(event, data) { if (!this.events[event]) return; this.events[event].forEach(callback => { callback(data); }); } once(event, callback) { const onceCallback = (data) => { callback(data); this.off(event, onceCallback); }; this.on(event, onceCallback); }}export const eventBus = new EventBus();// 定义事件类型export const Events = { USER_LOGIN: 'USER_LOGIN', USER_LOGOUT: 'USER_LOGOUT', CART_UPDATE: 'CART_UPDATE', PRODUCT_ADD: 'PRODUCT_ADD', NAVIGATION: 'NAVIGATION',};// 主应用中监听事件// app/_components/EventListeners.js'use client';import { useEffect } from 'react';import { eventBus, Events } from '@workspace/event-bus';import { useRouter } from 'next/navigation';export default function EventListeners() { const router = useRouter(); useEffect(() => { const handleNavigation = (data) => { console.log('Navigation event:', data); router.push(data.path); }; const handleCartUpdate = (data) => { console.log('Cart updated:', data); // 更新购物车 UI }; eventBus.on(Events.NAVIGATION, handleNavigation); eventBus.on(Events.CART_UPDATE, handleCartUpdate); return () => { eventBus.off(Events.NAVIGATION, handleNavigation); eventBus.off(Events.CART_UPDATE, handleCartUpdate); }; }, [router]); return null;}// 子应用中发送事件// product-app/components/AddToCart.js'use client';import { eventBus, Events } from '@workspace/event-bus';export default function AddToCart({ product }) { const handleAddToCart = () => { eventBus.emit(Events.PRODUCT_ADD, { product }); eventBus.emit(Events.CART_UPDATE, { type: 'ADD', product }); }; return ( <button onClick={handleAddToCart}> Add to Cart </button> );}样式隔离1. CSS Modules 隔离// product-app/components/ProductCard.module.css.productCard { border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white;}.productCard__title { font-size: 18px; font-weight: bold; margin-bottom: 8px;}.productCard__price { color: #e44d26; font-size: 20px; font-weight: bold;}// product-app/components/ProductCard.jsimport styles from './ProductCard.module.css';export default function ProductCard({ product }) { return ( <div className={styles.productCard}> <h3 className={styles.productCard__title}>{product.name}</h3> <p className={styles.productCard__price}>${product.price}</p> </div> );}2. CSS-in-JS 隔离// product-app/components/ProductCard.js'use client';import styled from 'styled-components';const Card = styled.div` border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white;`;const Title = styled.h3` font-size: 18px; font-weight: bold; margin-bottom: 8px;`;const Price = styled.p` color: #e44d26; font-size: 20px; font-weight: bold;`;export default function ProductCard({ product }) { return ( <Card> <Title>{product.name}</Title> <Price>${product.price}</Price> </Card> );}3. Shadow DOM 隔离// components/ShadowDOMWrapper.js'use client';import { useEffect, useRef } from 'react';export default function ShadowDOMWrapper({ children, styles }) { const containerRef = useRef(null); const shadowRootRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // 创建 Shadow DOM shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' }); // 添加样式 if (styles) { const styleElement = document.createElement('style'); styleElement.textContent = styles; shadowRootRef.current.appendChild(styleElement); } // 添加内容 const content = document.createElement('div'); content.className = 'shadow-content'; shadowRootRef.current.appendChild(content); return () => { if (shadowRootRef.current) { containerRef.current.removeChild(shadowRootRef.current); } }; }, [styles]); useEffect(() => { if (shadowRootRef.current) { const content = shadowRootRef.current.querySelector('.shadow-content'); if (content) { // 使用 ReactDOM 渲染到 Shadow DOM import('react-dom/client').then(({ createRoot }) => { const root = createRoot(content); root.render(children); }); } } }, [children]); return <div ref={containerRef} />;}// 使用 Shadow DOM// app/micro/page.js'use client';import ShadowDOMWrapper from '@/components/ShadowDOMWrapper';const shadowStyles = ` .product-card { border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white; } .product-title { font-size: 18px; font-weight: bold; }`;export default function MicroFrontendPage() { return ( <ShadowDOMWrapper styles={shadowStyles}> <div className="product-card"> <h3 className="product-title">Product Name</h3> <p>$99.99</p> </div> </ShadowDOMWrapper> );}部署策略1. 独立部署// Vercel 配置 - 主应用// vercel.json{ "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ]}// Vercel 配置 - 子应用// product-app/vercel.json{ "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ]}// Docker 部署配置// DockerfileFROM node:18-alpine AS base# 依赖安装FROM base AS depsWORKDIR /appCOPY package.json pnpm-lock.yaml ./RUN npm install -g pnpm && pnpm install --frozen-lockfile# 构建FROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN pnpm build# 运行FROM base AS runnerWORKDIR /appENV NODE_ENV productionCOPY --from=builder /app/public ./publicCOPY --from=builder /app/.next/standalone ./COPY --from=builder /app/.next/static ./.next/staticEXPOSE 3000CMD ["node", "server.js"]2. CI/CD 流程// .github/workflows/deploy.ymlname: Deployon: push: branches: [main]jobs: deploy-main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm --filter main-app build - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PROJECT_ID }} working-directory: ./apps/main-app deploy-product: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm --filter product-app build - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PRODUCT_PROJECT_ID }} working-directory: ./apps/product-app最佳实践选择合适的方案: Module Federation 适合技术栈统一的项目,iframe 适合完全隔离的场景共享依赖: 使用 monorepo 管理共享代码和依赖状态管理: 使用事件总线或共享状态管理跨应用通信样式隔离: 使用 CSS Modules、CSS-in-JS 或 Shadow DOM 避免样式冲突独立部署: 每个子应用独立构建和部署版本管理: 使用语义化版本管理子应用依赖监控和日志: 统一监控和日志收集性能优化: 按需加载子应用,避免重复依赖测试策略: 集成测试覆盖跨应用场景文档和规范: 建立清晰的开发规范和文档Next.js 与微前端架构的结合为企业级应用提供了灵活、可扩展的解决方案。
阅读 0·2月17日 22:52