标签

TypeORM

TypeORM 是一个面向对象的关系型数据库ORM框架,用于在 Node.js 应用程序中操作数据库。它支持多种数据库,包括 MySQL,PostgreSQL,SQLite,以及 Microsoft SQL Server 等。TypeORM 提供了使用 TypeScript 的完整ORM解决方案,它的主要目标是简化数据库操作,提高开发效率。

TypeORM
查看更多相关内容
服务端6月5日 22:02
TypeORM 查询该用 find 还是 QueryBuilder?三种方式适用场景对比TypeORM 查询数据有三条路:`find` 系列方法、`QueryBuilder`、原生 SQL。很多人上来就用 QueryBuilder,其实 80% 的查询 `find` 就够了——更简洁、类型安全、不容易出错。这篇把三种方式的适用边界和常见坑讲清楚。 ## find:日常查询的首选 ### 基础查询 ```typescript // 查所有 const users = await userRepository.find(); // 按 ID 查一条 const user = await userRepository.findOne({ where: { id: 1 } }); // 按条件查 const activeUsers = await userRepository.find({ where: { active: true }, }); ``` ### 条件组合 ```typescript import { And, Or, LessThan, MoreThan, Like, Between, In, IsNull } from 'typeorm'; // AND 条件——多个字段自动 AND const users = await userRepository.find({ where: { active: true, age: MoreThan(18), }, }); // OR 条件——数组内自动 OR const users = await userRepository.find({ where: [ { role: 'admin' }, { age: MoreThan(30) }, ], }); // 常用 FindOperator const users = await userRepository.find({ where: { name: Like('%john%'), // 模糊搜索 id: In([1, 2, 3]), // IN 查询 createdAt: Between(start, end), // 范围查询 deletedAt: IsNull(), // IS NULL }, }); ``` **find 的 OR 有个坑**:数组里每个元素是一个独立的 OR 分支,不是字段级别的 OR。如果你需要 `role = 'admin' AND (age > 30 OR name LIKE '%john%')` 这种混合逻辑,find 写不出来——得上 QueryBuilder。 ### 选择字段 ```typescript const users = await userRepository.find({ select: { id: true, name: true, email: true, // 不选 password 等敏感字段 }, where: { active: true }, }); ``` ### 排序和分页 ```typescript const users = await userRepository.find({ where: { active: true }, order: { createdAt: 'DESC' }, skip: 0, take: 20, }); // 带总数的分页——一次查询拿到数据 + 总数 const [users, total] = await userRepository.findAndCount({ where: { active: true }, order: { createdAt: 'DESC' }, skip: (page - 1) * pageSize, take: pageSize, }); ``` ### 加载关联 ```typescript // 简单关联 const users = await userRepository.find({ relations: { posts: true, profile: true }, }); // 嵌套关联 const users = await userRepository.find({ relations: { posts: { comments: true } }, }); ``` ### find 的 N+1 陷阱 `find` 加载关联时会发多条 SQL——每个关联单独查一次。如果查 100 个用户且关联了 posts,可能产生几十条查询: ```typescript // 可能触发 N+1 const users = await userRepository.find({ relations: { posts: true }, }); // SQL 1: SELECT * FROM user // SQL 2: SELECT * FROM post WHERE userId IN (1, 2, 3, ... 100) ``` TypeORM 0.3+ 已经优化了这个问题——会用 `IN` 批量查而不是逐个查。但如果关联层级很深(用户 → 文章 → 评论 → 作者),查询数仍然会膨胀。深层关联建议用 QueryBuilder 的 `leftJoinAndSelect` 一次性 JOIN。 ## QueryBuilder:find 搞不定的场景 ### 关联查询带条件过滤 `find` 的 `relations` 只能全量加载,不能过滤。QueryBuilder 可以: ```typescript // 只查有已发布文章的用户 const users = await userRepository .createQueryBuilder('user') .innerJoinAndSelect( 'user.posts', 'post', 'post.isPublished = :published', { published: true }, ) .getMany(); ``` 第三个参数是 JOIN 条件——`find` 做不到这个。 ### 条件组合:AND + OR + 括号 ```typescript const users = await userRepository .createQueryBuilder('user') .where('user.active = :active', { active: true }) .andWhere( new Brackets((qb) => { qb.where('user.role = :admin', { admin: 'admin' }).orWhere( 'user.createdAt > :date', { date: '2024-01-01' }, ); }), ) .getMany(); // WHERE active = true AND (role = 'admin' OR createdAt > '2024-01-01') ``` `Brackets` 生成括号——保证 OR 的优先级正确。没有它,`AND` 和 `OR` 的优先级会让你拿到错误的结果。 ### 聚合查询 ```typescript const stats = await userRepository .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .addSelect('AVG(user.age)', 'avgAge') .groupBy('user.role') .getRawMany(); // 返回: [{ role: 'admin', count: '5', avgAge: '32.5' }, ...] ``` 聚合查询必须用 `getRawMany()`——返回原始数据库行,字段类型都是字符串。`getMany()` 返回实体对象,但聚合结果不是实体,强用会出错。 ### 子查询 ```typescript const users = await userRepository .createQueryBuilder('user') .where((qb) => { const subQuery = qb .subQuery() .select('post.authorId') .from(Post, 'post') .groupBy('post.authorId') .having('COUNT(post.id) > :count') .getQuery(); return `user.id IN ${subQuery}`; }) .setParameter('count', 5) .getMany(); ``` ### QueryBuilder 的类型安全坑 `find` 是完全类型安全的——`where` 里的字段名写错了 TypeScript 直接报错。QueryBuilder 的 SQL 片段是字符串,写错了只有运行时才知道: ```typescript // 字段名拼错了,TypeScript 不会报错,运行时才炸 .where('user.actve = :active', { active: true }) // ↑ typo: active vs actve ``` **建议**:QueryBuilder 的 SQL 片段尽量短,把条件值用参数传递(`:param`),减少字符串拼写出错的可能。 ## 原生 SQL:最后的手段 QueryBuilder 也搞不定时,直接写 SQL: ```typescript // 查询 const result = await dataSource.query( 'SELECT * FROM user WHERE created_at > $1', [startDate], ); // 事务中执行 const result = await queryRunner.query( 'UPDATE user SET last_login = NOW() WHERE id = $1', [userId], ); ``` **什么时候用原生 SQL**: - 数据库特有的函数或语法(如 PostgreSQL 的 `JSONB` 操作、`UPSERT`) - 复杂的窗口函数(`ROW_NUMBER() OVER (...)`) - 需要极致优化的性能关键查询 - 迁移老项目的 SQL 不想重写 **注意**:`$1` 是 PostgreSQL 占位符,MySQL 用 `?`。原生 SQL 没有方言抽象——换数据库要手动改。 ## 性能对比 同样的查询,三种方式的性能差异: ```typescript // 1. find —— 最慢(多条 SQL + 实体转换开销) const users = await userRepository.find({ where: { active: true }, relations: { posts: true } }); // 2. QueryBuilder —— 中等(一条 JOIN SQL,但仍有实体转换) const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.active = :active', { active: true }) .getMany(); // 3. 原生 SQL —— 最快(一条 SQL,跳过实体转换) const rows = await dataSource.query( 'SELECT u.*, p.id as post_id, p.title FROM user u LEFT JOIN post p ON p.userId = u.id WHERE u.active = $1', [true], ); ``` 差距不大时(毫秒级),优先用 `find` 或 QueryBuilder——可维护性和类型安全比那几毫秒更有价值。只有查询确实是瓶颈时才用原生 SQL 优化。 ## 选择决策 | 场景 | 用什么 | 原因 | |------|--------|------| | 单表简单查询 | `find` | 简洁、类型安全 | | 单表条件组合 | `find` + FindOperator | `In`、`Like`、`Between` 够用 | | 多表关联 + 过滤 | QueryBuilder | `find` 的 relations 不能加条件 | | 聚合/分组 | QueryBuilder | `find` 不支持 GROUP BY | | 子查询 | QueryBuilder | `find` 不支持 | | 复杂 OR + AND 混合 | QueryBuilder | `find` 的 OR 只支持数组级 | | 数据库特有语法 | 原生 SQL | TypeORM 不覆盖所有特性 | | 性能关键查询 | 原生 SQL | 跳过实体转换 | **经验法则**:先试 `find`,搞不定再上 QueryBuilder,最后才用原生 SQL。层级越低灵活性越高,但可维护性和类型安全越差。
服务端6月5日 21:43
TypeORM 查询缓存实战:Redis 配置、主动失效和策略选择数据库查询是后端应用最常见的性能瓶颈。TypeORM 内置了查询缓存,支持内存缓存和 Redis 缓存两种存储后端,能在不改动业务代码的情况下大幅降低数据库负载。这篇讲清楚怎么配、怎么用、以及缓存策略的选择。 ## 两种缓存存储:数据库表 vs Redis ### 默认方案:数据库表缓存 不配置任何东西,TypeORM 默认用数据库的一张表存缓存: ```typescript const dataSource = new DataSource({ type: 'mysql', host: 'localhost', username: 'root', password: 'password', database: 'myapp', cache: true, // 开启缓存,默认 1000ms 过期 }) ``` TypeORM 会自动创建 `query-result-cache` 表,把查询 SQL 和结果序列化后存进去。下次同样的查询直接从这张表取,不执行 SQL。 **问题**:缓存本身也存数据库里——等于用数据库查数据库,只是从业务表换到了缓存表。单实例够用,分布式部署时每个实例有自己的缓存表,互相看不到。 ### 推荐方案:Redis 缓存 ```typescript const dataSource = new DataSource({ type: 'mysql', cache: { type: 'redis', options: { host: 'localhost', port: 6379, password: 'redis-password', db: 0, }, duration: 30000, // 默认缓存 30 秒 }, }) ``` Redis 的优势: - **快**:内存读取,微秒级延迟 - **共享**:多个应用实例访问同一个 Redis,缓存一致 - **可控**:Redis 的内存管理、过期策略、持久化都很成熟 Redis 集群场景用 ioredis: ```typescript cache: { type: 'ioredis/cluster', options: { startupNodes: [ { host: '10.0.0.1', port: 7000 }, { host: '10.0.0.2', port: 7000 }, { host: '10.0.0.3', port: 7000 }, ], }, } ``` ## 查询级缓存:精确控制哪些查询缓存 全局开缓存后,不是所有查询都会缓存——需要显式指定。 ### Repository 方式 ```typescript // 缓存 30 秒 const users = await userRepository.find({ cache: 30000, }) // 给缓存一个 ID,方便后续清除 const users = await userRepository.find({ cache: { id: 'users_list', milliseconds: 30000, }, }) // findAndCount 也支持 const [users, count] = await userRepository.findAndCount({ cache: { id: 'users_paginated', milliseconds: 30000, }, }) ``` ### QueryBuilder 方式 ```typescript const posts = await dataSource .createQueryBuilder(Post, 'post') .where('post.isPublished = :published', { published: true }) .cache('published_posts', 60000) // 缓存 ID + 过期时间 .getMany() ``` ### 缓存 ID 的作用 缓存 ID 是手动控制缓存的关键——通过 ID 可以精确清除某类查询的缓存: ```typescript // 清除指定 ID 的缓存 await dataSource.queryResultCache.remove(['users_list']) // 数据变更后清除相关缓存 async createUser(dto: CreateUserDto) { const user = await this.userRepo.save(dto) // 用户列表缓存失效 await this.dataSource.queryResultCache.remove(['users_list', 'users_paginated']) return user } ``` **原则**:所有需要缓存的查询都应该指定 ID,否则你无法在数据变更时精确失效缓存——只能等过期。 ## 缓存策略选择 ### 按数据变化频率决定缓存时长 | 数据特征 | 缓存时长 | 例子 | |---------|---------|------| | 几乎不变 | 5-30 分钟 | 省份列表、配置项 | | 偶尔变化 | 30-60 秒 | 文章列表、商品分类 | | 频繁变化 | 5-15 秒 | 实时排行榜、库存 | | 实时性要求高 | 不缓存 | 支付状态、账户余额 | ### 主动失效 vs 被动过期 - **被动过期**:设一个 `duration`,到期自动清除。简单,但数据变更后到过期前这段时间,用户看到的可能是旧数据 - **主动失效**:数据变更时手动 `remove()` 缓存 ID。更精确,但代码更复杂 生产环境推荐**两者结合**:设一个较长的 `duration` 做兜底,数据变更时主动失效。这样即使忘了失效,缓存最多存在 `duration` 时间也不会永远不过期。 ```typescript // 查询时设较长缓存 const users = await this.userRepo.find({ cache: { id: 'users_list', milliseconds: 300000 }, // 5 分钟兜底 }) // 变更时主动失效 async updateUser(id: number, dto: UpdateUserDto) { await this.userRepo.update(id, dto) await this.dataSource.queryResultCache.remove(['users_list']) } ``` ## 缓存与事务 TypeORM 的查询缓存在事务内**不会自动失效**。这可能导致一个问题: ```typescript // 事务外查询 → 走缓存 const user = await userRepo.findOne({ where: { id: 1 }, cache: 30000 }) // 事务内更新 await dataSource.transaction(async (manager) => { await manager.update(User, 1, { name: 'new name' }) // 此时缓存里还是旧数据! }) // 事务提交后再查 → 可能还是缓存旧数据 ``` 解决方案:事务提交后手动清除缓存: ```typescript await dataSource.transaction(async (manager) => { await manager.update(User, 1, { name: 'new name' }) }) await dataSource.queryResultCache.remove(['user_1']) ``` ## 缓存清理 ### 清除指定缓存 ```typescript // 按 ID 清除 await dataSource.queryResultCache.remove(['users_list', 'posts_list']) // 清除所有缓存 await dataSource.queryResultCache.clear() ``` ### 命令行清除 ```bash npx typeorm cache:clear ``` ### 自动清理 Redis 的 TTL 机制会自动清理过期缓存,不需要手动管理。数据库表缓存 TypeORM 也会定期清理过期记录。 ## ignoreErrors:缓存降级 缓存不可用时不应该让业务请求也失败: ```typescript cache: { type: 'redis', options: { host: 'localhost', port: 6379 }, ignoreErrors: true, // Redis 挂了不报错,直接查数据库 } ``` `ignoreErrors: true` 是生产环境必须加的——Redis 重启或网络抖动时,查询会降级到直接访问数据库,而不是直接 500。 ## 完整配置示例 ```typescript // data-source.ts import { DataSource } from 'typeorm' export const dataSource = new DataSource({ type: 'mysql', host: process.env.DB_HOST, port: 3306, username: process.env.DB_USER, password: process.env.DB_PASS, database: 'myapp', cache: { type: 'redis', options: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: 0, }, duration: 30000, // 默认 30 秒 ignoreErrors: true, // 缓存故障降级 tableName: 'query_result_cache', // 数据库表缓存的表名(如果用数据库缓存) }, }) ``` ## 缓存策略速查 | 场景 | 策略 | |------|------| | 配置数据、字典表 | 长缓存(5 分钟+)+ 变更时手动失效 | | 列表查询 | 中缓存(30-60 秒)+ 分页参数加入缓存 ID | | 详情页 | 短缓存(15-30 秒)+ 更新时失效 | | 实时数据 | 不缓存,或 5 秒极短缓存 | | 多实例部署 | 必须用 Redis,数据库表缓存不共享 | | 缓存故障容忍 | `ignoreErrors: true` | | 事务内更新 | 事务提交后手动清除缓存 |
服务端6月5日 13:57
TypeORM事务基础:三种写法、常见陷阱和NestJS集成数据库事务保证一组操作要么全部成功、要么全部回滚——转账是最经典的例子:A 扣 100 元和 B 加 100 元必须在同一个事务里,不能只执行一半。TypeORM 提供三种写事务的方式,这篇文章把每种的使用场景和容易踩的坑讲清楚。 ## 为什么需要事务 不用事务会怎样: ```typescript // ❌ 没有事务保护 async function transfer(fromId: number, toId: number, amount: number) { const from = await userRepo.findOne({ where: { id: fromId } }); from.balance -= amount; await userRepo.save(from); // 成功 const to = await userRepo.findOne({ where: { id: toId } }); to.balance += amount; await userRepo.save(to); // 如果这里报错,A 扣了钱但 B 没收到 } ``` 事务保证:要么两步都成功,要么两步都回滚。 ## 方式一:DataSource.transaction()(日常首选) 最简洁的写法——传一个回调,回调里所有操作自动在一个事务里: ```typescript await dataSource.transaction(async (manager) => { const from = await manager.findOne(User, { where: { id: fromId } }); const to = await manager.findOne(User, { where: { id: toId } }); if (from.balance < amount) { throw new Error('余额不足'); // 抛异常 → 自动 rollback } from.balance -= amount; to.balance += amount; await manager.save([from, to]); // 正常返回 → 自动 commit }); ``` **最关键的规则**:回调里必须用 `manager` 参数操作数据库,不能用全局的 `userRepository`。 ```typescript // ❌ 错误:全局 repository 不在事务里 await dataSource.transaction(async (manager) => { await manager.save(from); // 在事务里 ✅ await userRepository.save(to); // 不在事务里 ❌ }); ``` 全局 Repository 拿到的连接不在事务的连接上,它的操作不受事务保护。这是最常见的事务 bug——代码不报错,但数据不一致。 ### 回调正常返回 → commit ```typescript await dataSource.transaction(async (manager) => { await manager.save(user); // 没有 throw → 自动 commit }); ``` ### 回调抛异常 → rollback ```typescript await dataSource.transaction(async (manager) => { await manager.save(user); throw new Error('出错了'); // → 自动 rollback,上面的 save 被撤销 }); ``` 不需要手动 commit 或 rollback——TypeORM 自动处理。 ## 方式二:QueryRunner(精细控制) 当你需要手动控制 commit/rollback 时机、设隔离级别、或同一个连接上跑多个事务时: ```typescript const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); // 获取数据库连接 await queryRunner.startTransaction(); // 开启事务 try { await queryRunner.manager.save(user1); await queryRunner.manager.save(user2); // 可以在这里做其他判断,决定 commit 还是 rollback if (someCondition) { await queryRunner.commitTransaction(); } else { await queryRunner.rollbackTransaction(); } } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); // 必须释放连接! } ``` `release()` 是必须的——QueryRunner 持有数据库连接,不释放会泄漏连接池。`finally` 块保证即使 commit 失败也能释放。 ### 什么时候用 QueryRunner - 需要手动控制 commit 时机(如根据业务逻辑决定) - 需要设置隔离级别:`queryRunner.startTransaction('SERIALIZABLE')` - 需要在同一个连接上执行原生 SQL 和 ORM 操作 - 批量操作需要分批 commit(避免单个大事务锁表太久) ### 分批 commit 的模式 ```typescript const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); const users = await queryRunner.manager.find(User, { take: 10000 }); for (let i = 0; i < users.length; i += 1000) { await queryRunner.startTransaction(); try { const batch = users.slice(i, i + 1000); for (const user of batch) { user.processed = true; await queryRunner.manager.save(user); } await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); } } await queryRunner.release(); ``` 每 1000 条一个事务,而不是 10000 条一个大事务。大事务持有锁太久会阻塞其他查询。 ## 方式三:NestJS + TypeORM NestJS 项目里注入 DataSource 用事务: ```typescript import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; @Injectable() export class OrderService { constructor( @InjectDataSource() private dataSource: DataSource, ) {} async createOrder(dto: CreateOrderDto) { return this.dataSource.transaction(async (manager) => { const order = manager.create(Order, dto); await manager.save(order); for (const item of dto.items) { const product = await manager.findOne(Product, { where: { id: item.productId }, }); product.stock -= item.quantity; await manager.save(product); } return order; }); } } ``` ## 常见陷阱 ### 事务里调用了事务外的方法 ```typescript // ❌ 事务里调用了不在事务里的方法 async function updateProfile(userId: number) { await userRepo.update(userId, { name: 'new' }); // 不在事务里 } await dataSource.transaction(async (manager) => { await manager.save(order); await updateProfile(userId); // 这步不在事务保护下! }); ``` 解决:把需要事务保护的操作都放在回调里,或者把 `manager` 传进去: ```typescript async function updateProfile(manager: EntityManager, userId: number) { await manager.update(User, userId, { name: 'new' }); } await dataSource.transaction(async (manager) => { await manager.save(order); await updateProfile(manager, userId); // ✅ 在事务里 }); ``` ### 事务里做了网络请求 ```typescript // ❌ 事务里调第三方 API await dataSource.transaction(async (manager) => { await manager.save(order); await fetch('https://payment-gateway/charge', { ... }); // 网络请求可能超时 await manager.save(payment); }); ``` 网络请求可能耗时几秒甚至超时——事务期间数据库锁一直持有,其他请求被阻塞。正确做法:先完成数据库事务,再调外部 API,失败时用补偿机制回滚。 ### 忘记 await ```typescript // ❌ 没有 await,事务还没 commit 就返回了 await dataSource.transaction(async (manager) => { manager.save(user); // 没有 await! }); ``` `manager.save()` 返回 Promise,没有 `await` 的话,事务 commit 时 save 可能还没执行完。TypeScript 的 `async` 函数里忘记 `await` 不会报错——这是个无声的 bug。 ## 事务 vs 批量操作 不是所有操作都需要事务。批量插入、批量更新如果不需要原子性,可以不用事务——更快: ```typescript // 不需要事务:批量插入日志 await logRepo.insert(logs); // 需要事务:转账(必须保证原子性) await dataSource.transaction(async (manager) => { // ... }); ``` 判断标准:这些操作是否必须"全部成功或全部失败"?是 → 用事务,否 → 不用。
服务端6月5日 13:56
TypeORM迁移完整指南:自动生成、数据迁移和生产部署策略`synchronize: true` 在开发时很方便——改实体自动同步表结构。但生产环境这么做会丢数据:删字段时直接 DROP COLUMN,重命名字段被当作"删旧的加新的"。迁移(Migration)是生产环境管理数据库结构变更的唯一正确方式。这篇文章把迁移的完整流程和常见坑都讲清楚。 ## 迁移的工作原理 迁移就是一个类,有 `up()` 和 `down()` 两个方法——`up()` 执行变更,`down()` 回滚变更。TypeORM 按顺序执行迁移文件,并在数据库里记录哪些已经跑过。 ``` 数据表 _migration: ┌──────────────────────────────┬────────────┐ │ id │ timestamp │ ├──────────────────────────────┼────────────┤ │ InitSchema1700000000000 │ 2024-01-01 │ │ AddUserEmail1700000000001 │ 2024-01-02 │ └──────────────────────────────┴────────────┘ ``` 每次 `migration:run`,TypeORM 对比已执行的迁移和待执行的迁移文件,只跑新的。 ## 创建迁移 ### 方式一:自动生成(推荐) 修改实体后,让 TypeORM 自动对比生成迁移: ```bash npx typeorm migration:generate src/migration/AddUserEmail -d src/data-source.ts ``` TypeORM 会对比实体定义和当前数据库结构,生成差量的迁移文件。这是最安全的方式——不会漏字段、不会写错类型。 前提:数据库必须和当前代码的实体一致(上次迁移已执行)。如果数据库和实体不同步,生成会报错。 ### 方式二:手动创建 ```bash npx typeorm migration:create src/migration/AddUserEmail ``` 创建空模板,自己写 SQL: ```typescript import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserEmail1700000000001 implements MigrationInterface { name = 'AddUserEmail1700000000001'; public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn( 'user', new TableColumn({ name: 'email', type: 'varchar', length: '255', isUnique: true, }), ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'email'); } } ``` 手动迁移用于自动生成搞不定的场景:数据迁移(把数据从一列搬到另一列)、复杂的 schema 重构。 ## 执行迁移 ```bash # 执行所有未执行的迁移 npx typeorm migration:run -d src/data-source.ts # 回滚最后一次迁移 npx typeorm migration:revert -d src/data-source.ts # 查看迁移状态 npx typeorm migration:show -d src/data-source.ts ``` `migration:show` 输出: ``` [X] InitSchema1700000000000 # 已执行 [X] AddUserEmail1700000000001 # 已执行 [ ] AddPostTags1700000000002 # 未执行 ``` ### 在 NestJS 里执行 ```typescript // main.ts 或专门的迁移脚本 import { DataSource } from 'typeorm'; import { AppDataSource } from './data-source'; async function runMigrations() { await AppDataSource.initialize(); await AppDataSource.runMigrations(); await AppDataSource.destroy(); } runMigrations(); ``` 或在启动时自动跑: ```typescript async function bootstrap() { const app = await NestFactory.create(AppModule); // 启动时自动执行迁移 const dataSource = app.get(DataSource); await dataSource.runMigrations(); await app.listen(3000); } ``` **注意**:生产环境自动跑迁移有风险——如果迁移有 bug,服务启动就失败。更安全的做法是在部署流程里单独跑迁移,确认成功后再部署新代码。 ## 常见迁移场景 ### 加列 ```typescript public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn('user', new TableColumn({ name: 'email', type: 'varchar', isNullable: true, // 先允许 NULL,后续填充数据后再设 NOT NULL })); } ``` **安全做法**:新列先 `isNullable: true`,应用层填充数据后,再用另一个迁移改为 `NOT NULL`。直接 `NOT NULL` 会导致已有行插入失败。 ### 删列 ```typescript public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'deprecatedField'); } ``` 删列不可逆——数据丢了就没了。确保没有代码引用这个列后再删。建议先在代码里移除对列的引用,部署一版,确认没有报错,再加迁移删列。 ### 改列类型 ```typescript public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn('user', 'age', new TableColumn({ name: 'age', type: 'int', // 从 smallint 改为 int isNullable: false, })); } ``` 类型变更可能导致数据丢失——`varchar(255)` 改 `varchar(50)` 会截断数据。改之前检查现有数据是否都在新范围内。 ### 重命名列 ```typescript public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.renameColumn('user', 'name', 'fullName'); } ``` `renameColumn` 比 "删旧列+加新列" 安全——它用 `ALTER TABLE RENAME COLUMN`,数据不丢失。 ### 数据迁移 纯 schema 迁移只改表结构,不改数据。数据迁移需要在 `up()` 里写 SQL: ```typescript public async up(queryRunner: QueryRunner): Promise<void> { // 把 name 拆成 firstName 和 lastName await queryRunner.addColumn('user', new TableColumn({ name: 'firstName', type: 'varchar', isNullable: true })); await queryRunner.addColumn('user', new TableColumn({ name: 'lastName', type: 'varchar', isNullable: true })); // 数据迁移 await queryRunner.query(` UPDATE "user" SET "firstName" = split_part("name", ' ', 1), "lastName" = split_part("name", ' ', 2) `); // 数据填充完后删除旧列 await queryRunner.dropColumn('user', 'name'); } ``` 数据迁移要注意性能——百万级表的 UPDATE 可能跑几十分钟。大表迁移分批执行: ```typescript // 分批更新,每批 1000 行 await queryRunner.query(` UPDATE "user" SET "firstName" = split_part("name", ' ', 1) WHERE "firstName" IS NULL LIMIT 1000 `); ``` ## 迁移最佳实践 1. **每个迁移只做一件事**——加列是一个迁移,改类型是另一个。出错时可以精确回滚 2. **先加列后删列**——加列不影响现有代码,删列必须先改代码再跑迁移 3. **生产迁移先在 staging 测试**——同样的迁移在测试环境跑一遍确认没有报错 4. **永远不要手动修改 `_migration` 表**——TypeORM 靠它判断哪些迁移已执行,手动改会导致迁移混乱 5. **迁移文件提交到 git**——团队成员 pull 后跑 `migration:run` 就能同步数据库结构 6. **`down()` 必须正确实现**——回滚时 `down()` 是唯一的恢复手段 7. **部署流程里迁移先行**——先跑迁移再部署新代码,避免新代码期望新列但列还不存在
服务端6月5日 13:55
TypeORM N+1查询:relations、leftJoinAndSelect和DataLoader方案对比N+1 查询是 ORM 里最经典的性能坑:查 100 个用户,再查 100 次每个用户的文章——1 次主查询 + N 次关联查询 = N+1 次数据库往返。TypeORM 默认不加载关联数据,所以 N+1 不是 bug 而是默认行为——你得主动告诉 TypeORM 你要哪些关联数据。这篇文章讲清楚 N+1 怎么产生的、怎么解决、以及各种方案的取舍。 ## N+1 是怎么产生的 ```typescript // 查 100 个用户:1 次 SQL const users = await userRepository.find(); // 每个用户查文章:100 次 SQL for (const user of users) { user.posts = await postRepository.find({ where: { authorId: user.id } }); } // 总共 101 次数据库查询 ``` 100 个用户 101 次查询,1000 个用户 1001 次——线性增长。数据库连接池很快耗尽,API 响应从 50ms 飙到 5s。 ## 解决方案一:relations 选项 最简单的方案,在 `find` 时声明要加载的关联: ```typescript const users = await userRepository.find({ relations: ['posts', 'profile'], }); ``` 生成的 SQL: ```sql SELECT * FROM user; SELECT * FROM post WHERE authorId IN (1, 2, 3, ...); SELECT * FROM profile WHERE userId IN (1, 2, 3, ...); ``` 3 条 SQL——不管有多少用户。TypeORM 先查主表,拿 ID 列表,再用 `IN` 查关联表,最后在内存里组装关系。 ### 嵌套关联 ```typescript const users = await userRepository.find({ relations: ['posts', 'posts.comments'], }); ``` 加载用户的文章,以及文章的评论。每多一层嵌套多一条 SQL,但仍是固定数量。 ### 只加载部分关联字段 ```typescript const users = await userRepository.find({ relations: ['posts'], select: { id: true, name: true, posts: { id: true, title: true, // 只加载文章的 id 和 title }, }, }); ``` ## 解决方案二:leftJoinAndSelect QueryBuilder 版本,更灵活: ```typescript const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('user.profile', 'profile') .getMany(); ``` 和 `relations` 的区别: | | relations | leftJoinAndSelect | |---|---|---| | SQL 数量 | 多条(主查询 + 每个关联一条) | 一条(JOIN 合并) | | 大数据量性能 | 更好(IN 查询,无重复行) | 可能差(JOIN 产生笛卡尔积) | | 灵活性 | 只能加载,不能过滤 | 可以加 WHERE、ORDER BY | | 去重 | 自动 | 自动(TypeORM 处理) | **什么时候用 leftJoinAndSelect**:需要对关联数据做过滤或排序时。 ```typescript // 只加载已发布的文章 const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.published = :published', { published: true }) .getMany(); ``` 第三个参数是 JOIN 条件——只有 published=true 的文章会被加载。`relations` 做不到这个。 **什么时候用 relations**:只是加载数据不过滤时。`relations` 在数据量大时性能更好——JOIN 1000 个用户每人 10 篇文章会产生 10000 行中间结果,`IN` 查询只有 1000 + 10 = 1010 行。 ## 解决方案三:Eager 加载 在实体定义里设置 `eager: true`,每次查用户自动加载文章: ```typescript @Entity() export class User { @OneToMany(() => Post, post => post.author, { eager: true }) posts: Post[]; } ``` ```typescript // 自动加载 posts,不需要显式声明 const users = await userRepository.find(); ``` **不推荐**。`eager: true` 让你失去控制——有时候你只需要用户名,结果把文章也查出来了。而且 eager 关联嵌套时,层层加载,性能不可预测。 只在一种场景下用 eager:关联数据**总是**需要一起加载。比如 `User` 和 `UserProfile`(一对一,用户资料总是要一起展示的)。 ## 解决方案四:DataLoader(GraphQL 场景) GraphQL 的查询深度不确定——客户端可能查用户文章,也可能不查。用 `relations` 或 `leftJoinAndSelect` 会过度加载。DataLoader 按需批量加载: ```typescript import DataLoader from 'dataloader'; const postLoader = new DataLoader(async (authorIds: number[]) => { // 一次查出所有作者的文章 const posts = await postRepository.find({ where: { authorId: In(authorIds) }, }); // 按 authorId 分组 return authorIds.map(id => posts.filter(p => p.authorId === id)); }); // 在 resolver 里使用 const resolvers = { User: { posts: (user) => postLoader.load(user.id), }, }; ``` DataLoader 自动合并同一个请求里的多次 `load()` 调用——10 个用户查文章,只触发 1 次数据库查询。 ## 常见陷阱 ### 忘记加 relations 就访问关联属性 ```typescript const users = await userRepository.find(); console.log(users[0].posts); // undefined 或 [] ``` 没加 `relations`,关联属性不会自动加载。TypeORM 不会报错——它返回 `undefined` 或空数组,你的代码以为没有数据。 ### leftJoin vs leftJoinAndSelect ```typescript // leftJoin:只 JOIN 不过 SELECT,关联数据不返回 .leftJoin('user.posts', 'post') // leftJoinAndSelect:JOIN 并且 SELECT,关联数据返回 .leftJoinAndSelect('user.posts', 'post') ``` 用了 `leftJoin` 但没 `addSelect`,关联属性始终为空——很多人卡在这里。 ### 过度加载 ```typescript // ❌ 加载了所有关联,但只需要文章数 const users = await userRepository.find({ relations: ['posts'] }); const postCounts = users.map(u => u.posts.length); // ✅ 用子查询只拿计数 const users = await userRepository .createQueryBuilder('user') .loadRelationCountAndMap('user.postCount', 'user.posts') .getMany(); ``` `loadRelationCountAndMap` 只查 COUNT,不加载完整的关联数据——1000 个用户只需 1 条额外的 COUNT 查询。
服务端6月5日 00:31
TypeORM事务处理:三种API、隔离级别、悲观锁乐观锁和分布式SagaTypeORM 提供三种写事务的方式:`@Transaction` 装饰器(0.3.x 已移除)、`EntityManager.transaction`、和 `QueryRunner`。选哪种取决于你的场景——简单事务用 `EntityManager.transaction`,需要精细控制用 `QueryRunner`。这篇文章把三种方式、隔离级别、锁机制都讲清楚,以及分布式事务的替代方案。 ## 三种事务 API ### 方式一:EntityManager.transaction(推荐日常使用) 最常用的事务写法——传一个回调函数,回调里所有操作在同一个事务里: ```typescript await dataSource.transaction(async (manager) => { // 所有操作必须用 manager,不能用全局 repository const user = await manager.findOne(User, { where: { id: 1 } }); user.balance -= 100; await manager.save(user); const target = await manager.findOne(User, { where: { id: 2 } }); target.balance += 100; await manager.save(target); }); ``` **关键规则**:回调里必须用 `manager` 参数(`EntityManager`),不能用 `dataSource.getRepository()`。全局 Repository 不在事务里,它的操作不受事务保护。 回调函数正常返回 → 自动 commit。抛异常 → 自动 rollback。 ### 方式二:QueryRunner(需要精细控制时) 当你需要手动控制 commit/rollback 时机、设置隔离级别、或者同一个 QueryRunner 上执行多个事务时: ```typescript const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { await queryRunner.manager.save(User, { name: 'Alice', balance: 100 }); await queryRunner.manager.save(User, { name: 'Bob', balance: 200 }); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); // 必须释放,否则连接泄漏 } ``` `release()` 很关键——QueryRunner 持有数据库连接,不释放连接池会耗尽。`finally` 块里 release 保证即使 commit/rollback 抛异常也能释放。 ### 方式三:NestJS 里的 @TransactionEntityManager NestJS + TypeORM 项目用 `@Transaction()` 装饰器(TypeORM 0.3.x 已移除,改用 `DataSource.transaction`): ```typescript import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; @Injectable() export class TransferService { constructor( @InjectDataSource() private dataSource: DataSource, ) {} async transfer(fromId: number, toId: number, amount: number) { return this.dataSource.transaction(async (manager) => { const from = await manager.findOne(User, { where: { id: fromId } }); const to = await manager.findOne(User, { where: { id: toId } }); if (from.balance < amount) { throw new Error('余额不足'); } from.balance -= amount; to.balance += amount; await manager.save([from, to]); }); } } ``` ## 隔离级别 隔离级别决定事务之间能看到彼此的修改到什么程度。 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 | |----------|------|-----------|------|------|----------| | READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最快 | 几乎不用 | | READ COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 | 快 | PostgreSQL 默认 | | REPEATABLE READ | ❌ 不会 | ❌ 不会 | ✅ 可能 | 中 | MySQL 默认 | | SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最慢 | 金融交易 | ### 设置隔离级别 ```typescript // QueryRunner 方式 await queryRunner.startTransaction('SERIALIZABLE'); // DataSource.transaction 不支持直接设隔离级别 // 需要先用 QueryRunner 设 const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction('REPEATABLE READ'); ``` ### 实际场景 **READ COMMITTED**:大多数 Web 应用。你的 API 请求处理用这个级别够了——一个请求不会因为另一个请求的未提交数据而出错。 **REPEATABLE READ**:同一个事务里多次读同一行数据,结果必须一致。比如生成报表时,事务期间数据不能变。 **SERIALIZABLE**:转账、库存扣减等绝对不能出错的场景。性能代价大,只在关键业务上用。 ## 锁机制 ### 悲观锁:数据库层面加锁 查询时直接锁行,其他事务不能修改: ```typescript // 排他锁(FOR UPDATE):其他事务不能读也不能写 const user = await manager.findOne(User, { where: { id: 1 }, lock: { mode: 'pessimistic_write' }, }); // 共享锁(FOR SHARE):其他事务可读不可写 const user = await manager.findOne(User, { where: { id: 1 }, lock: { mode: 'pessimistic_read' }, }); ``` **使用场景**:扣库存、转账——先锁住行,再修改,防止两个事务同时读到同一个余额然后覆盖。 ```typescript await dataSource.transaction(async (manager) => { // 先锁行 const account = await manager.findOne(Account, { where: { id: 1 }, lock: { mode: 'pessimistic_write' }, }); // 再修改 account.balance -= amount; await manager.save(account); }); ``` 不加锁的并发问题:事务 A 和事务 B 同时读到余额 1000,各扣 100,都写入 900——应该是 800。加 `pessimistic_write` 后,事务 B 等 A commit 后才能读,拿到 900 再扣,最终 800。 ### 乐观锁:应用层面检查 不锁行,更新时检查数据是否被改过。实体加 `@Version()` 列: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @VersionColumn() version: number; // 每次更新自动 +1 } ``` ```typescript const user = await manager.findOne(User, { where: { id: 1 } }); // user.version = 1 user.name = 'Alice'; await manager.save(user); // SQL: UPDATE user SET name = 'Alice', version = 2 WHERE id = 1 AND version = 1 // 如果另一个事务已经改了这行(version 变成 2),WHERE 条件不匹配,影响行数为 0 // TypeORM 抛出 OptimisticLockVersionMismatchError ``` 乐观锁不锁行,不阻塞读,适合读多写少的场景。缺点是冲突时需要重试。 ### 乐观锁重试模式 ```typescript async function updateWithRetry(id: number, updateFn: (user: User) => void, retries = 3) { for (let i = 0; i < retries; i++) { const user = await dataSource.getRepository(User).findOne({ where: { id } }); updateFn(user); try { await dataSource.getRepository(User).save(user); return; } catch (err) { if (err.name === 'OptimisticLockVersionMismatchError' && i < retries - 1) { continue; // 重试 } throw err; } } } ``` ## 死锁 两个事务互相等对方释放锁,永远卡住。TypeORM 不会自动检测死锁——依赖数据库的死锁检测机制。MySQL 和 PostgreSQL 会自动检测并回滚其中一个事务。 避免死锁的原则: - 事务尽量短——快进快出,不持有锁太久 - 按固定顺序访问资源——总是先锁 A 再锁 B,不要一个先 A 后 B,另一个先 B 后 A - 不要在事务里做网络请求——网络慢会长时间持有锁 ## 分布式事务 TypeORM 没有内置分布式事务支持。如果两个操作分别在不同数据库或不同服务上,不能用 TypeORM 的事务保证原子性。 ### 替代方案:Saga 模式 把分布式事务拆成一系列本地事务,每步成功后触发下一步,失败时执行补偿操作: ```typescript async function createOrderSaga(orderData: OrderData) { // 步骤1:创建订单(本地事务) const order = await orderService.createOrder(orderData); try { // 步骤2:扣库存(远程调用) await inventoryService.deduct(orderData.items); try { // 步骤3:扣款(远程调用) await paymentService.charge(orderData.amount); } catch (err) { // 步骤3失败:补偿步骤2(还库存) await inventoryService.restore(orderData.items); throw err; } } catch (err) { // 步骤2失败:补偿步骤1(取消订单) await orderService.cancelOrder(order.id); throw err; } } ``` Saga 不保证强一致性——中间状态对外可见(订单创建了但库存还没扣)。但这是分布式系统里唯一的实用方案——两阶段提交(2PC)性能太差,跨服务基本不用。
服务端6月5日 00:30
TypeORM QueryBuilder高级查询:条件组合、子查询、分页和批量操作`find()` 和 `findBy()` 只能处理简单查询——多表关联、条件组合、子查询、聚合分组,都得用 QueryBuilder。但 QueryBuilder 的 API 设计有时让人困惑:`leftJoin` 和 `innerJoin` 有什么区别?`where` 和 `andWhere` 能不能混用?子查询怎么写?这篇文章用实际场景把 QueryBuilder 的高级用法讲清楚。 ## QueryBuilder 基础回顾 ```typescript // 简单查询:find() 能搞定的 const users = await userRepository.find({ where: { active: true }, order: { createdAt: 'DESC' }, take: 20, }); // 复杂查询:必须用 QueryBuilder const users = await userRepository .createQueryBuilder('user') .where('user.active = :active', { active: true }) .orderBy('user.createdAt', 'DESC') .limit(20) .getMany(); ``` `'user'` 是别名,后续引用这个表都用这个别名。SQL 里变成 `SELECT ... FROM user user`。 ## 条件组合:OR、AND、嵌套 ### OR 条件 ```typescript // 错误:连续写 where,后面的会覆盖前面的 .where('user.active = :active', { active: true }) .where('user.role = :role', { role: 'admin' }) // 生成:WHERE user.role = 'admin'(第一个 where 被覆盖!) // 正确:用 orWhere .where('user.active = :active', { active: true }) .orWhere('user.role = :role', { role: 'admin' }) // 生成:WHERE user.active = true OR user.role = 'admin' ``` ### 混合 AND + OR ```typescript // 需求:活跃用户,且(管理员 或 创建于2024年之后) .where('user.active = :active', { active: true }) .andWhere( new Brackets(qb => { qb.where('user.role = :role', { role: 'admin' }) .orWhere('user.createdAt > :date', { date: '2024-01-01' }); }) ) // 生成:WHERE user.active = true AND (user.role = 'admin' OR user.createdAt > '2024-01-01') ``` `Brackets` 是关键——不加的话 OR 会和前面的 AND 混在一起,逻辑不对。需要括号的地方就用 `Brackets`。 ### 动态条件 根据用户输入动态拼查询条件: ```typescript function buildUserQuery(filters: { name?: string; role?: string; minAge?: number; maxAge?: number; }) { const qb = userRepository.createQueryBuilder('user'); if (filters.name) { qb.andWhere('user.name LIKE :name', { name: `%${filters.name}%` }); } if (filters.role) { qb.andWhere('user.role = :role', { role: filters.role }); } if (filters.minAge) { qb.andWhere('user.age >= :minAge', { minAge: filters.minAge }); } if (filters.maxAge) { qb.andWhere('user.age <= :maxAge', { maxAge: filters.maxAge }); } return qb; } ``` **注意参数占位符**:用 `:paramName` + 对象传参,不要字符串拼接——防止 SQL 注入: ```typescript // ❌ SQL 注入风险 qb.where(`user.name = '${filters.name}'`); // ✅ 参数化查询 qb.where('user.name = :name', { name: filters.name }); ``` ## 关联查询:leftJoin vs innerJoin ### leftJoin:左连接,左表数据全保留 ```typescript const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .getMany(); // 即使没有文章的用户也会返回,post 字段为 [] ``` ### innerJoin:内连接,只返回有关联数据的行 ```typescript const usersWithPosts = await userRepository .createQueryBuilder('user') .innerJoin('user.posts', 'post') .getMany(); // 只返回至少有一篇文章的用户 ``` ### 关联数据的选择和过滤 ```typescript // 只查用户的文章标题,不加载整个 post 对象 const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('post.published = :published', { published: true }) .getMany(); ``` `leftJoinAndSelect` 会把关联数据一起查出来(自动 SELECT),`leftJoin` 只 JOIN 不 SELECT。如果你后面要用 `post.title` 做过滤但不需要返回 post 数据,用 `leftJoin`;如果需要返回 post 数据,用 `leftJoinAndSelect`。 ## 子查询 ### WHERE 里的子查询 查"有超过 5 篇文章的用户": ```typescript const users = await userRepository .createQueryBuilder('user') .where((qb) => { const subQuery = qb .subQuery() .select('post.authorId') .from(Post, 'post') .groupBy('post.authorId') .having('COUNT(post.id) > :count') .getQuery(); return `user.id IN ${subQuery}`; }) .setParameter('count', 5) .getMany(); ``` ### SELECT 里的子查询(关联计数) 查每个用户的文章数: ```typescript const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .groupBy('user.id') .select('user.id', 'id') .addSelect('user.name', 'name') .addSelect('COUNT(post.id)', 'postCount') .getRawMany(); // 返回: [{ id: 1, name: 'Alice', postCount: '3' }, ...] ``` 注意 `getRawMany()` 返回原始数据库行,字段名是 select 里指定的别名,类型都是字符串。`getMany()` 返回实体对象,但聚合查询返回的不是实体,所以必须用 `getRawMany()`。 ## 分页 TypeORM 的 `skip` + `take` 是最简单的分页: ```typescript const [users, total] = await userRepository .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount(); ``` `getManyAndCount()` 返回 `[数据, 总数]`——一次查询做 SELECT + COUNT,比分开两次高效。 **深度分页的坑**:`skip(100000).take(20)` 在 MySQL 上很慢——数据库要扫描前 100020 行再丢弃前 100000 行。深度分页用 keyset pagination: ```typescript // 不用 offset,用上一页最后一条的 ID const users = await userRepository .createQueryBuilder('user') .where('user.id > :lastId', { lastId: lastIdOfPrevPage }) .orderBy('user.id', 'ASC') .take(20) .getMany(); ``` ID 有索引时,不管翻到第几页都是 O(1)。 ## 聚合和分组 ```typescript // 每个分类的文章数 const stats = await postRepository .createQueryBuilder('post') .select('post.category', 'category') .addSelect('COUNT(*)', 'count') .addSelect('MAX(post.createdAt)', 'latestPost') .groupBy('post.category') .having('COUNT(*) > :minCount', { minCount: 1 }) .getRawMany(); ``` `where` 过滤行(分组前),`having` 过滤组(分组后)。 ## 窗口函数 TypeORM 的 QueryBuilder 不直接支持窗口函数语法。用 `.getRawMany()` + 原生 SQL: ```typescript // PostgreSQL:查每个用户的文章数和排名 const ranked = await dataSource.createQueryRunner() .query(` SELECT u.id, u.name, COUNT(p.id) AS post_count, RANK() OVER (ORDER BY COUNT(p.id) DESC) AS rank FROM "user" u LEFT JOIN post p ON p."authorId" = u.id GROUP BY u.id ORDER BY rank `); ``` 或者用 QueryBuilder 的 `select` 写原始片段: ```typescript const result = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .groupBy('user.id') .select('user.id', 'id') .addSelect('user.name', 'name') .addSelect('COUNT(post.id)', 'postCount') .addSelect('RANK() OVER (ORDER BY COUNT(post.id) DESC)', 'rank') .getRawMany(); ``` 窗口函数只有 PostgreSQL 和 SQL Server 完整支持,MySQL 8+ 也支持。SQLite 不支持。 ## 批量操作 ### 批量插入 ```typescript await userRepository.insert([ { name: 'Alice', email: 'a@test.com' }, { name: 'Bob', email: 'b@test.com' }, { name: 'Charlie', email: 'c@test.com' }, ]); ``` 比循环 `save()` 快得多——一条 INSERT 语句插入多行。 ### 批量更新 ```typescript await userRepository .createQueryBuilder() .update(User) .set({ active: false }) .where('lastLoginAt < :date', { date: sixMonthsAgo }) .execute(); ``` ### Upsert(PostgreSQL / MySQL) ```typescript // PostgreSQL: ON CONFLICT await userRepository .createQueryBuilder() .insert() .into(User) .values([{ id: 1, name: 'Alice', email: 'new@test.com' }]) .onConflict('("id") DO UPDATE SET "email" = EXCLUDED."email"') .execute(); // MySQL: ON DUPLICATE KEY UPDATE await userRepository .createQueryBuilder() .insert() .into(User) .values([{ id: 1, name: 'Alice', email: 'new@test.com' }]) .orUpdate(['email'], ['id']) .execute(); ``` Upsert 的语法在 PostgreSQL 和 MySQL 上不同——这是 TypeORM "一套代码多数据库" 最大的例外之一。
服务端6月5日 00:29
TypeORM多数据库支持:配置差异、MongoDB限制和多数据源方案TypeORM 的卖点之一是"一套代码跑在多种数据库上"。实际体验下来,SQL 数据库之间迁移确实顺畅,但 MongoDB 是另一回事——文档模型和关系模型的 API 差异很大。这篇文章聚焦实际项目中的数据库选择、配置、以及多数据源场景。 ## 支持的数据库一览 TypeORM 支持的数据库分两类: **SQL 数据库**(API 统一,切换成本低): - MySQL / MariaDB - PostgreSQL - SQLite - SQL Server - Oracle - CockroachDB - SAP Hana **文档数据库**(API 有差异): - MongoDB 关键区别:SQL 数据库共享同一套 QueryBuilder API,切换只改 DataSource 配置。MongoDB 不支持 QueryBuilder 的大部分方法,也不支持事务(TypeORM 层面)、关系懒加载等特性。 ## 基础配置 ### MySQL ```typescript import { DataSource } from 'typeorm'; export const appDataSource = new DataSource({ type: 'mysql', host: process.env.DB_HOST || 'localhost', port: 3306, username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'myapp', entities: ['src/entity/*.ts'], synchronize: process.env.NODE_ENV !== 'production', // 生产环境禁止 logging: ['error'], // 只记录错误 SQL }); ``` ### PostgreSQL ```typescript export const appDataSource = new DataSource({ type: 'postgres', host: process.env.DB_HOST || 'localhost', port: 5432, username: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'myapp', entities: ['src/entity/*.ts'], synchronize: process.env.NODE_ENV !== 'production', // PostgreSQL 特有配置 ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, schema: 'public', }); ``` MySQL 和 PostgreSQL 的配置几乎一样——改 `type` 和端口就行。实体代码不需要任何修改。 ### SQLite(本地开发和测试) ```typescript export const appDataSource = new DataSource({ type: 'sqlite', database: 'data/myapp.db', // 文件数据库 // database: ':memory:', // 内存数据库(测试用) entities: ['src/entity/*.ts'], synchronize: true, }); ``` SQLite 的优势:零配置、零依赖、单文件。适合桌面应用(Electron)、CLI 工具、本地开发。缺点:不支持并发写入、不支持 `RETURNING`、JSON 函数有限。 ### MongoDB ```typescript export const appDataSource = new DataSource({ type: 'mongodb', url: process.env.MONGODB_URL || 'mongodb://localhost:27017/myapp', entities: ['src/entity/*.ts'], synchronize: process.env.NODE_ENV !== 'production', // MongoDB 特有配置 authSource: 'admin', replicaSet: 'rs0', // 如果用了副本集 }); ``` MongoDB 的实体定义和 SQL 不同——没有 `@Column`,用 `@ObjectIdColumn` 和 `@Field`: ```typescript import { Entity, ObjectIdColumn, ObjectId, Column as Field } from 'typeorm'; @Entity() export class User { @ObjectIdColumn() _id: ObjectId; @Field() name: string; @Field() email: string; @Field(type => [String]) // 数组字段 tags: string[]; } ``` **MongoDB 的限制**: - 不支持 `@JoinColumn`、`@ManyToMany` 等 SQL 关系装饰器 - 不支持 QueryBuilder 的 `leftJoin`、`subQuery` - `Repository.find()` 的 `where` 语法不同(用 MongoDB 查询对象) - 不支持数据库层面的约束(唯一约束、外键) ## 数据库选择指南 | 场景 | 推荐 | 原因 | |------|------|------| | Web 后端 API | PostgreSQL | 功能最全(JSON、全文搜索、数组、窗口函数) | | 已有 MySQL 基础设施 | MySQL | 不需要额外学习,TypeORM 完全支持 | | 桌面应用(Electron) | SQLite | 零依赖,单文件分发 | | 测试 | SQLite :memory: | 最快,每个测试隔离 | | 文档型数据、灵活 schema | MongoDB | 无需预定义表结构 | | 高并发读 + 简单写 | MySQL + 读写分离 | MySQL 主从复制成熟 | PostgreSQL 是新项目的默认推荐——JSON 支持、全文搜索、数组类型、窗口函数,比 MySQL 功能丰富很多,而且 TypeORM 的 PostgreSQL 支持最完善。 ## 多数据源配置 一个项目需要连接多个数据库的场景:读写分离(主从)、跨库查询、迁移过渡期。 ### 注册多个 DataSource ```typescript // data-sources.ts export const primaryDataSource = new DataSource({ name: 'primary', // 必须有 name type: 'postgres', host: 'primary-db.example.com', // ... entities: ['src/entity/*.ts'], }); export const secondaryDataSource = new DataSource({ name: 'secondary', // 必须有 name type: 'mysql', host: 'legacy-db.example.com', // ... entities: ['src/entity-legacy/*.ts'], }); ``` ### NestJS 里使用多数据源 ```typescript import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ name: 'primary', type: 'postgres', host: 'primary-db.example.com', entities: [User, Post], }), TypeOrmModule.forRoot({ name: 'secondary', type: 'mysql', host: 'legacy-db.example.com', entities: [LegacyUser], }), // 模块指定使用哪个数据源 TypeOrmModule.forFeature([User, Post], 'primary'), TypeOrmModule.forFeature([LegacyUser], 'secondary'), ], }) export class AppModule {} ``` 第二个参数 `'primary'` / `'secondary'` 指定该模块用哪个数据源。不同模块可以用不同数据源。 ### 跨库查询 TypeORM 不支持跨数据源的 JOIN。需要手动查两个库再在代码里合并: ```typescript async function getUserWithLegacy(userId: number) { const user = await primaryDataSource.getRepository(User) .findOne({ where: { id: userId } }); const legacyUser = await secondaryDataSource.getRepository(LegacyUser) .findOne({ where: { email: user.email } }); return { ...user, legacyData: legacyUser }; } ``` ## 数据库切换和迁移 ### 从 MySQL 切换到 PostgreSQL 1. 修改 DataSource 配置(`type: 'mysql'` → `type: 'postgres'`) 2. 检查实体里的 MySQL 特有类型(如 `tinyint` 改成 `boolean`) 3. 生成迁移文件:`npx typeorm migration:generate -d src/data-source.ts src/migration/InitPg` 4. 跑迁移:`npx typeorm migration:run -d src/data-source.ts` 5. 检查 QueryBuilder 里有没有 MySQL 专有语法(如 `` `backtick` `` 改成 `"double quote"`) 大部分情况下只需改配置和迁移,实体代码不用动。 ### synchronize 的正确用法 `synchronize: true` 在开发时方便——改实体自动同步表结构。但**生产环境必须关闭**,否则: - 删字段时直接 `ALTER TABLE DROP COLUMN`,数据丢失 - 重命名字段被当作"删旧列 + 加新列",数据丢失 - 并发启动多个实例可能同时执行 schema 变更 生产环境用迁移:`typeorm migration:run`。
服务端6月5日 00:27
TypeORM实体继承:单表、类表和具体表继承的选择与实现TypeORM 支持三种实体继承模式,名字听着抽象,但对应的是数据库设计里真实的问题:不同类型的数据,是放一张表还是分多张表?放一张表会有大量 NULL 列,分多张表 JOIN 查询变慢。这篇文章把三种模式的实现方式、优缺点、以及什么时候选哪种,都讲清楚。 ## 三种模式对比 | 模式 | 表结构 | 查询性能 | NULL 列 | 适用场景 | |------|--------|----------|---------|----------| | 单表继承(STI) | 一张表,鉴别列区分类型 | 最好(无 JOIN) | 多 | 子类字段差异小 | | 类表继承(CTI) | 父类一张 + 每个子类一张 | 中(需 JOIN) | 少 | 子类字段差异大,但有关联查询需求 | | 具体表继承(CTI-var) | 每个子类独立一张表 | 差(UNION 查所有类型) | 无 | 子类完全独立,很少跨类型查询 | ## 单表继承(Single Table Inheritance) 所有子类存在同一张表里,用鉴别列(discriminator column)标识类型。子类独有的列在不对应的行里为 NULL。 ### 实现 ```typescript import { Entity, PrimaryGeneratedColumn, Column, ChildEntity, DiscriminatorValue } from 'typeorm'; @Entity() @DiscriminatorColumn({ name: 'type' }) export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column({ type: 'text' }) body: string; } @ChildEntity() @DiscriminatorValue('article') export class Article extends Content { @Column({ nullable: true }) // 必须设 nullable: true author: string; @Column({ nullable: true }) category: string; } @ChildEntity() @DiscriminatorValue('video') export class Video extends Content { @Column({ nullable: true }) url: string; @Column({ nullable: true }) duration: number; } ``` 生成的表结构: ``` content ┌────┬───────┬───────────┬────────┬──────┬──────────┬─────┬──────────┐ │ id │ type │ title │ body │ url │ duration │ author │ category │ ├────┼───────┼───────────┼────────┼──────┼──────────┼─────┼──────────┤ │ 1 │ article│ ... │ ... │ NULL │ NULL │ Alice│ tech │ │ 2 │ video │ ... │ ... │ a.mp4│ 600 │ NULL │ NULL │ └────┴───────┴───────────┴────────┴──────┴──────────┴─────┴──────────┘ ``` ### 查询方式 ```typescript // 查所有内容(不管类型) const all = await dataSource.getRepository(Content).find(); // 只查文章(TypeORM 自动加 WHERE type = 'article') const articles = await dataSource.getRepository(Article).find(); // 按 type 过滤 const videos = await dataSource.getRepository(Content) .find({ where: { type: 'video' } }); ``` ### NULL 列问题 单表继承最大的问题:子类独有字段在不对应的行里全是 NULL。3 个子类各 5 个独有字段,表里就有 15 个可能为 NULL 的列。如果子类差异大,一张表 20+ 列一半是 NULL,可读性和索引效率都差。 **适合场景**:子类字段少、差异小。比如 `User` 分 `Admin`/`Member`,独有字段各 2-3 个。 ## 类表继承(Class Table Inheritance) 父类一张表,每个子类各一张表,子类表通过外键关联父类表。没有 NULL 列问题,但查询需要 JOIN。 ### 实现 ```typescript @Entity() @TableInheritance({ column: { type: 'varchar', name: 'type' } }) export class Person { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; } @ChildEntity() export class Employee extends Person { @Column() position: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) salary: number; } @ChildEntity() export class Customer extends Person { @Column() companyName: string; @Column({ default: 0 }) loyaltyPoints: number; } ``` 生成的表结构: ``` person employee customer ┌────┬───────┬──────┬───────────┐ ┌────┬──────────┬────────┐ ┌────┬─────────────┬───────────────┐ │ id │ type │ name │ email │ │ id │ position │ salary │ │ id │ companyName │ loyaltyPoints │ ├────┼───────┼──────┼───────────┤ ├────┼──────────┼────────┤ ├────┼─────────────┼───────────────┤ │ 1 │ employee│ Alice│ a@t.com│ │ 1 │ Engineer │ 80000 │ │ │ │ │ │ 2 │ customer│ Bob │ b@t.com│ │ │ │ │ │ 2 │ Acme Inc │ 100 │ └────┴───────┴──────┴───────────┘ └────┴──────────┴────────┘ └────┴─────────────┴───────────────┘ ``` `person.id` 和 `employee.id`/`customer.id` 是同一个值——子类表的主键同时也是父类表的外键。 ### 查询方式 ```typescript // 查所有人(TypeORM 自动 JOIN) const all = await dataSource.getRepository(Person).find(); // 只查员工(自动 JOIN person + employee) const employees = await dataSource.getRepository(Employee).find(); // 保存 const emp = new Employee(); emp.name = 'Alice'; emp.email = 'alice@test.com'; emp.position = 'Engineer'; emp.salary = 80000; await dataSource.getRepository(Employee).save(emp); // 自动向 person 和 employee 两张表插入数据 ``` ### JOIN 的性能代价 每次查子类都要 JOIN 父类表。数据量大时 JOIN 比单表查询慢。但如果你的查询经常只查父类字段(如按 name 搜索),不需要 JOIN,直接查 person 表即可。 **适合场景**:子类字段差异大,且经常需要跨类型查询公共字段。 ## 具体表继承(Concrete Table Inheritance) 每个子类一张独立的表,父类不建表。公共字段在每个子类表里重复。TypeORM 用 `@Column()` 在抽象类里定义,子类继承后各自建表。 ### 实现 ```typescript export abstract class Payment { @PrimaryGeneratedColumn() id: number; @Column() amount: number; @Column() currency: string; @Column() createdAt: Date; } @Entity() export class CreditCardPayment extends Payment { @Column() cardNumber: string; @Column() expiryDate: string; } @Entity() export class BankTransferPayment extends Payment { @Column() bankName: string; @Column() accountNumber: string; } ``` 生成的表结构: ``` credit_card_payment bank_transfer_payment ┌────┬────────┬──────────┬──────┬───────────┬────────────┐ ┌────┬────────┬──────────┬──────┬──────────┬───────────────┐ │ id │ amount │ currency │ date │ cardNumber│ expiryDate│ │ id │ amount │ currency │ date │ bankName │ accountNumber │ └────┴────────┴──────────┴──────┴───────────┴────────────┘ └────┴────────┴──────────┴──────┴──────────┴───────────────┘ ``` 公共字段 `amount`、`currency`、`createdAt` 在每张表里都有。没有父类表,没有 JOIN,也没有 NULL 列。 ### 查询所有支付类型 具体表继承最大的问题:查"所有类型的支付"需要 UNION: ```typescript // TypeORM 不直接支持跨子类的 UNION 查询 // 需要手动查每张表再合并 const [creditCards, bankTransfers] = await Promise.all([ dataSource.getRepository(CreditCardPayment).find(), dataSource.getRepository(BankTransferPayment).find(), ]); const allPayments = [...creditCards, ...bankTransfers] .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); ``` 没有统一的 Repository 可以查所有子类——因为根本没有父类表。 **适合场景**:子类完全独立,几乎不需要跨类型查询。比如支付方式、通知渠道。 ## 选择决策 ``` 子类独有字段多吗? ├─ 少(每个子类 2-3 个独有字段)→ 单表继承(STI) └─ 多(每个子类 5+ 个独有字段)→ 经常跨类型查询吗? ├─ 是 → 类表继承(CTI) └─ 否 → 具体表继承 ``` ### 迁移时的注意事项 - 单表继承加新子类:加列就行,`ALTER TABLE ADD COLUMN`,不用建新表 - 类表继承加新子类:新建一张子类表,父类表不变 - 具体表继承加新子类:新建一张独立表 单表继承加子类最灵活——只加列,不动已有数据。类表和具体表继承加子类需要新表,但不会影响已有表结构。 如果未来子类数量不确定或可能频繁增加,优先选单表继承。
服务端6月5日 00:26
TypeORM测试:Mock Repository、SQLite内存数据库和NestJS集成TypeORM 的测试分两层:不依赖数据库的纯逻辑测试(单元测试),和需要真实数据库交互的测试(集成测试)。很多人所有测试都连数据库,跑得又慢又不稳定;也有人不连数据库,Mock 了一大堆,测完发现线上还是出 bug。这篇文章把两种策略的使用场景和实现方式都讲清楚。 ## 测试环境:SQLite 内存数据库 集成测试需要真实数据库,但不需要用 MySQL/PostgreSQL——SQLite 内存数据库够用,速度快 10 倍以上,每个测试文件启动不到 100ms: ```typescript import { DataSource } from 'typeorm'; import { User } from '../entity/User'; import { Post } from '../entity/Post'; export const testDataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User, Post], synchronize: true, // 自动建表 logging: false, }); ``` 为什么用 SQLite 而不是真实数据库: - **零配置**:不需要装数据库、建测试库、管连接字符串 - **隔离性**:内存数据库每次测试完自动销毁,测试之间无干扰 - **速度**:内存操作,无网络 IO,单测 < 50ms 但 SQLite 不支持某些 MySQL/PostgreSQL 特性(如 `RETURNING`、`ON CONFLICT`、JSON 函数)。如果你的查询用了这些特性,需要用真实的 PostgreSQL 做集成测试。 ### 测试基础设施 ```typescript // test/setup.ts import { DataSource } from 'typeorm'; let dataSource: DataSource; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [__dirname + '/../src/entity/*.ts'], synchronize: true, }); await dataSource.initialize(); }); afterAll(async () => { await dataSource.destroy(); }); // 每个测试前清空所有表 afterEach(async () => { const entities = dataSource.entityMetadatas; for (const entity of entities) { const repository = dataSource.getRepository(entity.name); await repository.clear(); } }); ``` ## 单元测试:不连数据库 单元测试只测业务逻辑,不测数据库交互。Repository 方法用 Mock 替代。 ### Mock Repository ```typescript import { UsersService } from './users.service'; import { Repository } from 'typeorm'; import { User } from './user.entity'; describe('UsersService', () => { let service: UsersService; let mockRepository: jest.Mocked<Repository<User>>; beforeEach(() => { // 创建 Mock Repository mockRepository = { find: jest.fn(), findOne: jest.fn(), create: jest.fn(), save: jest.fn(), remove: jest.fn(), count: jest.fn(), } as any; service = new UsersService(mockRepository); }); it('should return all users', async () => { const mockUsers = [ { id: 1, name: 'Alice', email: 'alice@test.com' }, { id: 2, name: 'Bob', email: 'bob@test.com' }, ]; mockRepository.find.mockResolvedValue(mockUsers as User[]); const result = await service.findAll(); expect(result).toEqual(mockUsers); expect(mockRepository.find).toHaveBeenCalled(); }); it('should create a user', async () => { const dto = { name: 'Alice', email: 'alice@test.com' }; const savedUser = { id: 1, ...dto }; mockRepository.create.mockReturnValue(savedUser as User); mockRepository.save.mockResolvedValue(savedUser as User); const result = await service.create(dto); expect(result).toEqual(savedUser); expect(mockRepository.create).toHaveBeenCalledWith(dto); expect(mockRepository.save).toHaveBeenCalled(); }); it('should throw when user not found', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.findOne(999)).rejects.toThrow('User not found'); }); }); ``` Mock 的核心原则:**只 Mock 外部依赖(数据库),不 Mock 被测代码本身的逻辑**。如果 Service 里有个计算函数,直接测它,不要 Mock。 ### 什么时候用 Mock,什么时候不用 | 场景 | 策略 | 原因 | |------|------|------| | 纯逻辑函数 | 不 Mock | 没有外部依赖 | | Service → Repository | Mock Repository | 隔离数据库,测试快 | | 复杂查询逻辑 | 不 Mock,用集成测试 | Mock 查询结果无法验证 SQL 正确性 | | 第三方 API | Mock HTTP | 不能调外部服务 | **关键判断**:你的测试目的是验证"代码逻辑对不对"还是"SQL 查询对不对"?前者用 Mock,后者用集成测试。 ## 集成测试:连真实数据库 集成测试验证 SQL 是否正确、事务是否生效、约束是否有效——这些 Mock 测不了。 ### 事务回滚策略 每个测试跑在一个事务里,测完回滚,数据库回到干净状态: ```typescript describe('User integration tests', () => { let dataSource: DataSource; let userRepository: Repository<User>; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }); await dataSource.initialize(); userRepository = dataSource.getRepository(User); }); afterAll(async () => { await dataSource.destroy(); }); it('should create and find a user', async () => { const user = userRepository.create({ name: 'Alice', email: 'alice@test.com', }); await userRepository.save(user); const found = await userRepository.findOne({ where: { email: 'alice@test.com' }, }); expect(found).toBeDefined(); expect(found!.name).toBe('Alice'); }); it('should enforce unique email constraint', async () => { await userRepository.save({ name: 'Alice', email: 'alice@test.com' }); await expect( userRepository.save({ name: 'Bob', email: 'alice@test.com' }) ).rejects.toThrow(); // SQLite 会抛 UNIQUE constraint 错误 }); }); ``` ### 测试复杂查询 Query Builder 的复杂查询必须用集成测试——Mock 的 `find` 返回值证明不了 SQL 写对了: ```typescript it('should find users with post count > 5', async () => { // 准备数据 const user = await userRepository.save({ name: 'Alice', email: 'a@test.com' }); for (let i = 0; i < 6; i++) { await postRepository.save({ title: `Post ${i}`, authorId: user.id }); } // 执行复杂查询 const result = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .groupBy('user.id') .having('COUNT(post.id) > 5') .getMany(); expect(result).toHaveLength(1); expect(result[0].name).toBe('Alice'); }); ``` ## NestJS + TypeORM 测试 NestJS 项目里,TypeORM 通过模块注入,测试需要用 `@nestjs/testing`: ### 单元测试(Mock DataSource) ```typescript import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { UsersService } from './users.service'; import { User } from './user.entity'; describe('UsersService', () => { let service: UsersService; let mockRepo: any; beforeEach(async () => { mockRepo = { find: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue(null), save: jest.fn().mockResolvedValue({}), create: jest.fn().mockReturnValue({}), }; const module = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }, ], }).compile(); service = module.get<UsersService>(UsersService); }); it('should be defined', () => { expect(service).toBeDefined(); }); }); ``` `getRepositoryToken(User)` 是关键——NestJS 用这个 token 注入 Repository,Mock 时也用同一个 token。 ### E2E 测试(真实数据库) ```typescript import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import * as request from 'supertest'; describe('Users API (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }), TypeOrmModule.forFeature([User]), UsersModule, ], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); afterAll(async () => { await app.close(); }); it('POST /users', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'Alice', email: 'alice@test.com' }) .expect(201); }); it('GET /users', () => { return request(app.getHttpServer()) .get('/users') .expect(200); }); }); ``` E2E 测试用 SQLite 内存数据库,请求走完整的 HTTP → Controller → Service → Repository → 数据库链路,最接近真实场景。 ## 测试策略总结 | 测试类型 | 用途 | 速度 | 数据库 | 覆盖范围 | |----------|------|------|--------|----------| | 单元测试 | 验证业务逻辑 | < 10ms | 不需要(Mock) | Service 层逻辑 | | 集成测试 | 验证 SQL 和约束 | 50-200ms | SQLite 内存 | Repository 查询 | | E2E 测试 | 验证完整请求链路 | 100-500ms | SQLite 内存 | HTTP → 数据库 | **经验法则**:单元测试占 70%,集成测试占 20%,E2E 测试占 10%。核心业务逻辑用单元测试覆盖,复杂查询用集成测试验证,API 入口用少量 E2E 测试保证链路畅通。
服务端6月2日 23:06
TypeORM 实体关系怎么定义?一对一、一对多、多对多装饰器详解TypeORM 用装饰器定义实体间的关系:一对一、一对多、多对多。关键是搞清谁是"拥有方"(存外键的一方),谁是"反方"(只声明关系不存外键)。 ## 一对一(OneToOne) 一个人只有一个档案,一个档案只属于一个人: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @OneToOne(() => Profile, profile => profile.user) @JoinColumn() // 外键加在这一侧(拥有方) profile: Profile; } @Entity() export class Profile { @PrimaryGeneratedColumn() id: number; @OneToOne(() => User, user => user.profile) user: User; // 反方,不加 @JoinColumn } ``` `@JoinColumn()` 标记拥有方——数据库会在 User 表加一个 `profileId` 外键列。一对一关系只有一方能加 `@JoinColumn`,另一方是反向引用。 ## 一对多 / 多对一(OneToMany / ManyToOne) 一个用户有多篇文章,一篇文章属于一个用户: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @OneToMany(() => Post, post => post.author) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @ManyToOne(() => User, user => user.posts) @JoinColumn({ name: 'authorId' }) // 外键在 Post 表 author: User; } ``` **ManyToOne 总是拥有方**——外键存在"多"的一侧的表里。OneToMany 只是反向引用,不能单独存在,必须配对 ManyToOne。 数据库里 Post 表有一个 `authorId` 列指向 User.id。 ## 多对多(ManyToMany) 一个文章有多个标签,一个标签属于多篇文章: ```typescript @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Tag, tag => tag.posts) @JoinTable() // 拥有方加 @JoinTable,自动创建中间表 tags: Tag[]; } @Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Post, post => post.tags) posts: Post[]; // 反方,不加 @JoinTable } ``` `@JoinTable()` 让 TypeORM 自动创建中间表 `post_tags_tags`(post_id + tag_id)。只有一方加 `@JoinTable`,另一方是反向引用。 ## 加载关联数据 定义了关系不代表查询时自动加载。必须显式指定: ```typescript // 方式一:find 时指定 relations const user = await userRepo.findOne({ where: { id: 1 }, relations: ['posts', 'posts.tags'] // 支持嵌套 }); // 方式二:QueryBuilder join const user = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.tags', 'tag') .where('user.id = :id', { id: 1 }) .getOne(); ``` 不加 `relations` 或 `leftJoinAndSelect`,返回的 user.posts 就是 undefined。这是 TypeORM 的设计选择——避免无意义的 JOIN 查询。 ## 常见坑 **循环引用**:User 引用 Post,Post 引用 User。TypeScript 用 `() => Post` 延迟求值避免循环依赖。不要直接写 `Post`,必须用箭头函数。 **级联操作**:`cascade: true` 让保存 User 时自动保存关联的 Post。小心使用——可能在你不知情时写入大量数据。建议只在明确的父子关系上使用。 **删除行为**:`onDelete: 'CASCADE'` 在数据库层面级联删除。删除 User 时自动删除其所有 Post。比应用层级联更可靠。
服务端6月2日 23:04
TypeORM Active Record 和 Data Mapper 有什么区别?怎么选?TypeORM 支持两种数据访问模式:Active Record(实体自带 CRUD 方法)和 Data Mapper(通过 Repository 操作实体)。选哪种影响代码组织方式,但不影响功能。 ## Active Record 模式 实体继承 `BaseEntity`,直接调用静态方法操作数据: ```typescript @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string; } // 使用:实体自带 find/save/remove const users = await User.find(); const user = await User.findOne({ where: { email: 'test@test.com' } }); user.name = 'Updated'; await user.save(); await user.remove(); ``` 优点:代码简洁,一个对象既承载数据又负责持久化。适合简单 CRUD 和小项目。 缺点:实体和数据库操作耦合——同一个类既定义数据结构又包含查询逻辑,业务复杂后类会膨胀。 ## Data Mapper 模式 实体是纯数据对象,通过 Repository 操作: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string; } // 使用:通过 Repository 操作 const userRepo = dataSource.getRepository(User); const users = await userRepo.find(); const user = await userRepo.findOne({ where: { email: 'test@test.com' } }); user.name = 'Updated'; await userRepo.save(user); await userRepo.remove(user); ``` 优点:关注点分离——实体只管数据结构,Repository 负责持久化逻辑。业务代码不依赖数据库 API,方便单元测试(mock Repository)。 缺点:多一层间接,简单操作代码量稍多。 ## 怎么选 **Active Record 适合**: - 小项目,实体少,查询逻辑简单 - 快速原型,不想写太多样板代码 - Rails/Django 背景,习惯模型自带操作 **Data Mapper 适合**: - 中大型项目,业务逻辑复杂 - 需要单元测试,要 mock 数据层 - 领域驱动设计(DDD)风格,实体只表达业务概念 NestJS 项目推荐 Data Mapper——NestJS 的 `@InjectRepository()` 天然就是 Data Mapper 风格,依赖注入也让测试更方便。 ## 两种模式可以混用 TypeORM 不强制二选一。同一个实体可以同时用两种方式: ```typescript @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; // Active Record 方式 static findActive() { return this.find({ where: { active: true } }); } } // Data Mapper 方式也可以 const repo = dataSource.getRepository(User); const users = await repo.find({ where: { active: true } }); ``` 但不建议混用——团队应该统一风格,避免同一项目里两种模式交叉出现。 ## 自定义查询放哪里 Active Record 模式的常见问题:复杂查询方法堆积在实体类上。 ```typescript // 不推荐:实体类越来越胖 export class User extends BaseEntity { static findWithStats() { ... } static findActiveUsers() { ... } static findByDepartment() { ... } static searchByName() { ... } } // 推荐:抽取到单独的 Service 或自定义 Repository export class UserService { constructor(@InjectRepository(User) private repo: Repository<User>) {} async findWithStats() { return this.repo.createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .loadRelationCountAndMap('user.postCount', 'user.posts') .getMany(); } } ``` Data Mapper 模式自然避免这个问题——查询逻辑在 Service 里,实体保持干净。
服务端6月2日 01:50
TypeORM 查询缓存怎么配?Redis 缓存和缓存一致性实战数据库查询是后端应用最常见的性能瓶颈。缓存是第一道防线——能从缓存拿的数据就不查数据库。TypeORM 支持查询级缓存和 Redis 缓存,配置简单但有几个细节要注意。 ## TypeORM 查询缓存 开启缓存后,相同的 SQL 查询结果会被缓存,指定时间内不重复查数据库: ```typescript TypeOrmModule.forRoot({ type: 'postgres', cache: { type: 'redis', options: { host: 'localhost', port: 6379, }, duration: 30000, // 缓存 30 秒 }, }), ``` 在查询时指定缓存: ```typescript const users = await userRepo.find({ cache: true, // 使用全局 duration(30 秒) }); // 或自定义缓存时间 const users = await userRepo.find({ cache: 60000, // 缓存 60 秒 }); // QueryBuilder 也可以 const users = await createQueryBuilder('user') .cache(true) .getMany(); ``` 缓存 key 基于 SQL 语句自动生成——相同 SQL 共享缓存,不同 SQL 各自缓存。 ## 什么时候用缓存 **适合缓存的查询**: - 配置数据(很少变) - 用户信息(变更频率低) - 热门文章列表(可以接受短暂不一致) **不适合缓存的查询**: - 实时数据(库存、余额) - 分页偏移大的查询(缓存命中率低) - 写多读少的数据(缓存频繁失效) ## 缓存一致性 缓存的经典问题:数据库更新了但缓存还是旧数据。TypeORM 不会自动在数据变更时清除缓存——需要手动处理。 **方案一:写入后清除缓存** ```typescript async function updateUser(id: number, data: Partial<User>) { await userRepo.update(id, data); // 清除与 User 相关的所有缓存 await getConnection().queryResultCache.clear(); } ``` `queryResultCache.clear()` 清除所有缓存,比较粗暴但简单。精细清除需要知道具体的缓存 key,TypeORM 没有直接提供按表清除的 API。 **方案二:短过期时间 + 接受短暂不一致** ```typescript const users = await userRepo.find({ cache: 5000, // 5 秒过期,最多延迟 5 秒 }); ``` 简单有效,大部分场景够用。5 秒的不一致对用户体验几乎没有影响。 **方案三:手动管理缓存(Redis 直接操作)** 绕过 TypeORM 的缓存机制,用 ioredis 自己管理: ```typescript import Redis from 'ioredis'; const redis = new Redis(); async function getUser(id: string) { const cached = await redis.get(`user:${id}`); if (cached) return JSON.parse(cached); const user = await userRepo.findOne(id); await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 60); return user; } async function updateUser(id: string, data: Partial<User>) { await userRepo.update(id, data); await redis.del(`user:${id}`); // 精确清除 } ``` 自己管理更灵活——可以按 key 精确清除、设置不同过期时间、做缓存预热。但需要更多代码。 ## 缓存命中率监控 Redis 缓存命中率反映了缓存是否有效: ```bash redis-cli info stats | grep keyspace_hits redis-cli info stats | grep keyspace_misses ``` 命中率低于 50% 说明缓存策略有问题——要么过期时间太短,要么查询太分散没有复用。 ## TypeORM 缓存的局限 - 缓存粒度是 SQL 级别,不是实体级别。同一个 User 的不同查询(列表查询 vs 详情查询)有各自独立的缓存 - 没有 Cache Aside 模式的内置支持(先查缓存再查数据库) - 分布式环境下需要用 Redis 共享缓存,内存缓存(默认)会导致各实例缓存不一致 如果缓存需求复杂,建议绕过 TypeORM 缓存,直接用 Redis + 自定义缓存层。
服务端6月2日 01:49
TypeORM 软删除怎么用?@DeleteDateColumn 配置和唯一约束冲突解决软删除不是真的删除数据,而是标记 `deletedAt` 字段,查询时自动过滤已删除的记录。TypeORM 内置支持软删除,一行配置开启。 ## 开启软删除 在实体上加 `@DeleteDateColumn()`: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date; // null = 未删除,有值 = 已删除 } ``` `delete` 操作变成 UPDATE: ```typescript await userRepo.softDelete(1); // SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1 await userRepo.remove(user); // 同样效果,设置 deletedAt 而不是 DELETE ``` `find` 自动排除已删除记录: ```typescript const users = await userRepo.find(); // SQL: SELECT * FROM user WHERE "deletedAt" IS NULL ``` ## 查询包含已删除的记录 ```typescript // 包含已删除的记录 const allUsers = await userRepo.find({ withDeleted: true }); // 只查已删除的记录 const deletedUsers = await userRepo.find({ where: { deletedAt: Not(IsNull()) }, withDeleted: true }); ``` `withDeleted: true` 告诉 TypeORM 不要加 `WHERE "deletedAt" IS NULL` 条件。 ## 恢复已删除的记录 ```typescript await userRepo.restore(1); // SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1 ``` `restore` 把 `deletedAt` 设回 NULL,记录恢复正常。 ## 软删除的坑 **1. 唯一约束冲突** 软删除后,唯一字段(如 email)仍然占用唯一约束。删除 user@email.com 后,新建同名用户会报唯一冲突。 解决方案:唯一约束包含 deletedAt。 ```typescript @Entity() @Unique(['email', 'deletedAt']) export class User { @Column() email: string; @DeleteDateColumn() deletedAt: Date; } ``` 但这样未删除记录的 deletedAt 是 NULL,多条 NULL 在 PostgreSQL 的唯一约束里不冲突(符合 SQL 标准),MySQL 则需要用条件索引或去掉唯一约束。 **2. 关联数据不会级联软删除** ```typescript @OneToMany(() => Post, post => post.author) posts: Post[]; ``` 软删除 User 时,Post 不会被自动软删除。需要手动处理: ```typescript async function softDeleteUser(id: number) { await postRepo.softDelete({ authorId: id }); await userRepo.softDelete(id); } ``` **3. 物理删除仍然是需要的** 软删除的数据积累会膨胀表。定期清理: ```typescript // 物理删除 30 天前软删除的记录 await userRepo .createQueryBuilder() .delete() .where("deletedAt < :date", { date: new Date(Date.now() - 30 * 86400000) }) .execute(); ``` ## 不用软删除的替代方案 - **事件溯源**:不删除数据,而是追加"已删除"事件。查询时通过事件回放构建当前状态 - **归档表**:DELETE 前把数据 INSERT 到归档表,然后物理删除。查询只看主表,归档表做审计 软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。
服务端6月2日 01:49
Node.js ORM 怎么选?TypeORM、Prisma、Sequelize、MikroORM 对比Node.js 的 ORM 主要有四个:TypeORM、Prisma、Sequelize、MikroORM。2025 年的格局:Prisma 增长最快,TypeORM 用量最大但有退潮趋势,Sequelize 已经老化,MikroORM 是小众但口碑好。 ## 四个 ORM 对比 | 维度 | TypeORM | Prisma | Sequelize | MikroORM | |------|---------|--------|-----------|----------| | 类型安全 | 弱(装饰器 + any) | 强(自动生成类型) | 弱 | 强 | | 查询方式 | 装饰器 + QueryBuilder | 链式 API | 链式 API | 链式 API + QB | | 迁移工具 | 内置,体验一般 | 最好 | 内置 | 内置 | | 关联查询 | 容易 N+1 | include 清晰 | include | 自动 JOIN | | 社区 | 最大 | 增长最快 | 缩小中 | 小但活跃 | | 学习曲线 | 中 | 低 | 高 | 中 | | NestJS 集成 | 官方推荐 | 社区包 | 社区包 | 社区包 | ## Prisma:类型安全的赢家 Prisma 的核心优势是类型推导——schema 定义一次,查询自动补全、返回类型自动推导,几乎不需要手写类型。 ```prisma model User { id String @id @default(uuid()) email String @unique posts Post[] } ``` ```typescript const user = await prisma.user.findUnique({ where: { email: 'test@test.com' }, include: { posts: true } }); // user 的类型自动包含 posts,不需要手动标注 ``` 迁移体验也是最好的:`npx prisma migrate dev --name init` 自动生成迁移 SQL,`prisma migrate deploy` 在生产环境执行。 Prisma 的缺点:schema 用自己的 DSL(不是 TypeScript),复杂查询要写 raw SQL,N+1 问题用 include 解决但不如 DataLoader 灵活。 ## TypeORM:装饰器风格的老牌 TypeORM 用装饰器定义实体,和 NestJS 风格统一: ```typescript @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) email: string; @OneToMany(() => Post, post => post.author) posts: Post[]; } ``` 优势:和 NestJS 深度集成,装饰器风格统一。QueryBuilder 灵活,复杂查询不依赖 raw SQL。 劣势:类型安全弱——`findOne` 返回的是 `User | undefined`,关联字段需要手动标注类型。N+1 问题严重,`relations` 的加载策略容易踩坑。迁移工具粗糙,自动生成的迁移经常需要手动修改。 ## Sequelize:该退休了 Sequelize 是最老的 Node.js ORM,v5 及以前用 callback 风格,v6 改成了 Promise 但 API 设计仍然笨重。类型安全最差(TypeScript 支持是后加的)。不建议新项目使用。 ## MikroORM:小众精品 MikroORM 的设计理念更接近 Doctrine(PHP),强调 Unit of Work 模式——所有修改先在内存中记录,`flush()` 时一次性写入数据库。好处是自动处理关联关系,不需要手动 save 每个实体。 ```typescript const user = em.create(User, { email: 'test@test.com' }); await em.flush(); // 一次性写入 ``` 类型安全比 TypeORM 好很多,自动推导关联类型。但社区小,遇到问题不好查。适合对 ORM 设计有洁癖的开发者。 ## 怎么选 - **新项目**:Prisma(类型安全 + 迁移体验),或 MikroORM(如果你喜欢 Unit of Work) - **NestJS 项目**:TypeORM 仍是官方默认,但 Prisma 在 NestJS 社区的采用率快速上升 - **已有 TypeORM 项目**:不必迁移。TypeORM 能正常工作,迁移成本 > 收益 - **不要选 Sequelize**:除非你在维护老项目
服务端6月2日 01:46
TypeORM 查询太慢怎么优化?N+1、索引和批量插入实战TypeORM 性能问题主要来自三个地方:N+1 查询、缺少索引、大量数据操作用了逐条插入。逐一解决后,大部分应用的数据库性能就够了。 ## 1. 解决 N+1 查询 最常见也最致命的性能问题。查询用户列表后逐个查用户的文章: ```typescript // N+1:1 次查用户 + N 次查文章 const users = await userRepo.find(); for (const user of users) { user.posts = await postRepo.find({ where: { authorId: user.id } }); } ``` 100 个用户 = 101 条 SQL。解决方案:用 `relations` 或 `join` 一次性查出。 ```typescript // 方案一:relations(发 2 条 SQL,但比 N+1 好很多) const users = await userRepo.find({ relations: ['posts'] }); // 方案二:QueryBuilder join(1 条 SQL) const users = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany(); ``` `leftJoinAndSelect` 同时查出关联数据。`leftJoin` 只 join 不 select——用于过滤条件但不返回关联数据。 ## 2. 只查需要的字段 `find()` 默认 SELECT 所有列。大表里有 TEXT/BLOB 列时,查全列浪费网络带宽和内存: ```typescript // 查全列 const users = await userRepo.find(); // 只查需要的列 const users = await userRepo.find({ select: ['id', 'name', 'email'] }); // QueryBuilder 方式 const users = await userRepo .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany(); ``` 列表页只需要 id 和 name,详情页才需要所有字段。按场景选择查询范围。 ## 3. 分页不要用 skip/take 处理大偏移 ```typescript // 大偏移时很慢:MySQL 要扫描前 10000 行然后丢掉 const posts = await postRepo.find({ skip: 10000, take: 20 }); ``` 改用游标分页(cursor-based pagination): ```typescript // 基于 ID 的游标分页 const posts = await postRepo.find({ where: { id: LessThan(lastId) }, order: { id: 'DESC' }, take: 20 }); ``` 游标分页不依赖偏移量,性能与页码无关。缺点是不能跳转到指定页码。 ## 4. 批量插入 逐条插入 1000 条数据 = 1000 条 INSERT 语句,极慢: ```typescript // 慢:逐条插入 for (const item of data) { await repo.save(item); } ``` ```typescript // 快:批量插入 await repo.save(data); // TypeORM 自动合并成批量 INSERT ``` `save` 传入数组时,TypeORM 会合并成一条 INSERT 语句(部分数据库支持)。更大数据量用 `createQueryBuilder` 的 INSERT: ```typescript await createQueryBuilder() .insert() .into(User) .values(data) .execute(); ``` 极大数据量(10 万+)分批插入,每批 1000 条,避免单条 SQL 过长。 ## 5. 索引优化 TypeORM 的 `@Index` 装饰器声明索引: ```typescript @Entity() @Index(['email']) // 单列索引 @Index(['firstName', 'lastName']) // 复合索引 export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() firstName: string; @Column() lastName: string; } ``` 索引设计原则: - WHERE 条件里的列加索引 - 复合索引把区分度高的列放前面(`@Index(['status', 'createdAt'])` 如果 status 只有 3 个值就不如 `@Index(['createdAt', 'status'])`) - 不要过度索引——每个索引增加写入开销 查看慢查询:`EXPLAIN ANALYZE` + SQL 日志。TypeORM 开启 SQL 日志: ```typescript TypeOrmModule.forRoot({ logging: true, // 打印所有 SQL }) ``` ## 6. 连接池配置 默认连接池大小可能不够用: ```typescript TypeOrmModule.forRoot({ type: 'postgres', extra: { max: 20, // 最大连接数 idleTimeoutMillis: 30000, }, }) ``` `max` 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。
服务端2月18日 22:20
TypeORM 的事件系统如何工作?包括实体监听器和订阅者TypeORM 的事件系统允许开发者在实体操作的生命周期中执行自定义逻辑,提供了强大的扩展能力。 ## 事件类型 ### 1. 实体生命周期事件 TypeORM 提供了以下实体生命周期事件: * `BeforeInsert` - 在实体插入之前触发 * `AfterInsert` - 在实体插入之后触发 * `BeforeUpdate` - 在实体更新之前触发 * `AfterUpdate` - 在实体更新之后触发 * `BeforeRemove` - 在实体删除之前触发 * `AfterRemove` - 在实体删除之后触发 * `BeforeSoftRemove` - 在实体软删除之前触发 * `AfterSoftRemove` - 在实体软删除之后触发 * `BeforeRecover` - 在实体恢复之前触发 * `AfterRecover` - 在实体恢复之后触发 ### 2. 订阅者事件 订阅者可以监听所有实体的特定事件。 ## 使用实体监听器 ### 基本用法 ```typescript import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate, AfterInsert, AfterUpdate } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @Column({ default: 0 }) version: number; @BeforeInsert() beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); this.version = 1; } @BeforeUpdate() beforeUpdate() { this.updatedAt = new Date(); this.version++; } @AfterInsert() afterInsert() { console.log(`User ${this.name} inserted with ID ${this.id}`); } @AfterUpdate() afterUpdate() { console.log(`User ${this.name} updated to version ${this.version}`); } } ``` ### 复杂逻辑处理 ```typescript import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm'; import { hash } from 'bcrypt'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column() password: string; @Column({ default: false }) emailVerified: boolean; @Column({ type: 'timestamp', nullable: true }) emailVerifiedAt: Date; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; @BeforeInsert() async beforeInsert() { this.createdAt = new Date(); this.updatedAt = new Date(); // 加密密码 if (this.password) { this.password = await hash(this.password, 10); } // 验证邮箱格式 if (!this.validateEmail(this.email)) { throw new Error('Invalid email format'); } } @BeforeUpdate() async beforeUpdate() { this.updatedAt = new Date(); // 如果密码被修改,重新加密 if (this.password && this.isPasswordModified()) { this.password = await hash(this.password, 10); } // 如果邮箱被验证,记录验证时间 if (this.emailVerified && !this.emailVerifiedAt) { this.emailVerifiedAt = new Date(); } } private validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } private isPasswordModified(): boolean { // 实现密码修改检测逻辑 return true; } } ``` ## 使用订阅者 ### 基本订阅者 ```typescript import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; import { User } from '../entity/User'; @EventSubscriber() export class UserSubscriber implements EntitySubscriberInterface<User> { // 指定监听的实体 listenTo() { return User; } // 插入前 beforeInsert(event: InsertEvent<User>) { console.log(`Before inserting user: ${event.entity.name}`); // 可以修改实体 event.entity.createdAt = new Date(); } // 插入后 afterInsert(event: InsertEvent<User>) { console.log(`After inserting user with ID: ${event.entity.id}`); // 发送欢迎邮件 this.sendWelcomeEmail(event.entity); } // 更新前 beforeUpdate(event: UpdateEvent<User>) { console.log(`Before updating user: ${event.entity.name}`); // 记录变更 this.logChanges(event); } // 更新后 afterUpdate(event: UpdateEvent<User>) { console.log(`After updating user: ${event.entity.name}`); // 发送通知 this.sendUpdateNotification(event.entity); } // 删除前 beforeRemove(event: RemoveEvent<User>) { console.log(`Before removing user: ${event.entity.name}`); // 检查是否可以删除 if (event.entity.posts && event.entity.posts.length > 0) { throw new Error('Cannot delete user with posts'); } } // 删除后 afterRemove(event: RemoveEvent<User>) { console.log(`After removing user: ${event.entity.name}`); // 清理相关数据 this.cleanupUserData(event.entity.id); } private sendWelcomeEmail(user: User) { // 发送欢迎邮件逻辑 console.log(`Sending welcome email to ${user.email}`); } private sendUpdateNotification(user: User) { // 发送更新通知逻辑 console.log(`Sending update notification to ${user.email}`); } private logChanges(event: UpdateEvent<User>) { // 记录变更逻辑 console.log('Changes:', event.updatedColumns); } private cleanupUserData(userId: number) { // 清理用户数据逻辑 console.log(`Cleaning up data for user ${userId}`); } } ``` ### 全局订阅者 ```typescript import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; @EventSubscriber() export class AuditSubscriber implements EntitySubscriberInterface { // 监听所有实体 listenTo() { return Object; } // 所有实体的插入操作 afterInsert(event: InsertEvent<any>) { console.log(`Entity ${event.metadata.name} inserted with ID ${event.entity.id}`); // 记录审计日志 this.logAudit({ action: 'INSERT', entity: event.metadata.name, entityId: event.entity.id, timestamp: new Date(), }); } private logAudit(log: any) { // 记录审计日志逻辑 console.log('Audit log:', log); } } ``` ## 注册订阅者 ### 在 DataSource 中注册 ```typescript import { DataSource } from 'typeorm'; import { UserSubscriber } from './subscriber/UserSubscriber'; import { AuditSubscriber } from './subscriber/AuditSubscriber'; const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, // 注册订阅者 subscribers: [UserSubscriber, AuditSubscriber], }); ``` ### 动态注册订阅者 ```typescript import { DataSource } from 'typeorm'; const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true, }); // 初始化后动态注册订阅者 dataSource.initialize().then(() => { const userSubscriber = new UserSubscriber(); dataSource.subscribers.push(userSubscriber); }); ``` ## 高级事件处理 ### 事务中的事件 ```typescript @EventSubscriber() export class TransactionSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 检查是否在事务中 if (event.queryRunner?.isTransactionActive) { console.log('Insert operation is part of a transaction'); } // 使用事务执行器 if (event.queryRunner) { event.queryRunner.manager.getRepository(AuditLog).save({ action: 'USER_INSERT', userId: event.entity.id, timestamp: new Date(), }); } } } ``` ### 异步事件处理 ```typescript @EventSubscriber() export class AsyncSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 异步发送邮件 await this.sendEmailAsync(event.entity); // 异步生成用户资料 await this.generateUserProfileAsync(event.entity); } private async sendEmailAsync(user: User) { // 模拟异步邮件发送 return new Promise((resolve) => { setTimeout(() => { console.log(`Email sent to ${user.email}`); resolve(null); }, 1000); }); } private async generateUserProfileAsync(user: User) { // 模拟异步用户资料生成 return new Promise((resolve) => { setTimeout(() => { console.log(`Profile generated for user ${user.id}`); resolve(null); }, 500); }); } } ``` ### 条件事件处理 ```typescript @EventSubscriber() export class ConditionalSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } beforeUpdate(event: UpdateEvent<User>) { // 只在特定条件下执行 if (this.shouldProcessUpdate(event)) { this.processUpdate(event); } } private shouldProcessUpdate(event: UpdateEvent<User>): boolean { // 检查是否更新了特定字段 const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('email') || updatedFields.includes('password'); } private processUpdate(event: UpdateEvent<User>) { // 处理更新逻辑 console.log('Processing critical update:', event.entity); } } ``` ## 事件最佳实践 ### 1. 保持事件处理简单 ```typescript // ✅ 好的做法:事件处理简单直接 @EventSubscriber() export class SimpleSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 简单的日志记录 console.log(`User created: ${event.entity.name}`); } } // ❌ 不好的做法:事件处理过于复杂 @EventSubscriber() export class ComplexSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 复杂的业务逻辑 const user = event.entity; // 发送邮件 await this.sendEmail(user); // 创建用户资料 await this.createProfile(user); // 初始化用户设置 await this.initializeSettings(user); // 发送欢迎消息 await this.sendWelcomeMessage(user); // 记录统计 await this.recordStatistics(user); // 更新缓存 await this.updateCache(user); // 触发其他事件 await this.triggerEvents(user); } } ``` ### 2. 避免循环事件 ```typescript // ✅ 好的做法:避免循环事件 @EventSubscriber() export class SafeSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 使用标志位避免循环 if (event.entity.processed) { return; } // 处理逻辑 await this.processUser(event.entity); // 标记为已处理 event.entity.processed = true; } } // ❌ 不好的做法:可能导致循环事件 @EventSubscriber() export class CircularSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { // 更新用户,可能触发 afterUpdate 事件 await event.manager.save(User, { id: event.entity.id, processed: true, }); } } ``` ### 3. 错误处理 ```typescript // ✅ 好的做法:适当的错误处理 @EventSubscriber() export class ErrorHandlingSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async afterInsert(event: InsertEvent<User>) { try { await this.sendWelcomeEmail(event.entity); } catch (error) { console.error('Failed to send welcome email:', error); // 记录错误,但不影响主流程 await this.logError(error, event.entity); } } private async logError(error: any, user: User) { // 记录错误到数据库 await event.manager.getRepository(ErrorLog).save({ error: error.message, userId: user.id, timestamp: new Date(), }); } } ``` ### 4. 性能考虑 ```typescript // ✅ 好的做法:批量处理 @EventSubscriber() export class BatchSubscriber implements EntitySubscriberInterface<User> { private batch: User[] = []; private timer: NodeJS.Timeout | null = null; listenTo() { return User; } afterInsert(event: InsertEvent<User>) { // 添加到批次 this.batch.push(event.entity); // 设置定时器 if (!this.timer) { this.timer = setTimeout(() => { this.processBatch(); }, 1000); // 1 秒后处理 } } private async processBatch() { if (this.batch.length === 0) { return; } const usersToProcess = [...this.batch]; this.batch = []; this.timer = null; // 批量处理 await this.sendBatchNotifications(usersToProcess); } private async sendBatchNotifications(users: User[]) { console.log(`Sending notifications to ${users.length} users`); // 批量发送通知逻辑 } } ``` ## 实际应用场景 ### 1. 审计日志 ```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. 缓存失效 ```typescript @EventSubscriber() export class CacheInvalidationSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } afterUpdate(event: UpdateEvent<User>) { // 清除用户缓存 this.clearUserCache(event.entity.id); // 清除相关缓存 this.clearRelatedCache(event.entity.id); } afterRemove(event: RemoveEvent<User>) { // 清除所有相关缓存 this.clearAllUserCache(event.entity.id); } private clearUserCache(userId: number) { // 清除用户缓存逻辑 console.log(`Clearing cache for user ${userId}`); } private clearRelatedCache(userId: number) { // 清除相关缓存逻辑 console.log(`Clearing related cache for user ${userId}`); } private clearAllUserCache(userId: number) { // 清除所有用户缓存逻辑 console.log(`Clearing all cache for user ${userId}`); } } ``` ### 3. 通知系统 ```typescript @EventSubscriber() export class NotificationSubscriber implements EntitySubscriberInterface<Post> { listenTo() { return Post; } afterInsert(event: InsertEvent<Post>) { // 通知关注者 this.notifyFollowers(event.entity); // 通知作者 this.notifyAuthor(event.entity); } afterUpdate(event: UpdateEvent<Post>) { // 如果文章被发布,通知关注者 if (this.isPublished(event)) { this.notifyFollowers(event.entity); } } private isPublished(event: UpdateEvent<Post>): boolean { const updatedFields = event.updatedColumns.map(col => col.propertyName); return updatedFields.includes('status') && event.entity.status === 'published'; } private async notifyFollowers(post: Post) { // 通知关注者逻辑 console.log(`Notifying followers of post ${post.id}`); } private async notifyAuthor(post: Post) { // 通知作者逻辑 console.log(`Notifying author of post ${post.id}`); } } ``` TypeORM 的事件系统提供了强大的扩展能力,合理使用事件可以简化业务逻辑,提高代码的可维护性。
服务端2月18日 22:20
TypeORM 中如何定义和使用关系映射?包括一对一、一对多、多对多关系的详细配置TypeORM 提供了四种主要的关系映射类型,每种关系都有其特定的使用场景和配置方式。理解这些关系映射对于构建复杂的数据模型至关重要。 ## 四种关系类型 ### 1. One-to-One (一对一) 一对一关系表示两个实体之间存在唯一的对应关系。例如,一个用户只能有一个个人资料。 ```typescript @Entity() export class Profile { @PrimaryGeneratedColumn() id: number; @Column() gender: string; @Column() bio: string; @OneToOne(() => User, user => user.profile) @JoinColumn() user: User; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToOne(() => Profile, profile => profile.user, { cascade: true }) profile: Profile; } ``` **关键点**: * 使用 `@OneToOne()` 装饰器定义关系 * 在拥有方使用 `@JoinColumn()` 指定外键列 * `cascade: true` 允许级联操作(保存、删除等) ### 2. One-to-Many / Many-to-One (一对多/多对一) 一对多关系是最常见的关系类型。例如,一个用户可以发表多篇文章。 ```typescript @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() content: string; @ManyToOne(() => User, user => user.posts) author: User; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[]; } ``` **关键点**: * `@ManyToOne()` 放在"多"的一方,包含外键 * `@OneToMany()` 放在"一"的一方,不需要 `@JoinColumn()` * 外键自动添加到"多"的一方表中 ### 3. Many-to-Many (多对多) 多对多关系需要中间表来连接两个实体。例如,一篇文章可以有多个标签,一个标签也可以属于多篇文章。 ```typescript @Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToMany(() => Post, post => post.tags) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToMany(() => Tag, tag => tag.posts, { cascade: true }) @JoinTable() tags: Tag[]; } ``` **关键点**: * 使用 `@JoinTable()` 在关系的一方定义中间表 * 中间表自动创建,包含两个外键 * 可以自定义中间表名称和列名 ## 关系配置选项 ### Eager 和 Lazy 加载 ```typescript @Entity() export class User { @OneToMany(() => Post, post => post.author, { eager: true // 立即加载关联数据 }) posts: Post[]; } // 或者使用懒加载 @Entity() export class User { @OneToMany(() => Post, post => post.author) posts: Promise<Post[]>; } ``` ### 级联操作 (Cascade) ```typescript @OneToMany(() => Post, post => post.author, { cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover'] }) posts: Post[]; ``` 级联操作选项: * `insert`: 保存父实体时自动保存子实体 * `update`: 更新父实体时自动更新子实体 * `remove`: 删除父实体时自动删除子实体 * `soft-remove`: 软删除 * `recover`: 恢复软删除的实体 ### OnDelete 和 OnUpdate ```typescript @ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE', // 删除用户时级联删除文章 onUpdate: 'CASCADE' // 更新用户ID时级联更新文章 }) author: User; ``` ## 关系查询 ### 使用 FindOptions 查询关联数据 ```typescript const userRepository = dataSource.getRepository(User); // 加载关联数据 const users = await userRepository.find({ relations: ['posts', 'profile'] }); // 条件查询关联数据 const usersWithPosts = await userRepository.find({ relations: { posts: true }, where: { posts: { title: Like('%TypeORM%') } } }); ``` ### 使用 QueryBuilder ```typescript const users = await dataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('user.profile', 'profile') .where('post.title = :title', { title: 'TypeORM Guide' }) .getMany(); ``` ## 自定义关系 ### 自定义 JoinTable ```typescript @ManyToMany(() => Tag, tag => tag.posts) @JoinTable({ name: 'post_tags', joinColumn: { name: 'postId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'tagId', referencedColumnName: 'id' } }) tags: Tag[]; ``` ### 自定义 JoinColumn ```typescript @ManyToOne(() => User, user => user.posts) @JoinColumn({ name: 'author_id', referencedColumnName: 'id' }) author: User; ``` ## 最佳实践 1. **合理选择关系类型**: 根据业务需求选择最合适的关系类型 2. **避免过度使用 Eager 加载**: 可能导致 N+1 查询问题 3. **谨慎使用级联删除**: 确保不会意外删除重要数据 4. **使用索引优化查询**: 为外键列添加索引 5. **考虑性能影响**: 复杂关系查询可能影响性能 TypeORM 的关系映射系统提供了强大而灵活的方式来处理实体之间的关系,掌握这些概念对于构建高效、可维护的应用程序至关重要。
服务端2月18日 22:19
TypeORM 的 QueryBuilder 如何使用?包括复杂查询、关联查询、分页排序等高级功能QueryBuilder 是 TypeORM 中最强大、最灵活的查询工具,它允许开发者构建复杂的 SQL 查询,同时保持类型安全和可读性。 ## QueryBuilder 基础用法 ### 创建 QueryBuilder ```typescript import { DataSource } from 'typeorm'; const dataSource = new DataSource(/* 配置 */); // 方式1: 通过 Repository 创建 const userRepository = dataSource.getRepository(User); const queryBuilder = userRepository.createQueryBuilder('user'); // 方式2: 通过 DataSource 创建 const queryBuilder = dataSource.createQueryBuilder(User, 'user'); ``` ### 基本查询操作 ```typescript // 查询所有用户 const users = await dataSource .createQueryBuilder('user') .getMany(); // 查询单个用户 const user = await dataSource .createQueryBuilder('user') .where('user.id = :id', { id: 1 }) .getOne(); // 查询并计数 const count = await dataSource .createQueryBuilder('user') .getCount(); ``` ## 条件查询 ### Where 子句 ```typescript // 简单条件 const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .getMany(); // 多个条件 (AND) const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .andWhere('user.isActive = :isActive', { isActive: true }) .getMany(); // OR 条件 const users = await dataSource .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .orWhere('user.role = :role', { role: 'admin' }) .getMany(); // 复杂条件组合 const users = await dataSource .createQueryBuilder('user') .where( new Brackets(qb => { qb.where('user.age > :age', { age: 18 }) .orWhere('user.role = :role', { role: 'admin' }); }) ) .andWhere('user.isActive = :isActive', { isActive: true }) .getMany(); ``` ### 操作符 ```typescript import { Like, Between, In, MoreThan, LessThan } from 'typeorm'; // LIKE 查询 const users = await dataSource .createQueryBuilder('user') .where('user.name LIKE :name', { name: '%John%' }) .getMany(); // 或者使用 Like 操作符 const users = await dataSource .createQueryBuilder('user') .where('user.name = :name', { name: Like('%John%') }) .getMany(); // BETWEEN 查询 const users = await dataSource .createQueryBuilder('user') .where('user.age = :age', { age: Between(18, 30) }) .getMany(); // IN 查询 const users = await dataSource .createQueryBuilder('user') .where('user.id IN :ids', { ids: [1, 2, 3] }) .getMany(); // 或者使用 In 操作符 const users = await dataSource .createQueryBuilder('user') .where('user.id = :ids', { ids: In([1, 2, 3]) }) .getMany(); // 比较操作符 const users = await dataSource .createQueryBuilder('user') .where('user.age = :age', { age: MoreThan(18) }) .andWhere('user.score = :score', { score: LessThan(100) }) .getMany(); ``` ## 关联查询 ### Left Join 和 Inner Join ```typescript // Left Join (包含没有关联数据的记录) const users = await dataSource .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getMany(); // Inner Join (只包含有关联数据的记录) const users = await dataSource .createQueryBuilder('user') .innerJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getMany(); // 多层关联 const users = await dataSource .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.comments', 'comment') .leftJoinAndSelect('comment.author', 'commentAuthor') .getMany(); ``` ### Join 条件 ```typescript const users = await dataSource .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.status = :status', { status: 'published' }) .addSelect(['post.title', 'post.createdAt']) .getMany(); ``` ## 排序和分页 ### 排序 ```typescript // 单字段排序 const users = await dataSource .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .getMany(); // 多字段排序 const users = await dataSource .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .addOrderBy('user.name', 'ASC') .getMany(); // 随机排序 (MySQL) const users = await dataSource .createQueryBuilder('user') .orderBy('RAND()') .getMany(); ``` ### 分页 ```typescript // 基本分页 const page = 1; const pageSize = 10; const users = await dataSource .createQueryBuilder('user') .skip((page - 1) * pageSize) .take(pageSize) .getMany(); // 获取总数和分页数据 const [users, total] = await dataSource .createQueryBuilder('user') .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount(); console.log(`Total: ${total}, Page: ${page}, PageSize: ${pageSize}`); ``` ## 聚合查询 ### Group By 和 Having ```typescript // 按角色分组统计用户数 const result = await dataSource .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .groupBy('user.role') .getRawMany(); // 使用 Having 过滤分组 const result = await dataSource .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .groupBy('user.role') .having('COUNT(*) > :minCount', { minCount: 5 }) .getRawMany(); ``` ### 聚合函数 ```typescript // 统计总数 const count = await dataSource .createQueryBuilder('user') .select('COUNT(*)', 'count') .getRawOne(); // 计算平均值 const avgAge = await dataSource .createQueryBuilder('user') .select('AVG(user.age)', 'avgAge') .getRawOne(); // 求和 const totalScore = await dataSource .createQueryBuilder('user') .select('SUM(user.score)', 'totalScore') .getRawOne(); // 最大值和最小值 const result = await dataSource .createQueryBuilder('user') .select('MAX(user.age)', 'maxAge') .addSelect('MIN(user.age)', 'minAge') .getRawOne(); ``` ## 子查询 ### 使用 SubQueryFactory ```typescript import { SubQueryFactory } from 'typeorm'; const users = await dataSource .createQueryBuilder('user') .where((qb: SelectQueryBuilder<User>) => { const subQuery = qb .subQuery() .select('post.userId') .from(Post, 'post') .where('post.title LIKE :title', { title: '%TypeORM%' }) .getQuery(); return 'user.id IN ' + subQuery; }) .setParameter('title', '%TypeORM%') .getMany(); ``` ### 使用 EXISTS ```typescript const users = await dataSource .createQueryBuilder('user') .where((qb: SelectQueryBuilder<User>) => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.userId = user.id') .getQuery(); return 'EXISTS ' + subQuery; }) .getMany(); ``` ## 更新和删除 ### 更新操作 ```typescript // 简单更新 await dataSource .createQueryBuilder(User, 'user') .update() .set({ name: 'Updated Name' }) .where('id = :id', { id: 1 }) .execute(); // 条件更新 await dataSource .createQueryBuilder(User, 'user') .update() .set({ isActive: false }) .where('user.lastLoginAt < :date', { date: new Date('2024-01-01') }) .execute(); // 基于子查询的更新 await dataSource .createQueryBuilder(User, 'user') .update() .set({ score: () => 'score + 10' }) .where('user.id IN :ids', { ids: [1, 2, 3] }) .execute(); ``` ### 删除操作 ```typescript // 简单删除 await dataSource .createQueryBuilder(User, 'user') .delete() .where('id = :id', { id: 1 }) .execute(); // 条件删除 await dataSource .createQueryBuilder(User, 'user') .delete() .where('user.createdAt < :date', { date: new Date('2023-01-01') }) .andWhere('user.isActive = :isActive', { isActive: false }) .execute(); ``` ## 高级特性 ### 原生 SQL ```typescript const users = await dataSource .createQueryBuilder(User, 'user') .where('user.id = :id', { id: 1 }) .andWhere('JSON_CONTAINS(user.preferences, :preferences)', { preferences: JSON.stringify({ theme: 'dark' }) }) .getMany(); ``` ### 缓存 ```typescript const users = await dataSource .createQueryBuilder('user') .where('user.isActive = :isActive', { isActive: true }) .cache(60000) // 缓存 60 秒 .getMany(); ``` ### 事务 ```typescript await dataSource.transaction(async transactionalEntityManager => { const queryRunner = transactionalEntityManager.queryRunner; await queryRunner.manager .createQueryBuilder(User, 'user') .insert() .values({ name: 'John', email: 'john@example.com' }) .execute(); await queryRunner.manager .createQueryBuilder(Post, 'post') .insert() .values({ title: 'New Post', authorId: 1 }) .execute(); }); ``` ## 性能优化建议 1. **避免 N+1 查询**: 使用 `leftJoinAndSelect` 一次性加载关联数据 2. **只选择需要的字段**: 使用 `select()` 明确指定需要的列 3. **合理使用索引**: 为常用查询条件添加数据库索引 4. **使用缓存**: 对不常变化的数据启用查询缓存 5. **限制返回结果**: 使用 `take()` 和 `skip()` 实现分页 6. **监控查询性能**: 使用 `getQuery()` 和 `getSql()` 查看生成的 SQL QueryBuilder 是 TypeORM 中最强大的查询工具,掌握它的使用可以让你构建出高效、灵活的数据库查询。
服务端2月18日 22:19
TypeORM 的核心概念是什么?包括 Entity、Repository、DataSource 等主要组件的详细说明TypeORM 是一个基于 TypeScript 和 JavaScript 的 ORM 框架,它采用 Active Record 和 Data Mapper 两种设计模式,让开发者能够使用面向对象的方式来操作关系型数据库。 ## 核心概念 ### 1. Entity (实体) Entity 是 TypeORM 的核心概念,它对应数据库中的表。每个 Entity 类都使用 `@Entity()` 装饰器标记,并映射到数据库表。 ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } ``` ### 2. Column (列) Column 装饰器用于定义实体属性如何映射到数据库列: * `@Column()`: 基本列定义 * `@PrimaryGeneratedColumn()`: 自增主键 * `@CreateDateColumn()`: 自动创建时间 * `@UpdateDateColumn()`: 自动更新时间 * `@Generated()`: 自动生成值 ### 3. Repository (仓储) Repository 是用于操作实体的主要接口,提供了 CRUD 操作: ```typescript const userRepository = dataSource.getRepository(User); // 创建 const user = userRepository.create({ name: 'John', email: 'john@example.com' }); await userRepository.save(user); // 查询 const users = await userRepository.find(); const user = await userRepository.findOne({ where: { id: 1 } }); // 更新 await userRepository.update(1, { name: 'John Updated' }); // 删除 await userRepository.delete(1); ``` ### 4. DataSource (数据源) DataSource 是数据库连接的配置和管理中心: ```typescript const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test', entities: [User], synchronize: true, }); ``` ### 5. Relation (关系) TypeORM 支持四种主要的关系类型: * **One-to-One (一对一)**: 使用 `@OneToOne()` 装饰器 * **One-to-Many (一对多)**: 使用 `@OneToMany()` 装饰器 * **Many-to-One (多对一)**: 使用 `@ManyToOne()` 装饰器 * **Many-to-Many (多对多)**: 使用 `@ManyToMany()` 装饰器 ```typescript @Entity() export class Profile { @OneToOne(() => User, user => user.profile) user: User; } @Entity() export class User { @OneToOne(() => Profile, profile => profile.user) profile: Profile; @OneToMany(() => Post, post => post.author) posts: Post[]; } ``` ## 设计模式 ### Active Record 模式 在 Active Record 模式中,实体本身包含业务逻辑和数据访问方法: ```typescript @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; static async findByName(name: string) { return this.find({ where: { name } }); } } // 使用 const users = await User.findByName('John'); ``` ### Data Mapper 模式 在 Data Mapper 模式中,数据访问逻辑与实体分离,通过 Repository 操作: ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; } // 使用 Repository const userRepository = dataSource.getRepository(User); const users = await userRepository.find({ where: { name: 'John' } }); ``` ## 关键特性 1. **类型安全**: 完全支持 TypeScript,提供编译时类型检查 2. **装饰器驱动**: 使用装饰器简化配置 3. **迁移系统**: 支持数据库迁移和版本控制 4. **查询构建器**: 提供灵活的查询构建 API 5. **事务支持**: 完整的事务管理 6. **缓存支持**: 内置查询缓存机制 7. **多数据库支持**: MySQL, PostgreSQL, SQLite, SQL Server, Oracle 等 TypeORM 的这些核心概念使其成为 Node.js 生态中最流行的 ORM 框架之一,特别适合需要类型安全和面向对象编程风格的项目。