2月18日 22:20
How does TypeORM's event system work? Including entity listeners and subscribers
TypeORM's event system allows developers to execute custom logic during the lifecycle of entity operations, providing powerful extensibility.
Event Types
1. Entity Lifecycle Events
TypeORM provides the following entity lifecycle events:
BeforeInsert- Triggered before entity insertionAfterInsert- Triggered after entity insertionBeforeUpdate- Triggered before entity updateAfterUpdate- Triggered after entity updateBeforeRemove- Triggered before entity deletionAfterRemove- Triggered after entity deletionBeforeSoftRemove- Triggered before entity soft deletionAfterSoftRemove- Triggered after entity soft deletionBeforeRecover- Triggered before entity recoveryAfterRecover- Triggered after entity recovery
2. Subscriber Events
Subscribers can listen to specific events for all entities.
Using Entity Listeners
Basic Usage
typescriptimport { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate, AfterInsert, AfterUpdate } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @Column({ default: 0 }) version: number; @BeforeInsert() beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); this.version = 1; } @BeforeUpdate() beforeUpdate() { this.updatedAt = new Date(); this.version++; } @AfterInsert() afterInsert() { console.log(`User ${this.name} inserted with ID ${this.id}`); } @AfterUpdate() afterUpdate() { console.log(`User ${this.name} updated to version ${this.version}`); } }
Complex Logic Handling
typescriptimport { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm'; import { hash } from 'bcrypt'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column() password: string; @Column({ default: false }) emailVerified: boolean; @Column({ type: 'timestamp', nullable: true }) emailVerifiedAt: Date; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @BeforeInsert() async beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); // Hash password if (this.password) { this.password = await hash(this.password, 10); } // Validate email format if (!this.validateEmail(this.email)) { throw new Error('Invalid email format'); } } @BeforeUpdate() async beforeUpdate() { this.updatedAt = new Date(); // If password is modified, re-hash it if (this.password && this.isPasswordModified()) { this.password = await hash(this.password, 10); } // If email is verified, record verification time if (this.emailVerified && !this.emailVerifiedAt) { this.emailVerifiedAt = new Date(); } } private validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } private isPasswordModified(): boolean { // Implement password modification detection logic return true; } }
Using Subscribers
Basic Subscriber
typescriptimport { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; import { User } from '../entity/User'; @EventSubscriber() export class UserSubscriber implements EntitySubscriberInterface<User> { // Specify the entity to listen to listenTo() { return User; } // Before insert beforeInsert(event: InsertEvent<User>) { console.log(`Before inserting user: ${event.entity.name}`); // Can modify entity event.entity.createdAt = new Date(); } // After insert afterInsert(event: InsertEvent<User>) { console.log(`After inserting user with ID: ${event.entity.id}`); // Send welcome email this.sendWelcomeEmail(event.entity); } // Before update beforeUpdate(event: UpdateEvent<User>) { console.log(`Before updating user: ${event.entity.name}`); // Log changes this.logChanges(event); } // After update afterUpdate(event: UpdateEvent<User>) { console.log(`After updating user: ${event.entity.name}`); // Send notification this.sendUpdateNotification(event.entity); } // Before remove beforeRemove(event: RemoveEvent<User>) { console.log(`Before removing user: ${event.entity.name}`); // Check if can delete if (event.entity.posts && event.entity.posts.length > 0) { throw new Error('Cannot delete user with posts'); } } // After remove afterRemove(event: RemoveEvent<User>) { console.log(`After removing user: ${event.entity.name}`); // Clean up related data this.cleanupUserData(event.entity.id); } private sendWelcomeEmail(user: User) { // Send welcome email logic console.log(`Sending welcome email to ${user.email}`); } private sendUpdateNotification(user: User) { // Send update notification logic console.log(`Sending update notification to ${user.email}`); } private logChanges(event: UpdateEvent<User>) { // Log changes logic console.log('Changes:', event.updatedColumns); } private cleanupUserData(userId: number) { // Clean up user data logic console.log(`Cleaning up data for user ${userId}`); } }
Global Subscriber
typescriptimport { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; @EventSubscriber() export class AuditSubscriber implements EntitySubscriberInterface { // Listen to all entities listenTo() { return Object; } // Insert operations for all entities afterInsert(event: InsertEvent<any>) { console.log(`Entity ${event.metadata.name} inserted with ID ${event.entity.id}`); // Log audit this.logAudit({ action: 'INSERT', entity: event.metadata.name, entityId: event.entity.id, timestamp: new Date(), }); } private logAudit(log: any) { // Log audit logic console.log('Audit log:', log); } }
Registering Subscribers
Register in DataSource
typescriptimport { DataSource } from 'typeorm'; import { UserSubscriber } from './subscriber/UserSubscriber'; import { AuditSubscriber } from './subscriber/AuditSubscriber'; const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, // Register subscribers subscribers: [UserSubscriber, AuditSubscriber], });
Dynamic Subscriber Registration
typescriptimport { DataSource } from 'typeorm'; const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, }); // Dynamically register subscribers after initialization dataSource.initialize().then(() => { const userSubscriber = new UserSubscriber(); dataSource.subscribers.push(userSubscriber); });
Advanced Event Handling
Events in Transactions
typescript@EventSubscriber() export class TransactionSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // Check if in transaction if (event.queryRunner?.isTransactionActive) { console.log('Insert operation is part of a transaction'); } // Use transaction executor if (event.queryRunner) { event.queryRunner.manager.getRepository(AuditLog).save({ action: 'USER_INSERT', userId: event.entity.id, timestamp: new Date(), }); } } }
Async Event Handling
typescript@EventSubscriber() export class AsyncSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // Async send email await this.sendEmailAsync(event.entity); // Async generate user profile await this.generateUserProfileAsync(event.entity); } private async sendEmailAsync(user: User) { // Simulate async email sending return new Promise((resolve) => { setTimeout(() => { console.log(`Email sent to ${user.email}`); resolve(null); }, 1000); }); } private async generateUserProfileAsync(user: User) { // Simulate async user profile generation return new Promise((resolve) => { setTimeout(() => { console.log(`Profile generated for user ${user.id}`); resolve(null); }, 500); }); } }
Conditional Event Handling
typescript@EventSubscriber() export class ConditionalSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeUpdate(event: UpdateEvent<User>) { // Only execute under certain conditions if (this.shouldProcessUpdate(event)) { this.processUpdate(event); } } private shouldProcessUpdate(event: UpdateEvent<User>): boolean { // Check if specific fields were updated const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('email') || updatedFields.includes('password'); } private processUpdate(event: UpdateEvent<User>) { // Process update logic console.log('Processing critical update:', event.entity); } }
Event Best Practices
1. Keep Event Handling Simple
typescript// ✅ Good: Simple and direct event handling @EventSubscriber() export class SimpleSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // Simple logging console.log(`User created: ${event.entity.name}`); } } // ❌ Bad: Too complex event handling @EventSubscriber() export class ComplexSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // Complex business logic const user = event.entity; // Send email await this.sendEmail(user); // Create user profile await this.createProfile(user); // Initialize user settings await this.initializeSettings(user); // Send welcome message await this.sendWelcomeMessage(user); // Record statistics await this.recordStatistics(user); // Update cache await this.updateCache(user); // Trigger other events await this.triggerEvents(user); } }
2. Avoid Circular Events
typescript// ✅ Good: Avoid circular events @EventSubscriber() export class SafeSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // Use flag to avoid loops if (event.entity.processed) { return; } // Process logic await this.processUser(event.entity); // Mark as processed event.entity.processed = true; } } // ❌ Bad: May cause circular events @EventSubscriber() export class CircularSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // Update user, may trigger afterUpdate event await event.manager.save(User, { id: event.entity.id, processed: true, }); } }
3. Error Handling
typescript// ✅ Good: Proper error handling @EventSubscriber() export class ErrorHandlingSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { try { await this.sendWelcomeEmail(event.entity); } catch (error) { console.error('Failed to send welcome email:', error); // Log error, but don't affect main flow await this.logError(error, event.entity); } } private async logError(error: any, user: User) { // Log error to database await event.manager.getRepository(ErrorLog).save({ error: error.message, userId: user.id, timestamp: new Date(), }); } }
4. Performance Considerations
typescript// ✅ Good: Batch processing @EventSubscriber() export class BatchSubscriber implements EntitySubscriberInterface<User> { private batch: User[] = []; private timer: NodeJS.Timeout | null = null; listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // Add to batch this.batch.push(event.entity); // Set timer if (!this.timer) { this.timer = setTimeout(() => { this.processBatch(); }, 1000); // Process after 1 second } } private async processBatch() { if (this.batch.length === 0) { return; } const usersToProcess = [...this.batch]; this.batch = []; this.timer = null; // Batch process await this.sendBatchNotifications(usersToProcess); } private async sendBatchNotifications(users: User[]) { console.log(`Sending notifications to ${users.length} users`); // Batch send notification logic } }
Real-World Use Cases
1. Audit Logging
typescript@EventSubscriber() export class AuditLogSubscriber implements EntitySubscriberInterface { listenTo() { return Object; } afterInsert(event: InsertEvent<any>) { this.logAudit('INSERT', event.entity); } afterUpdate(event: UpdateEvent<any>) { this.logAudit('UPDATE', event.entity, event.updatedColumns); } afterRemove(event: RemoveEvent<any>) { this.logAudit('DELETE', event.entity); } private async logAudit(action: string, entity: any, columns?: any[]) { const auditLog = { action, entityName: entity.constructor.name, entityId: entity.id, changes: columns ? columns.map(col => col.propertyName) : null, timestamp: new Date(), }; await event.manager.getRepository(AuditLog).save(auditLog); } }
2. Cache Invalidation
typescript@EventSubscriber() export class CacheInvalidationSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterUpdate(event: UpdateEvent<User>) { // Clear user cache this.clearUserCache(event.entity.id); // Clear related cache this.clearRelatedCache(event.entity.id); } afterRemove(event: RemoveEvent<User>) { // Clear all related cache this.clearAllUserCache(event.entity.id); } private clearUserCache(userId: number) { // Clear user cache logic console.log(`Clearing cache for user ${userId}`); } private clearRelatedCache(userId: number) { // Clear related cache logic console.log(`Clearing related cache for user ${userId}`); } private clearAllUserCache(userId: number) { // Clear all user cache logic console.log(`Clearing all cache for user ${userId}`); } }
3. Notification System
typescript@EventSubscriber() export class NotificationSubscriber implements EntitySubscriberInterface<Post> { listenTo() { return Post; } afterInsert(event: InsertEvent<Post>) { // Notify followers this.notifyFollowers(event.entity); // Notify author this.notifyAuthor(event.entity); } afterUpdate(event: UpdateEvent<Post>) { // If post is published, notify followers if (this.isPublished(event)) { this.notifyFollowers(event.entity); } } private isPublished(event: UpdateEvent<Post>): boolean { const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('status') && event.entity.status === 'published'; } private async notifyFollowers(post: Post) { // Notify followers logic console.log(`Notifying followers of post ${post.id}`); } private async notifyAuthor(post: Post) { // Notify author logic console.log(`Notifying author of post ${post.id}`); } }
TypeORM's event system provides powerful extensibility. Proper use of events can simplify business logic and improve code maintainability.