6月5日 21:43
How does TypeORM's caching mechanism work? Including query cache, Redis cache, and caching strategies
Caching is an important means of improving application performance. TypeORM provides multiple caching strategies and mechanisms, allowing developers to flexibly manage data caching.
Cache Types
1. Query Cache
TypeORM provides built-in query caching functionality that can cache query results.
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, // Enable query cache cache: { type: 'database', // or 'redis', 'ioredis' duration: 30000, // Cache duration (milliseconds) options: { // Database cache options tableName: 'query_cache', }, }, });
2. Redis Cache
Using Redis as cache storage:
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, cache: { type: 'redis', // or 'ioredis' duration: 30000, options: { host: 'localhost', port: 6379, password: 'password', db: 0, }, }, });
3. Database Cache
Using database table as cache storage:
typescriptconst dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, cache: { type: 'database', duration: 30000, options: { tableName: 'query_cache', }, }, });
Query Cache Usage
Basic Query Cache
typescriptimport { DataSource } from 'typeorm'; // Query with cache enabled const users = await dataSource .getRepository(User) .find({ cache: true, // Enable cache }); // Custom cache duration const users = await dataSource .getRepository(User) .find({ cache: { id: 'users_list', // Cache ID milliseconds: 60000, // Cache for 60 seconds }, });
QueryBuilder Cache
typescript// Enable cache using QueryBuilder const users = await dataSource .getRepository(User) .createQueryBuilder('user') .cache(true) // Enable cache .getMany(); // Custom cache configuration const users = await dataSource .getRepository(User) .createQueryBuilder('user') .cache('user_list', 60000) // Cache ID and time .where('user.age > :age', { age: 18 }) .getMany();
Relation Query Cache
typescript// Cache relation queries const usersWithPosts = await dataSource .getRepository(User) .find({ relations: ['posts'], cache: true, }); // Use QueryBuilder to cache relation queries const usersWithPosts = await dataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .cache('users_with_posts', 30000) .getMany();
Cache Management
Clearing Cache
typescript// Clear specific query cache await dataSource .getRepository(User) .query('CLEAR CACHE'); // Clear all cache await dataSource.queryResultCache?.clear(); // Clear specific cache ID await dataSource.queryResultCache?.remove(['users_list']);
Cache Invalidation Strategy
typescript// Clear related cache after updating data async function updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // Update user await userRepository.update(userId, userData); // Clear related cache await dataSource.queryResultCache?.remove([ 'users_list', 'user_details', ]); } // Use transaction and cache clearing async function updateUserWithCache(userId: number, userData: Partial<User>) { await dataSource.transaction(async (manager) => { const userRepository = manager.getRepository(User); // Update user await userRepository.update(userId, userData); // Clear cache await manager.queryResultCache?.remove([ 'users_list', ]); }); }
Entity Cache
Entity-Level Cache
typescriptimport { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; // Entity cache configuration @Column({ type: 'timestamp' }) cachedAt: Date; } // Use entity cache async function getUserWithCache(userId: number) { const cacheKey = `user:${userId}`; // Try to get from cache const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // Get from database const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // Store in cache await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user; }
Custom Cache Decorator
typescriptimport { Cacheable } from './decorators/Cacheable'; @Entity() export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; @Cacheable({ ttl: 30000 }) // Cache for 30 seconds async getDetails(): Promise<Product> { return this; } } // Use custom cache decorator const product = await productRepository.findOne({ where: { id: 1 } }); const details = await product.getDetails();
Caching Strategies
Read-Write Separation Cache
typescript// Read operations use cache async function getUser(userId: number) { const cacheKey = `user:${userId}`; // Try to read from cache const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // Read from database const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // Store in cache await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user; } // Write operations clear cache async function updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // Update database await userRepository.update(userId, userData); // Clear cache const cacheKey = `user:${userId}`; await dataSource.queryResultCache?.remove([cacheKey]); }
Multi-Level Cache
typescript// Level 1 cache: Memory cache const memoryCache = new Map<string, any>(); // Level 2 cache: Redis cache const redisCache = dataSource.queryResultCache; async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // Try level 1 cache if (memoryCache.has(cacheKey)) { return memoryCache.get(cacheKey); } // Try level 2 cache const cached = await redisCache?.get(cacheKey); if (cached) { const product = JSON.parse(cached); memoryCache.set(cacheKey, product); return product; } // Get from database const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // Store in level 2 cache await redisCache?.store(cacheKey, JSON.stringify(product), 60000); // Store in level 1 cache memoryCache.set(cacheKey, product); return product; }
Cache Warmup
typescript// Warmup cache on application startup async function warmupCache() { // Warmup popular users const popularUsers = await dataSource .getRepository(User) .createQueryBuilder('user') .where('user.followersCount > :count', { count: 1000 }) .limit(100) .getMany(); for (const user of popularUsers) { const cacheKey = `user:${user.id}`; await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 300000 // 5 minutes ); } // Warmup popular posts const popularPosts = await dataSource .getRepository(Post) .createQueryBuilder('post') .where('post.views > :views', { views: 1000 }) .limit(100) .getMany(); for (const post of popularPosts) { const cacheKey = `post:${post.id}`; await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(post), 300000 ); } } // Call on application startup warmupCache();
Caching Best Practices
1. Choose Appropriate Caching Strategy
typescript// ✅ Good: Use different caching strategies for different data types // Static data: Long cache time const staticData = await dataSource .getRepository(Category) .find({ cache: { milliseconds: 3600000, // 1 hour }, }); // Dynamic data: Short cache time const dynamicData = await dataSource .getRepository(Post) .find({ cache: { milliseconds: 30000, // 30 seconds }, }); // Real-time data: No cache const realTimeData = await dataSource .getRepository(OnlineUser) .find({ cache: false, });
2. Cache Key Naming Convention
typescript// ✅ Good: Use clear cache key naming const cacheKeys = { user: (id: number) => `user:${id}`, userList: (page: number) => `user_list:page:${page}`, userPosts: (userId: number) => `user:${userId}:posts`, popularPosts: () => `posts:popular`, }; // Usage example await dataSource.queryResultCache?.store( cacheKeys.user(1), JSON.stringify(user), 60000 );
3. Cache Invalidation Timing
typescript// ✅ Good: Clear cache promptly when data changes class UserService { async updateUser(userId: number, userData: Partial<User>) { const userRepository = dataSource.getRepository(User); // Update data await userRepository.update(userId, userData); // Clear related cache await this.clearUserCache(userId); } async clearUserCache(userId: number) { const cacheKeys = [ `user:${userId}`, `user:${userId}:posts`, `user:${userId}:comments`, ]; await dataSource.queryResultCache?.remove(cacheKeys); } }
4. Monitor Cache Performance
typescript// Cache performance monitoring class CacheMonitor { private hits = 0; private misses = 0; async get<T>(key: string): Promise<T | null> { const cached = await dataSource.queryResultCache?.get(key); if (cached) { this.hits++; return JSON.parse(cached); } this.misses++; return null; } getHitRate(): number { const total = this.hits + this.misses; return total > 0 ? this.hits / total : 0; } getStats() { return { hits: this.hits, misses: this.misses, hitRate: this.getHitRate(), }; } } // Use monitoring const monitor = new CacheMonitor(); async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // Try to get from cache const cached = await monitor.get<Product>(cacheKey); if (cached) { return cached; } // Get from database const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // Store in cache await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), 60000 ); return product; } // Periodically output cache statistics setInterval(() => { console.log('Cache Stats:', monitor.getStats()); }, 60000);
Advanced Caching Techniques
1. Cache Penetration Protection
typescript// Use null value caching to prevent cache penetration async function getUser(userId: number) { const cacheKey = `user:${userId}`; // Try to get from cache const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { const user = JSON.parse(cached); // Check if it's a null value marker if (user === null) { return null; } return user; } // Get from database const user = await dataSource.getRepository(User).findOne({ where: { id: userId }, }); // Store in cache (including null values) await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(user), 60000 ); return user; }
2. Cache Avalanche Protection
typescript// Use random cache time to prevent cache avalanche async function getProduct(productId: number) { const cacheKey = `product:${productId}`; // Try to get from cache const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // Get from database const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // Use random cache time (30-60 seconds) const cacheTime = 30000 + Math.random() * 30000; // Store in cache await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), cacheTime ); return product; }
3. Cache Breakdown Protection
typescript// Use mutex lock to prevent cache breakdown const locks = new Map<string, Promise<any>>(); async function getHotProduct(productId: number) { const cacheKey = `product:${productId}`; // Try to get from cache const cached = await dataSource.queryResultCache?.get(cacheKey); if (cached) { return JSON.parse(cached); } // Check if there's a lock if (locks.has(cacheKey)) { return await locks.get(cacheKey); } // Create lock const promise = (async () => { try { // Get from database const product = await dataSource.getRepository(Product).findOne({ where: { id: productId }, }); // Store in cache await dataSource.queryResultCache?.store( cacheKey, JSON.stringify(product), 60000 ); return product; } finally { // Release lock locks.delete(cacheKey); } })(); locks.set(cacheKey, promise); return await promise; }
TypeORM's caching mechanisms provide powerful performance optimization capabilities. Proper use of caching can significantly improve application performance.