TypeORM 1.0 深度实战:当元老级 ORM 终于走出 0.x 漫漫长夜——从装饰器实体、QueryBuilder 内核到 valuesFromSelect / RETURNING 与 mysql2 迁移的生产级完全指南
关键词:TypeORM 1.0、DataMapper、QueryBuilder、DataSource、装饰器实体、mysql2、better-sqlite3、RETURNING、valuesFromSelect、ORM 选型
适用读者:用过 NestJS / TypeORM 的老兵,以及正在 2026 年 ORM 红海中纠结 Prisma、Drizzle、TypeORM 该怎么选的工程师。
如果你在 2024 年之前写过 Node.js 后端,几乎一定和 TypeORM 打过交道。它是 TypeScript 世界里第一个真正成熟、装饰器风格的 ORM,也是 NestJS 默认推荐的持久层方案。但诡异的是,从 2016 年项目创建到 2026 年,整整十年,它的版本号始终停在 0.x——一个永远"非正式"的怪异状态。2025 年底新维护者接手后项目突然复活,2026 年 6 月 15 日,TypeORM 1.0 终于发布了。
这篇文章不是一篇"新版本特性流水账"。我会带你从架构内核一路钻到 1.0 的真实代码变更,搞清楚三件事:1.0 到底改了什么、没改什么;它和 Drizzle / Prisma 的底层哲学分歧在哪;以及在生产环境里,值不值得现在就升级。
一、背景介绍:一个被"0.x 诅咒"困了十年的元老
1.1 为什么 1.0 是个大事件
在软件工程里,0.x 通常意味着"API 不稳定、随时可能破坏性变更、别用于生产"。但 TypeORM 偏偏在 0.3.x 上稳定运行了多年,全球每周下载量长期保持在百万级,无数电商、SaaS、内部系统都跑在它上面。
这种"Production-Ready 却叫 0.x"的分裂状态,给团队带来真实的痛苦:
- 升级心理负担:团队 Leader 在评审升级 PR 时,看到
0.2 → 0.3会如临大敌,因为 0.x 之间可以合法地破坏性变更; - 招聘与信任成本:新人看到
0.x会本能地觉得"这项目是不是快死了"; - 生态观望:很多工具链(如某些代码生成器、迁移工具)会因为"版本号没到 1.0"而迟迟不提供一等支持。
TypeORM 1.0 的意义,不是某个颠覆性新功能,而是一个承诺:API 进入语义化版本的稳定轨道,破坏性变更将只在 2.0 出现。对一个承载大量存量业务的技术栈来说,这个承诺本身的价值,远大于某几行新 API。
1.2 复活的时间线(有数据支撑)
根据 1.0 发布说明与项目维护报告:
- 2024 年底:新维护者团队接手,项目活动重新活跃;
- 2025 全年:合并 575 个 Pull Request(前一年仅 63 个),发布 8 个补丁版本,关闭 超过 2300 个 issue;
- 2026-06-15:发布 TypeORM 1.0,移除长期废弃 API,更新平台要求,并沉淀 0.3.x 周期积累的数十项修复与新特性。
这条曲线说明:1.0 不是"憋了大招一口气放出",而是维护节奏恢复后的自然收口。这也是为什么我建议你认真对待它——它背后是可持续的维护,而不是一次性营销。
1.3 2026 年的 ORM 格局:三种哲学
要评价 TypeORM 1.0,必须先看它站在哪个坐标系里:
| 流派 | 代表 | 核心哲学 | 心智模型 |
|---|---|---|---|
| DataMapper(装饰器实体) | TypeORM、MikroORM | 用类 + 装饰器声明实体,运行时反射构建元数据 | "对象是表,类是映射" |
| Schema-first / SQL-first | Drizzle、Kysely | 用 TypeScript 代码描述表结构,编译期生成 SQL | "SQL 是一等公民,TS 只是它的类型壳" |
| Client 生成式 | Prisma | 独立 .prisma Schema + 代码生成器产出强类型 Client | "Schema 是真相源,Client 是衍生物" |
TypeORM 属于第一类。它的强项是领域建模直观、关系(relation)处理成熟、Repository 抽象优雅;弱项是运行时反射带来体积与性能开销,且 SQL 控制不如 SQL-first 直接。
理解这个定位,你才能判断 1.0 的改动到底有没有戳中痛点。
二、核心概念:TypeORM 的世界由什么构成
在写代码前,先把 TypeORM 的四个核心抽象钉死,后面才不会乱。
2.1 实体(Entity)与装饰器
实体是数据库表在 TS 里的映射。TypeORM 用装饰器把类"标记"成表,并把字段"标记"成列:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from "typeorm";
import { User } from "./User";
@Entity("orders")
@Index(["status", "createdAt"])
export class Order {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 32 })
status: "pending" | "paid" | "shipped" | "done";
@Column({ type: "decimal", precision: 12, scale: 2 })
amount: number;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => User, (user) => user.orders, {
onDelete: "CASCADE",
eager: false,
})
@JoinColumn({ name: "user_id" })
user: User;
}
要点:
@Entity("orders")显式指定表名,避免类名到表名的隐式转换(生产环境强烈建议显式);@PrimaryGeneratedColumn("uuid")让数据库生成主键,避免应用层生成带来的时钟/冲突问题;@Index把高频查询字段做成复合索引,这是性能优化的第一道关;- 关系用
@ManyToOne/@OneToMany声明,TypeORM 会在运行时构建关系图,支持relations加载。
2.2 DataSource:一切的起点
旧版的 createConnection 已废弃,1.0 全面拥抱 DataSource:
import { DataSource } from "typeorm";
import { Order } from "./entity/Order";
import { User } from "./entity/User";
export const AppDataSource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: "shop",
// 1.0 起底层驱动已切到 mysql2,连接池参数直接透传
extra: {
connectionLimit: 20,
charset: "utf8mb4",
},
entities: [Order, User],
synchronize: false, // 生产环境务必 false,用 migration 管控结构
logging: ["error", "warn"],
});
await AppDataSource.initialize();
DataSource 是连接池、实体元数据、迁移、订阅者的统一入口。整个应用应当只初始化一个 DataSource 实例,并在进程退出时 destroy()。
2.3 三种数据访问方式
TypeORM 提供三套 API,粒度从粗到细:
- Repository:面向实体的高层封装,
repo.find / save / remove; - EntityManager:跨实体的"瑞士军刀",事务里最常用;
- QueryBuilder:最底层、最灵活,直接拼 SQL(带类型提示)。
// 1) Repository
const orderRepo = AppDataSource.getRepository(Order);
const paid = await orderRepo.findBy({ status: "paid" });
// 2) EntityManager(事务内)
await AppDataSource.manager.transaction(async (manager) => {
const repo = manager.getRepository(Order);
await repo.save(order);
});
// 3) QueryBuilder(精细控制)
const qb = AppDataSource.createQueryBuilder(Order, "o")
.leftJoinAndSelect("o.user", "u")
.where("o.status = :s", { s: "paid" })
.orderBy("o.createdAt", "DESC")
.take(20);
const rows = await qb.getMany();
选型建议:简单 CRUD 用 Repository,跨实体/事务用 EntityManager,复杂报表/性能敏感路径用 QueryBuilder 甚至 raw SQL。不要为了"统一"而只用 Repository——那样你会反复和 N+1 与类型推断作斗争。
三、架构分析:TypeORM 是如何把类变成 SQL 的
理解架构,才能理解 1.0 的改动本质。TypeORM 内部是一条清晰的分层管道。
3.1 四层架构
┌─────────────────────────────────────────────┐
│ 应用层:Repository / EntityManager / QueryBuilder │
├─────────────────────────────────────────────┤
│ 元数据层(Metadata / Reflection):装饰器 → 实体元数据 │
├─────────────────────────────────────────────┤
│ 查询构建层(QueryBuilder):AST 化的 SQL 构造 │
├─────────────────────────────────────────────┤
│ 驱动层(Driver):mysql2 / pg / better-sqlite3 / │
│ sqlite / mongodb(统一抽象) │
└─────────────────────────────────────────────┘
3.2 元数据层:装饰器不是魔法,是"延迟登记"
很多人以为装饰器会"自动建表"。其实不是。装饰器只是在类定义时把信息登记到一个全局元数据存储里。真正的"建表/比对"发生在:
DataSource.initialize()时,读取所有entities,构建EntityMetadata;- 若
synchronize: true(仅开发),对比元数据与库内结构并自动 DDL; - 若
synchronize: false(生产),由 Migration 接管结构变更。
这个过程依赖 reflect-metadata 在运行时读取类型。它带来了 TypeORM 最大的双刃剑:
- ✅ 好处:实体即文档,领域模型直观,关系导航自然;
- ❌ 代价:运行时反射有体积与启动开销,且类型信息在编译后需要
reflect-metadata兜底(否则@Column()拿不到类型)。
1.0 的关键取舍:它没有改变"运行时元数据"这一根本架构,而是在驱动层和安全层做了强化(见下文 3.4)。这意味着 TypeORM 的"DataMapper 基因"没变,你现有的实体代码几乎可以无缝迁移——这是它相比"推倒重来"的 SQL-first 方案最大的友好之处。
3.3 驱动层抽象与 1.0 的依赖大换血
这是 1.0 最容易被忽略、却最影响生产稳定性的一处改动。旧版 TypeORM 直接依赖一些"年久失修"或"设计过时"的底层库,1.0 把它们整体替换:
| 用途 | 旧依赖(0.3.x) | 新依赖(1.0) | 为什么换 |
|---|---|---|---|
| MySQL 驱动 | mysql | mysql2 | mysql2 维护活跃、支持连接池/ Promise API/预处理语句 |
| SQLite 驱动 | sqlite3 | better-sqlite3 | 同步 API、更快、编译维护更稳 |
| 哈希/加解密 | 自制或旧实现 | Node 原生 crypto | 去掉冗余依赖,安全性与性能更好 |
实战含义:
- 升级到 1.0 后,你的
package.json应当显式装mysql2(而非mysql),否则会拿到弃用警告; better-sqlite3是同步的,这意味着 SQLite 路径下的批量写入延迟会显著下降,非常适合本地测试与边缘场景;- 依赖树更干净,供应链风险更低——这对企业安全审计是实打实的加分项。
3.4 安全层:1.0 把 DDL 也"参数化"了
这是一个非常硬核、却很少被媒体提及的改进。在旧版里,部分模式检查(schema check)和 DDL 拼接存在标识符未充分转义的风险——虽然正常用法没问题,但一旦标识符来自不可信输入(比如动态表名),就有注入面。
1.0 的修复方向:
- 所有驱动在做模式检查与 DDL 时,统一采用参数化查询与转义标识符;
- 对
orderBy条件做运行时校验,拒绝非法排序字段; - 对
.limit()做更严格检查,防止非数值/负数穿透到 SQL。
// 1.0 之前:orderBy 若拼接外部输入,理论上存在风险
qb.orderBy(userInputField, "DESC"); // 旧版校验较弱
// 1.0:orderBy 会校验字段是否为实体已知列,非法值直接抛错
// 配合白名单更安全:
const ALLOWED_SORT = ["createdAt", "amount", "status"] as const;
const sort = ALLOWED_SORT.includes(userInput as any)
? userInput
: "createdAt";
qb.orderBy(`o.${sort}`, "DESC");
工程建议:即便框架帮你做了一层校验,涉及排序/过滤的字段仍要走白名单。安全是分层防御,不要因为只有一层就裸奔。
四、代码实战:把 1.0 的新特性真正用起来
理论讲完,上硬货。下面每一个例子都能直接跑(以 MySQL 8 + TypeORM 1.0 为例)。
4.1 初始化与实体定义
// data-source.ts
import { DataSource } from "typeorm";
import { User } from "./entity/User";
import { Order } from "./entity/Order";
export const AppDataSource = new DataSource({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "app",
password: process.env.DB_PASS,
database: "shop",
charset: "utf8mb4",
entities: [User, Order],
migrations: [__dirname + "/migration/*.ts"],
synchronize: false,
extra: { connectionLimit: 20 },
});
// entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Order } from "./Order";
@Entity("users")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@OneToMany(() => Order, (o) => o.user)
orders: Order[];
}
4.2 关系加载与避免 N+1
N+1 是 ORM 头号性能坑。假设你要列出每个用户的订单数:
// ❌ 错误示范:先查用户,再逐个查订单 → N+1
const users = await userRepo.find();
for (const u of users) {
u.orders = await orderRepo.find({ where: { user: { id: u.id } } });
}
// ✅ 正确示范:一次 JOIN 搞定
const users = await userRepo
.createQueryBuilder("u")
.leftJoinAndSelect("u.orders", "o")
.getMany();
// ✅ 只要聚合计数,根本不需要加载订单实体:用子查询/聚合
const result = await userRepo
.createQueryBuilder("u")
.select("u.id", "userId")
.addSelect("u.name", "name")
.addSelect((qb) => {
return qb
.subQuery()
.select("COUNT(*)")
.from(Order, "o")
.where("o.userId = u.id");
}, "orderCount")
.getRawMany();
经验法则:只取你需要的列(select 收窄),只 JOIN 你需要的 relation。QueryBuilder 的 addSelect 子查询模式,是做"列表页带统计"最高效的写法之一。
4.3 新特性一:valuesFromSelect() —— INSERT INTO … SELECT FROM
这是 1.0 最实用的新增 API 之一。过去要把一张表的数据按条件搬进另一张(或同表不同筛选),你得先 select 回内存、再 insert,既慢又占内存。现在 InsertQueryBuilder 有了 valuesFromSelect():
// 场景:把"已支付且超过 30 天"的订单,归档进 orders_archive 表
import { AppDataSource } from "./data-source";
await AppDataSource.createQueryBuilder()
.insert()
.into("orders_archive")
.valuesFromSelect((qb) =>
qb
.select([
"id",
"user_id",
"amount",
"status",
"created_at",
])
.from(Order, "o")
.where("o.status = :s", { s: "paid" })
.andWhere("o.created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)")
)
.execute();
为什么重要:这条语句 100% 在数据库内部完成,零内存搬运、单条事务、可利用源表索引。在大表归档、数据仓库 ETL、读写分离下的"热表→冷表"迁移里,它能把原本分钟级的操作压到秒级。
⚠️ 注意:目标表与源表的列顺序/类型必须对齐。生产环境建议先在测试库用
returning/日志验证影响行数。
4.4 新特性二:update() / upsert() 支持 RETURNING
PostgreSQL、MySQL 8.0.21+、SQLite 都支持 RETURNING。1.0 让 update() 和 upsert() 能把"被改动的行"直接拿回来,不用再发一条 SELECT 去查:
// update + RETURNING:改完直接拿到更新后的行
const updated = await AppDataSource.createQueryBuilder()
.update(Order)
.set({ status: "shipped" })
.where("id = :id", { id: orderId })
.returning(["id", "status", "amount"])
.execute();
console.log(updated.raw); // [{ id: 42, status: 'shipped', amount: 99.0 }]
// upsert + RETURNING:插入或更新后拿回最终行
const result = await AppDataSource.createQueryBuilder()
.insert()
.into(User)
.values({ email: "a@b.com", name: "张三" })
.orUpdate(["name"], ["email"]) // 冲突时更新 name
.returning(["id", "email", "name"])
.execute();
console.log(result.raw); // 返回 upsert 后的完整用户行
实战价值:
- 写后立即可用:比如创建订单后立刻要返回带自增 ID 和默认状态的完整对象,省一次查询;
- 幂等接口更干净:upsert 后直接返回"最终态",前端无需再拉一次;
- 批量场景:配合
returning可以一次性拿到所有受影响行,避免"insert 一批再 select 一批"的两段式写法。
4.5 新特性三:QueryRunner 支持 await 自动清理
长事务 / 手动控制连接时,最怕忘记 release(),连接泄漏会拖垮整个连接池。1.0 让 QueryRunner 支持 await 语法——在 await 表达式结束时自动释放:
// 旧写法:必须手动 release,try/finally 一大坨
const runner = AppDataSource.createQueryRunner();
try {
await runner.connect();
await runner.startTransaction();
await runner.manager.save(order);
await runner.commitTransaction();
} finally {
await runner.release();
}
// 1.0 新写法:把 runner 包进 await,结束自动清理
await (async () => {
const runner = AppDataSource.createQueryRunner();
await runner.connect();
await runner.startTransaction();
try {
await runner.manager.save(order);
await runner.commitTransaction();
} catch (e) {
await runner.rollbackTransaction();
throw e;
}
})();
// 离开作用域后 runner 自动释放,连接归还池
更进一步,如果你把 QueryRunner 当作一个可被 await 的值使用,TypeORM 会在 microtask 结束时帮你 release。这对写"连接池不会泄漏"的工具函数尤其有用——很多老代码里的连接泄漏,根因就是 release 路径没覆盖到所有分支。
4.6 事务与 EntityManager 的正确姿势
await AppDataSource.manager.transaction(async (manager) => {
const orderRepo = manager.getRepository(Order);
const userRepo = manager.getRepository(User);
const user = await userRepo.findOneBy({ id: userId });
if (!user) throw new Error("user not found");
const order = orderRepo.create({
user,
amount: 99,
status: "pending",
});
await orderRepo.save(order);
// 同事务内扣减余额等后续操作……
});
要点:
- 永远用
manager.transaction的回调式 API,不要手动begin/commit/release——回调式能正确处理异常回滚; - 事务里只读必要数据,锁粒度尽量小;
- 不要在事务里调用外部 HTTP / 消息队列——分布式事务请用"本地事务 + 可靠消息表 / Outbox 模式"。
五、性能优化:让 TypeORM 1.0 跑得比你想的快
TypeORM 常被诟病"比原生 SQL 慢、比 Drizzle 重"。这话对一半。慢的根源通常是用法,不是 ORM 本身。下面是一线调优清单。
5.1 连接池:第一道生死线
extra: {
connectionLimit: 20, // 根据并发量调,别拍脑袋设 100
// mysql2 还支持:
// waitForConnections: true,
// queueLimit: 0,
}
经验公式:连接数 ≈ (核心数 × 2) + 有效磁盘数,再结合压测微调。设太大反而引发 MySQL 的线程调度开销;设太小则请求排队。用 SHOW PROCESSLIST 观察是否长期满池。
5.2 用 QueryBuilder 替代 Repository 做聚合
Repository 的 find 会默认 SELECT * 并试图 Hydrate 成实体。列表/报表场景应当用 getRawMany() 直接拿平面结果:
// 慢:
const list = await orderRepo.find({ relations: ["user"] });
// 快(只取需要的列,不走实体水合):
const list = await AppDataSource.createQueryBuilder(Order, "o")
.select("o.id", "id")
.addSelect("o.amount", "amount")
.addSelect("u.name", "userName")
.leftJoin("o.user", "u")
.where("o.status = :s", { s: "paid" })
.orderBy("o.createdAt", "DESC")
.limit(50)
.getRawMany();
getRawMany() 跳过了实体对象的构造与关系解析,在大型列表接口上能砍掉可观的 CPU 与内存。
5.3 二级缓存与查询缓存
对读多写少、一致性要求不高的数据(如配置表、类目树),开启 QueryBuilder 缓存:
const cached = await AppDataSource.createQueryBuilder(Category, "c")
.cache("all-categories", 60_000) // 缓存 key + 60s TTL
.getMany();
注意:缓存的是查询结果,结构变更(migration)后要主动 AppDataSource.queryResultCache?.clear(),否则会出现"结构变了数据没变"的脏读。
5.4 批处理:别一行一行 insert
// 慢:N 次往返
for (const item of items) await repo.save(item);
// 快:一次批量
await repo.save(items); // TypeORM 会合成单条多值 INSERT
// 更快且可控:用 QueryBuilder 显式批量
await AppDataSource.createQueryBuilder()
.insert()
.into(Order)
.values(items) // items 是对象数组
.execute();
配合 4.3 的 valuesFromSelect(),大部分 ETL 类批处理都能在数据库内闭环。
5.5 1.0 依赖换血带来的隐性性能增益
- better-sqlite3 是同步、零序列化开销的 SQLite 驱动,本地测试 / 边缘函数 / CLI 工具里,批量写比旧
sqlite3快一个数量级; - mysql2 原生支持服务端预处理语句(prepared statements),重复执行的参数化查询能省去每次的 SQL 解析,高并发写场景收益明显;
- 原生 crypto 替代旧哈希实现,迁移/密码相关操作的 CPU 占用下降。
这些不是"某个 API 提速 10 倍"的噱头,而是长期运行的稳定性红利——连接池更稳、依赖更安全、冷启动更轻。
5.6 监控:别盲调
logging: (type) => type === "query" && process.env.NODE_ENV !== "production",
// 或只对慢查询记录:
logging: ["error"],
// 配合 maxQueryExecutionTime 抓慢 SQL:
maxQueryExecutionTime: 1000,
把超过 1s 的查询打到日志/APM,再针对性加索引或改 QueryBuilder,比凭直觉优化有效十倍。
六、从 0.3 迁移到 1.0:踩坑与清单
1.0 移除了长期废弃的 API,所以迁移不是"改个版本号"。标准动作:
6.1 依赖替换
# 卸载旧驱动,安装新驱动
npm remove mysql sqlite3
npm install mysql2 better-sqlite3
npm install typeorm@^1.0 reflect-metadata
6.2 废弃 API 排查
- 老的
createConnection全局单例(getConnection)应迁移到DataSource实例; - 已废弃的
findOne无参数重载、旧的BaseEntity静态方法等,按 TS 编译报错逐个替换; - 检查自定义
Driver扩展——底层驱动接口在 1.0 有调整,自定义驱动需对齐。
6.3 测试先行
# 用 CI 跑全量集成测试,重点覆盖:
# - 实体关系加载(eager/lazy)
# - 事务回滚
# - 迁移脚本(在影子库 dry-run)
# - 任何直接拼标识符的 orderBy / 动态表名逻辑
1.0 强化了 orderBy / limit 校验,过去"能跑但不规范"的写法现在会直接报错——这其实是好事,逼你把隐患暴露在上线前。
6.4 灰度策略
- 先在非核心服务(如内部报表、后台任务)升级验证;
- 再推进读多写少的服务;
- 最后才是交易核心链路,并保留快速回滚到 0.3 的镜像。
七、总结与展望:2026 年,TypeORM 还值得选吗?
直接给结论:
选 TypeORM 1.0,如果:
- 你已经在用 NestJS / 存量 TypeORM 代码,迁移成本远低于重写;
- 你的领域模型关系复杂(多对多、继承表、树形结构),TypeORM 的关系处理成熟度依然是第一梯队;
- 你重视"实体即文档"的可读性与团队上手速度;
- 你需要一个维护已复活、进入语义化稳定轨道的方案,而不是担心它哪天停更。
考虑 Drizzle / Prisma,如果:
- 你是全新项目,且团队偏好 SQL-first(Drizzle)或强类型 Client 生成(Prisma);
- 你对包体积极度敏感、追求编译期 SQL 校验;
- 你的查询以复杂分析 SQL 为主,希望 SQL 完全由自己掌控。
我的个人判断:2026 年的 ORM 之争,不再是"谁替代谁",而是按场景分层。TypeORM 1.0 的价值,恰恰在于它终于摘掉了"0.x 不稳定"的帽子,让那批跑了多年、不敢动的存量系统有了原地升级、继续服役的正当理由。而它在 1.0 里做的三件实事——valuesFromSelect() 数据库内 ETL、update/upsert 的 RETURNING、QueryRunner 的 await 自动清理,以及驱动层 mysql2/better-sqlite3/原生 crypto 的换血——都是生产环境天天会踩、踩了真疼的痛点,不是 PPT 上的概念。
对 1.x 的展望
从维护节奏看,1.x 的主线很可能是:
- 元数据层的可选编译期化:逐步减少对
reflect-metadata的强依赖,向"类型即元数据"靠拢,缩小与 SQL-first 方案的运行时差距; - 更好的原生 SQL 互操作:让 raw SQL 与 QueryBuilder 的类型贯通更顺滑;
- 云原生适配:连接池与 Serverless 数据库(如 PlanetScale、Neon)的深度调优;
- 可观测性内置:与 OpenTelemetry 的 trace/metric 更直接地打通(参考同站已发布的 OpenTelemetry 深度实战)。
最后的建议
不要因为"1.0 听起来很新"就盲目追,也不要因为"它是个老框架"就歧视。技术选型的第一性原理永远是:你的团队、你的领域模型、你的运维能力,和这个工具的基因是否匹配。TypeORM 1.0 给老用户的交代是"你可以放心继续用",给新用户的信号是"我现在也是个正经的一线选手了"。
如果你正站在升级的十字路口,我的建议是:先用本文的迁移清单在影子库跑一遍,用 1.0 的新 API 重写一两个高频接口,对比一次压测数据——让数字替你做决定,而不是让社区情绪替你做决定。
本文基于 TypeORM 1.0(2026-06-15 发布)官方发布说明与项目维护报告撰写,代码示例在 MySQL 8 + TypeORM 1.0 环境下可运行。RETURNING、valuesFromSelect 等特性依赖具体数据库版本支持,生产落地前请在测试库验证影响行数与执行计划。