第二章: 实体与列类型
本章目标
- 掌握
@Entity的常用选项(表名、引擎、注释) - 记住 TypeScript 类型与 MySQL 列类型的对应关系
- 会用
@Column的全部常用选项控制列行为 - 理解 transformer(值转换器)并能写一个简单的双向转换
- 知道 decimal、datetime、enum 这三个高频坑
核心概念
实体(Entity)就像表格的设计图纸:类名对应表名,属性对应列,装饰器(@Entity、@Column)就是图纸上的标注——这一列多长、能不能为空、有没有默认值。
TypeORM 启动时(synchronize: true)读图纸建表;运行时把行数据按图纸"浇铸"成类实例。
知识点详解
@Entity 选项(以 TypeORM 1.1.1 的 EntityOptions 为准)
ts
@Entity({
name: "t_member", // 数据库表名;不指定则按命名策略生成(默认类名小写)
engine: "InnoDB", // 建表引擎(仅建表时生效;表已存在时改了也没用)
comment: "会员表", // 表注释
})
class Member {
/* ... */
}| 选项 | 说明 |
|---|---|
name | 表名 |
engine | MySQL 引擎,如 "InnoDB" / "MyISAM" |
comment | 表注释 |
database | 指定该实体属于哪个数据库(MySQL/SqlServer 用) |
schema | Schema 名(Postgres/SqlServer 用,MySQL 用 database) |
synchronize | 设为 false 则该实体不参与自动同步和迁移 |
orderBy | 查询该表时的默认排序 |
TypeScript 类型 ↔ MySQL 列类型映射(实测结果)
| TypeScript 写法 | MySQL 实际列类型 | 读回的 JS 类型 |
|---|---|---|
@PrimaryGeneratedColumn() id: number | int + AUTO_INCREMENT | number |
@Column() name: string | varchar(255) | string |
@Column({ type: "int" }) n: number | int | number |
@Column({ type: "decimal", precision: 10, scale: 2 }) | decimal(10,2) | ⚠️ string("299.90") |
@Column({ type: "boolean" }) b: boolean | tinyint | boolean |
@Column({ type: "datetime" }) d: Date | datetime | Date |
@Column({ type: "text" }) / { type: "longtext" } | text / longtext | string |
@Column({ type: "json" }) | json | object(自动解析) |
@Column({ type: "enum", enum: [...] }) | enum('a','b','c') | string |
@Column({ type: "simple-json" }) | text(内容是一段 JSON 字符串) | object |
@Column({ type: "simple-array" }) | text(逗号拼接) | string[] |
@Column({ type: "blob" }) data: Buffer | blob | Buffer |
说明:
simple-json/simple-array是 TypeORM 的"伪类型"——数据库里只是text,进出时由 TypeORM 帮你序列化/反序列化。json才是 MySQL 原生 JSON 列,可以做 JSON 路径查询。
@Column 选项全解(以 1.1.1 的 ColumnOptions 为准)
| 选项 | 作用 | 示例 |
|---|---|---|
type | 列类型 | "varchar"、"int"、"decimal"、"text"、"json"… |
name | 数据库列名(与属性名不同时用) | { name: "user_name" } |
length | 长度 | { length: 50 } → varchar(50) |
nullable | 允许 NULL,默认 false | { nullable: true } |
unique | 唯一约束 | { unique: true } |
default | 数据库默认值 | { default: 0 } |
comment | 列注释 | { comment: "昵称" } |
select | 默认查询是否返回,默认 true | 密码字段设 select: false |
insert | save/insert 是否写入,默认 true | 数据库自维护列可设 false |
update | save/update 是否更新,默认 true | 创建时间、注册来源设 update: false |
precision / scale | decimal 的总位数/小数位 | { precision: 10, scale: 2 } |
unsigned | 无符号(仅 MySQL) | { type: "int", unsigned: true } |
charset / collation | 列级字符集/排序规则 | { collation: "utf8mb4_bin" } |
onUpdate | ON UPDATE 触发器(仅 MySQL) | { onUpdate: "CURRENT_TIMESTAMP" } |
enum / enumName | 枚举候选值/枚举约束名 | { type: "enum", enum: ["a","b"] } |
transformer | 值转换器(见下) | — |
⚠️ 1.1.1 里没有
width和zerofill选项(早期版本的 int 显示宽度已随 MySQL 8 淘汰),写了会直接报类型错误。
transformer:值转换器
实体里的类型和数据库里的存储形式不一致时,用 transformer 做双向转换:
ts
import { ValueTransformer } from "typeorm";
// JS 里是 boolean,数据库里存 'Y'/'N' 字符串
const ynTransformer: ValueTransformer = {
to: (value?: boolean) => (value ? "Y" : "N"), // JS → 数据库
from: (value?: string) => value === "Y", // 数据库 → JS
};
@Column({ type: "char", length: 1, transformer: ynTransformer })
isVip: boolean;注意:用了 transformer 就要自己选好底层列类型。上面的列如果写成 boolean + length 会直接报错(boolean/tinyint 不支持 length),所以要显式
type: "char", length: 1。
动手实验
演示(1)
ts
import "reflect-metadata";
import { DataSource, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import {
createDatabaseIfNotExists,
BASE_DATASOURCE_OPTIONS,
} from "../utils/db";
@Entity()
class Product {
@PrimaryGeneratedColumn() // int + AUTO_INCREMENT
id: number;
@Column() // string → varchar(255)
name: string;
@Column({ type: "int" }) // 显式指定 int
stock: number;
@Column({ type: "decimal", precision: 10, scale: 2 }) // decimal(10,2)
price: number;
@Column({ type: "boolean" }) // boolean → tinyint(1)
onSale: boolean;
@Column({ type: "datetime" }) // Date → datetime
producedAt: Date;
@Column({ type: "text", nullable: true }) // 长文本
description: string | null;
@Column({ type: "longtext", nullable: true }) // 超长文本
detailHtml: string | null;
@Column({ type: "json", nullable: true }) // 原生 JSON
attrs: Record<string, any> | null;
@Column({
type: "enum",
enum: ["small", "medium", "large"],
default: "medium",
})
size: "small" | "medium" | "large";
@Column({ type: "simple-json", nullable: true }) // 存在 text 列里的 JSON 字符串
extra: Record<string, any> | null;
@Column({ type: "simple-array", nullable: true }) // 逗号拼接存 text 列
tags: string[] | null;
@Column({ type: "blob", nullable: true }) // 二进制
thumbnail: Buffer | null;
}
async function main() {
const dbName = "typeorm_ch02";
await createDatabaseIfNotExists(dbName);
const ds = new DataSource({
...BASE_DATASOURCE_OPTIONS,
database: dbName,
entities: [Product],
synchronize: true,
logging: false,
});
await ds.initialize();
console.log("✅ 1. DataSource 初始化成功,product 表已同步");
// ---------- 1. 打印 MySQL 中真实的列类型 ----------
const columns: any[] = await ds.query(
`SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'product'
ORDER BY ORDINAL_POSITION`,
[dbName],
);
console.log("✅ 2. MySQL 实际列类型(information_schema):");
for (const c of columns) {
console.log(
` ${c.COLUMN_NAME.padEnd(12)} ${c.COLUMN_TYPE.padEnd(16)} NULL=${c.IS_NULLABLE} 默认=${c.COLUMN_DEFAULT}`,
);
}
// ---------- 2. 插入一条 ----------
const repo = ds.getRepository(Product);
await repo.clear();
const saved = await repo.save({
name: "机械键盘",
stock: 100,
price: 299.9,
onSale: true,
producedAt: new Date("2026-01-15T08:30:00"),
description: "87 键,热插拔",
detailHtml: "<p>详细介绍略……</p>",
attrs: { color: "黑", weightKg: 1.2 },
size: "medium",
extra: { gift: true },
tags: ["电竞", "办公", "RGB"],
thumbnail: Buffer.from([0x89, 0x50, 0x4e, 0x47]), // PNG 文件头
});
console.log("✅ 3. 插入成功,id =", saved.id);
// ---------- 3. 读回,逐个观察 JS 侧类型 ----------
const found = (await repo.findOneBy({ id: saved.id }))!;
console.log("✅ 4. 读回后的字段与 JS 类型:");
const show = (label: string, v: any) =>
console.log(
` ${label.padEnd(12)} typeof=${Buffer.isBuffer(v) ? "Buffer" : Array.isArray(v) ? "array" : typeof v} 值=`,
v,
);
show("stock", found.stock);
show("price", found.price); // ⚠️ decimal 读回是字符串!
show("onSale", found.onSale);
show("producedAt", found.producedAt);
show("attrs", found.attrs);
show("size", found.size);
show("extra", found.extra);
show("tags", found.tags);
show("thumbnail", found.thumbnail);
await ds.destroy();
console.log("🎉 第 02 章演示 1 完成");
}
main().catch((e) => {
console.error("❌ 运行失败:", e);
process.exit(1);
});演示(二)
ts
/**
* 第 02 章 · 演示 2:@Column 选项全解 + @Entity 选项 + transformer
* - @Entity:自定义表名、engine、comment
* - @Column:length / nullable / unique / default / comment / select / insert / update / precision / scale / unsigned / charset / collation
* - transformer:JS 侧 boolean ↔ DB 侧 0/1 的自定义双向转换
*/
import "reflect-metadata";
import {
DataSource,
Entity,
PrimaryGeneratedColumn,
Column,
ValueTransformer,
} from "typeorm";
import {
createDatabaseIfNotExists,
BASE_DATASOURCE_OPTIONS,
} from "../utils/db";
/** 自定义 transformer:实体里是 boolean,数据库里存 "Y"/"N" 字符串 */
const ynTransformer: ValueTransformer = {
to: (value?: boolean) => (value ? "Y" : "N"), // JS → 数据库
from: (value?: string) => value === "Y", // 数据库 → JS
};
@Entity({
name: "t_member", // 数据库里的表名(默认会用类名小写 member)
engine: "InnoDB", // 建表引擎
comment: "会员表(演示 @Entity 选项)",
})
class Member {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50, comment: "昵称" }) // varchar(50)
nickname: string;
@Column({ length: 100, unique: true, comment: "邮箱,唯一索引" })
email: string;
@Column({ type: "int", unsigned: true, default: 0, comment: "积分" })
points: number;
@Column({
type: "decimal",
precision: 8,
scale: 2,
default: 0,
comment: "余额",
})
balance: number;
@Column({ nullable: true, comment: "可空字段" })
bio: string | null;
@Column({
select: false, // 默认查询不返回此字段(适合密码等大/敏感字段)
comment: "密码哈希",
})
passwordHash: string;
@Column({
update: false, // 只允许首次插入写入,之后的 save/update 不会更新它
comment: "注册来源,写入后不可改",
})
registerFrom: string;
@Column({
charset: "utf8mb4",
collation: "utf8mb4_bin", // 区分大小写的排序规则
comment: "校验码,大小写敏感",
})
verifyCode: string;
@Column({
type: "char",
length: 1,
transformer: ynTransformer,
comment: "是否 VIP,存 Y/N",
})
isVip: boolean;
}
async function main() {
const dbName = "typeorm_ch02";
await createDatabaseIfNotExists(dbName);
const ds = new DataSource({
...BASE_DATASOURCE_OPTIONS,
database: dbName,
entities: [Member],
synchronize: true,
logging: false,
});
await ds.initialize();
console.log("✅ 1. t_member 表已同步(自定义表名/引擎/表注释)");
const table: any[] = await ds.query(
`SELECT TABLE_NAME, ENGINE, TABLE_COMMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 't_member'`,
[dbName],
);
console.log(" 表信息:", table[0]);
const repo = ds.getRepository(Member);
await repo.clear();
// ---------- 2. 插入(default 生效:points/balance 不传也有值) ----------
const saved = await repo.save({
nickname: "阿强",
email: "aqiang@example.com",
bio: null,
passwordHash: "HASH_ABC123",
registerFrom: "web",
verifyCode: "Ab12",
isVip: true,
});
console.log("✅ 2. 插入成功(未传 points/balance,走数据库默认值):");
console.log(
" points =",
saved.points,
"| balance =",
saved.balance,
"| bio =",
saved.bio,
);
// ---------- 3. select: false 的效果 ----------
const found = (await repo.findOneBy({ id: saved.id }))!;
console.log("✅ 3. 默认 find 不返回 passwordHash:", found.passwordHash); // undefined
// 想查出来,要在 find 选项里显式 select(第 05 章细讲)
const withPwd = await repo.findOne({
where: { id: saved.id },
select: { id: true, passwordHash: true },
});
console.log(" 显式 select 后:", withPwd!.passwordHash);
// ---------- 4. update: false 的效果 ----------
await repo.save({ id: saved.id, registerFrom: "app", nickname: "阿强2" });
const afterSave = (await repo.findOneBy({ id: saved.id }))!;
console.log(
"✅ 4. save 尝试改 registerFrom='app':实际仍是 =",
afterSave.registerFrom,
";nickname 变成 =",
afterSave.nickname,
);
// ---------- 5. transformer 的效果 ----------
console.log("✅ 5. transformer:JS 侧 isVip =", afterSave.isVip, "(boolean)");
const raw: any[] = await ds.query(`SELECT isVip FROM t_member WHERE id = ?`, [
saved.id,
]);
console.log(" 数据库里真实存储 =", raw[0].isVip, "(字符串 Y/N)");
// ---------- 6. unique 约束的效果 ----------
try {
await repo.insert({
nickname: "重复邮箱",
email: "aqiang@example.com",
passwordHash: "x",
registerFrom: "web",
verifyCode: "Zz99",
isVip: false,
});
} catch (e: any) {
console.log(
"✅ 6. unique 生效:重复 email 插入被拒 →",
e.code,
e.sqlMessage?.slice(0, 50),
);
}
await ds.destroy();
console.log("🎉 第 02 章演示 2 完成");
}
main().catch((e) => {
console.error("❌ 运行失败:", e);
process.exit(1);
});小结
@Entity控制表级:表名、引擎、注释;@Column控制列级:类型、长度、约束、默认值、读写行为- string→varchar(255)、number→int、boolean→tinyint、Date→datetime;
json原生、simple-json/simple-array实为 text select/insert/update三个开关控制列在查询、插入、更新中的参与与否- transformer 是 JS 值 ↔ 数据库存储值的双向翻译器
- 三大高频坑:decimal 读出是字符串、datetime 时区、enum 变更需改表