从 Sequelize 迁移到 TypeORM:数据源、实体定义、字段选项与索引的完整对照指南
【免费下载链接】typeormTypeScript & JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm
本文是一份面向 Sequelize 存量项目的 TypeORM 迁移实战指南,核心围绕“同一个需求在 Sequelize 与 TypeORM 中各自的写法”展开:从数据源初始化、表结构同步,到实体模型与各种字段约束(可空、默认值、唯一、自增、主键、时间戳列),再到模型的新增/更新与多列索引创建,均给出可直接对照、可运行的代码。读完本文,你能够把一段现有的 Sequelize 模型代码逐行翻译成 TypeORM 的实体定义,并了解底层装饰器与DataSource的实现机制,做到“知其然也知其所以然”。
本文内容以 迁移指南 为骨架,代码示例、结论均可在此文档与仓库源码中找到依据。
一、初始化数据源:从new Sequelize(...)到new DataSource({...})
在 Sequelize 中,数据源(连接配置)通过new Sequelize(database, username, password, options)创建,随后调用authenticate()验证连接是否成功:
const sequelize = new Sequelize("database", "username", "password", { host: "localhost", dialect: "mysql", }) sequelize .authenticate() .then(() => { console.log("Data Source has been initialized successfully.") }) .catch((err) => { console.error("Error during Data Source initialization:", err) })TypeORM 中使用DataSource承载相同的职责,但它把连接参数统一收进一个选项对象,并用type字段取代 Sequelize 的dialect:
import { DataSource } from "typeorm" const dataSource = new DataSource({ type: "mysql", host: "localhost", username: "username", password: "password", }) dataSource .initialize() .then(() => { console.log("Data Source has been initialized successfully.") }) .catch((err) => { console.error("Error during Data Source initialization:", err) })两者最直观的差异是构造签名:
| 维度 | Sequelize | TypeORM |
|---|---|---|
| 构造方式 | 位置参数(database, user, password)+ 选项 | 全部使用选项对象{ type, host, username, password, database } |
| 数据库类型字段 | dialect: "mysql" | type: "mysql" |
| 连接验证/初始化 | authenticate() | initialize() |
| 可用范围 | 需要传入各模型 | 全局导出后可在任意模块使用 |
几点迁移要点:
- 推荐在单独文件中创建
DataSource实例并export,这样应用各处都能复用同一连接。官方 DataSource 指南 同样建议“把AppDataSource通过export全局暴露”,并且一个应用可以按需创建多个数据源(例如 MySQL 一个、Postgres 一个)。 - 不要忘记
type是可选的数据库平台集合(mysql、postgres、mariadb、sqlite、mssql、oracle、mongodb等),DataSource接受的DataSourceOptions会随type不同而提供不同字段。更多参数含义见 DataSourceOptions。 - 从源码看,
initialize()是真正的“建连 + 初始化元数据”入口(DataSource.ts),并且在满足条件时还会触发自动建表(见下文同步机制)。
二、表结构同步:从逐模型sync()到synchronize: true
Sequelize 中要为每个模型单独同步表结构:
Project.sync({ force: true }) Task.sync({ force: true }){ force: true }表示“先 DROP 再 CREATE”,容易造成数据丢失。TypeORM 则把“是否自动同步”做成了数据源级开关,只需在选项里加一个布尔值:
const dataSource = new DataSource({ type: "mysql", host: "localhost", username: "username", password: "password", synchronize: true, })此时 TypeORM 会在启动时根据你注册的所有实体(entities)自动比对并创建/更新表结构。源码层面,initialize()在建立连接后会检查this.options.synchronize,为真则调用内部synchronize()方法(DataSource.ts)。
生产环境提示:synchronize: true适合开发期快速迭代;在真实生产环境里,更稳妥的做法是把它关闭,改用显式的 Migrations 机制 通过迁移文件管理结构变更,可审计、可回滚。Sequelize 生态同样有独立迁移工具,TypeORM 则把迁移作为一等公民内置。
三、定义模型:从sequelize.define到实体类(Entity)
Sequelize 用sequelize.define("tableName", attributes)工厂函数定义模型:
module.exports = function (sequelize, DataTypes) { const Project = sequelize.define("project", { title: DataTypes.STRING, description: DataTypes.TEXT, }) return Project }module.exports = function (sequelize, DataTypes) { const Task = sequelize.define("task", { title: DataTypes.STRING, description: DataTypes.TEXT, deadline: DataTypes.DATE, }) return Task }在 TypeORM 中,模型被命名为Entity(实体)——一个普通 TypeScript 类配合装饰器声明式地描述表结构:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class Project { @PrimaryGeneratedColumn() id: number @Column() title: string @Column() description: string }import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class Task { @PrimaryGeneratedColumn() id: number @Column() title: string @Column("text") description: string @Column() deadline: Date }迁移建议与约定:
- 一个实体类放一个文件,这是社区强烈推荐的实践,也便于通过 glob 批量注册实体。
@Entity()标记类映射到数据库表;默认表名取类名(Project→project),也可显式指定@Entity("my_table_name")。@PrimaryGeneratedColumn()声明自增主键;@Column()声明普通列。- 类型映射:
DataTypes.STRING通常对应默认字符串列;DataTypes.TEXT需要显式写成@Column("text")(示例中正是如此);DataTypes.DATE对应实体里的Date属性。 - TypeORM 允许把类直接当作数据库模型使用,并“以声明式方式说明模型的哪一部分会成为数据库表列”。同时,TypeScript 的类型系统会为这些类带来类型提示(type hinting)等额外收益——这是相比纯 JS 的 Sequelize 模型的一大优势。
值得留意的是:实体中每个被@Column()标记的属性最终都会映射为表的一列;列的数据库类型既可以通过@Column第一个参数显式给出(如@Column("text")),也可以不写、让 TypeORM 依据属性反射元数据自动推断。实体必须拥有主键列(关系型场景),否则会报错。字段类型、主键列、特殊列的完整展开可参考 Entities 文档。
四、其他模型设置:Sequelize 字段选项 →@Column选项
这是迁移中最琐碎也最常碰到的部分。下面逐条给出“Sequelize 写法 → TypeORM 写法”的对照。
4.1 可空 + 默认值
Sequelize:
flag: { type: Sequelize.BOOLEAN, allowNull: true, defaultValue: true },TypeORM 使用nullable与default两个列选项:
@Column({ nullable: true, default: true }) flag: boolean;4.2 数据库当前时间作为默认值
Sequelize:
flag: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }TypeORM:
@Column({ default: () => "NOW()" }) myDate: Date;这里用函数形式default: () => "NOW()"让默认值落到数据库侧的NOW()(生成数据库层面的DEFAULT),而不是在 JS 侧先计算一个固定时间。
4.3 唯一约束
Sequelize:
someUnique: { type: Sequelize.STRING, unique: true },TypeORM:
@Column({ unique: true }) someUnique: string;从源码看,@Column({ unique: true })会被拆成两步处理:一方面注册普通列元数据,另一方面向uniques元数据集合登记一条唯一约束(Column.ts),最终在数据库中生成唯一约束或唯一索引。
4.4 数据库列名与属性名不一致
Sequelize 用field指定物理列名:
fieldWithUnderscores: { type: Sequelize.STRING, field: "field_with_underscores" },TypeORM 中对应的是name:
@Column({ name: "field_with_underscores" }) fieldWithUnderscores: string;默认情况下 TypeORM 会用属性名生成列名;需要别名时用name覆盖。
4.5 自增列
Sequelize:
incrementMe: { type: Sequelize.INTEGER, autoIncrement: true },TypeORM:
@Column() @Generated() incrementMe: number;@Generated()用于标记“插入实体时自动生成值”的非主键列。其默认生成策略为increment(源码 Generated.ts 中strategy: "increment" | "uuid" | "rowid" = "increment");另外也支持@Generated("uuid")。需要留意的是,部分数据库只允许一张表存在一个 increment 列,或要求自增列必须是主键,若遇阻可改用@PrimaryGeneratedColumn()。
4.6 手动主键
Sequelize:
identifier: { type: Sequelize.STRING, primaryKey: true },TypeORM:
@Column({ primary: true }) identifier: string;@Column({ primary: true })等价于使用@PrimaryColumn()。若希望该主键自增或生成 UUID,则应使用@PrimaryGeneratedColumn()/@PrimaryGeneratedColumn("uuid")。
4.7createDate/updateDate时间戳列
Sequelize 通常需要手动配置createdAt/updatedAt。TypeORM 为此提供了专用的时间戳列装饰器,不必每次手写默认值:
@CreateDateColumn(); createDate: Date; @UpdateDateColumn(); updateDate: Date;@CreateDateColumn()会在对象首次插入时写入创建时间且此后不再变动,实现上以mode: "createDate"注册列元数据(CreateDateColumn.ts);@UpdateDateColumn()则在每次执行save/upsert(命中更新分支)时自动刷新为当前时间。列名可由你自由命名。
除上述对照外,@Column还支持更丰富的选项,例如length(varchar(150))、comment、precision/scale、select(查询默认是否隐藏该列)、transformer(读写时类型转换)等,详见 Entity columns 章节。上述多数选项为关系型数据库专用,在 MongoDB 驱动下不可用。
4.8 小结对照表
| Sequelize 属性选项 | TypeORM 写法 | 说明 |
|---|---|---|
allowNull: true | nullable: true | 列是否允许 NULL |
defaultValue: value | default: value | 列默认值 |
defaultValue: Sequelize.NOW | default: () => "NOW()" | 数据库侧当前时间 |
unique: true | unique: true | 生成唯一约束 |
field: "db_name" | name: "db_name" | 指定物理列名 |
autoIncrement: true | @Column()+@Generated() | 自增值生成 |
primaryKey: true | primary: true(或@PrimaryColumn()) | 声明主键 |
createdAt/updatedAt | @CreateDateColumn()/@UpdateDateColumn() | 自动时间戳 |
五、使用模型:新增、保存、加载与属性访问
5.1 创建并保存新模型
Sequelize 通过Model.create(object)一行完成“创建 + 落库”:
const employee = await Employee.create({ name: "John Doe", title: "senior engineer", })TypeORM 提供了多种等价途径,你可以按团队偏好选择Data Mapper或Active Record两种模式(这也是 Sequelize 迁移到 TypeORM 后最需要适应的编程风格差异)。
Data Mapper 风格(默认):实体只描述属性,通过仓库(Repository)操作数据库。
const employee = new Employee() // 也可以在构造函数中传参 employee.name = "John Doe" employee.title = "senior engineer" await dataSource.getRepository(Employee).save(employee)Active Record 风格:实体继承BaseEntity,直接在模型上提供save、find等静态与实例方法。
const employee = Employee.create({ name: "John Doe", title: "senior engineer" }) await employee.save()BaseEntity.create是Repository.create的静态等价物,BaseEntity上还提供preload、save、remove、findOneBy等一系列对应方法(见 BaseEntity.ts 中的静态方法实现)。两种模式各有取舍:Data Mapper 在大型应用中更利于维护(职责分离),Active Record 在小型项目中更简洁直接。完整对比见 Active Record vs Data Mapper。
5.2 预加载并替换已存在实体
如果想从数据库加载一条已存在记录并仅替换其中部分属性,两种风格都能用preload:
const employee = await Employee.preload({ id: 1, name: "John Doe" })注意它的语义:给定对象必须携带主键(id),TypeORM 先按该主键查出完整实体,再用传入对象中的属性做覆盖;若对应 id 不存在,则返回undefined。其底层经由Repository.preload转发给EntityManager.preload(Repository.ts)。相比 Sequelize 先findByPk再手动赋值的惯用法,preload一步到位。
5.3 访问属性
Sequelize 中属性需要经过实例的get()方法:
console.log(employee.get("name"))TypeORM 的实体就是普通类实例,直接访问属性即可:
console.log(employee.name)这带来的直接收益是:IDE 自动补全 + 编译期类型检查成为常态,字符串键拼错的问题可以在编译阶段被拦截。
Repository/BaseEntity上其余常用 API(find、findOneBy、findAndCount、save、remove、softDelete、update、upsert、分页选项等)的完整签名可查阅 Repository API。
六、创建索引:从indexes配置到@Index装饰器
Sequelize 在define的第三个参数里集中配置索引数组:
sequelize.define( "user", {}, { indexes: [ { unique: true, fields: ["firstName", "lastName"], }, ], }, )TypeORM 把索引以装饰器形式直接附着在实体/列上。上面的“复合唯一索引”等价写法是把@Index放在类上,并传入参与索引的列名数组:
@Entity() @Index(["firstName", "lastName"], { unique: true }) export class User {}@Index在 Index.ts 中提供多种重载形态,按需选用:
- 单列索引:直接标注在列属性上,如
@Index()加在@Column()上; - 带名索引:
@Index("name-idx"); - 列级唯一索引:
@Index({ unique: true }); - 实体级多列索引:
@Index(["col1", "col2"]),可加{ unique: true }变成复合唯一索引。
单一列唯一约束更常见的是直接写@Column({ unique: true })。空间索引(spatial: true)、synchronize关闭后配合迁移脚本手工建索引等更多场景,参见 Indexes 完整文档。
七、迁移路线总结与进一步阅读
把上述各节串起来,一条典型的 Sequelize → TypeORM 迁移路径是:
- 建数据源:把
new Sequelize(db, user, pass, { host, dialect })换成导出的new DataSource({ type, host, username, password, database, entities }); - 小步同步:开发期打开
synchronize: true验证结构,生产切到 Migrations; - 改写模型:把每个
sequelize.define(...)文件改写成“一个实体类一个文件”,并用@Entity/@Column系列装饰器逐字段翻译类型与约束; - 替换读写:把
Model.create、model.get("x")、Model.update等调用点改造成 Repository / EntityManager / BaseEntity 风格; - 平移索引:把模型第三参的
indexes配置翻译成列级或实体级@Index。
每一步都可以回到对应官方文档做深度补充:数据源与连接生命周期见 DataSource 指南 与 DataSourceOptions 参数说明;实体与列的细节见 Entities 文档;仓储层完整 API 见 Repository API;编程范式选型见 Active Record vs Data Mapper。对于仓库中的真实实现,可继续阅读 DataSource.ts、Column.ts、Generated.ts、BaseEntity.ts 等源码文件,验证本文所述机制。
【免费下载链接】typeormTypeScript & JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考