ORM 与 QueryWrapper
模块位于 src/core/ORM:基于 mysql2 连接池。实体用装饰器描述表结构,Mapper / Service 提供 CRUD;SQL 装饰器可写自定义语句;字段级 @Select 可按父行填充关联数据。
装饰器从 @/core/ORM 或 @/core/Service 导入:
import {
TableName,
TableId,
TableField,
Select,
Insert,
Update,
Delete,
Param,
QueryWrapper,
hydrateFieldSelects,
} from '@/core/ORM'
import { Mapper, Service, MapperType } from '@/core/Service'一、实体装饰器
@TableName(tableName)
作用:类装饰器。声明这个 class 对应哪张数据库表。
参数
| 参数 | 类型 | 必填 | 含义 |
|---|---|---|---|
tableName | string | 是 | 表名,不能为空 |
说明:@Mapper(Entity) 会读这里的表名。不写则无法按实体绑定 Mapper。
@TableName('user')
export default class UserEntity {}@TableId(valueOrOptions?)
作用:属性装饰器。标记主键。selectById / updateById / deleteById 用这个列。
可以怎么传
| 写法 | 含义 |
|---|---|
@TableId() | 列名 = 属性名(如 id) |
@TableId('user_id') | 列名显式为 user_id |
@TableId({ value: 'user_id' }) | 同上 |
TableIdOptions
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
value | string | 属性名 | 数据库列名 |
@TableId()
id!: string
@TableId('user_id')
userId!: string@TableField(valueOrOptions?)
作用:属性装饰器。把 class 属性映射到表列;可声明插入/更新默认值;也可标成「非表字段」。
可以怎么传
| 写法 | 含义 |
|---|---|
@TableField() | 列名 = 属性名 |
@TableField('nick_name') | 列名为 nick_name |
@TableField({ value, exist, insertFill, updateFill }) | 完整配置 |
TableFieldOptions
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
value | string | 属性名 | 数据库列名 |
exist | boolean | true | false:不是表列,不参与 insert/update 默认 SQL。关联字段、组装字段用这个 |
insertFill | 常量或 () => value(可 async) | — | insert 时,该列仍是 undefined 才写入 |
updateFill | 同上 | — | update 时同样只填 undefined |
insertFill / updateFill 可以是:
- 常量:
insertFill: 0 - 同步函数:
insertFill: () => new Date() - 异步函数:
insertFill: async () => await AuthUtil.getLoginIdDefaultNull()
已有值不会被覆盖。填充写入的是列名(如 create_time),避免 camelCase 被当成 SQL 列。
@TableField()
username!: string
@TableField('nick_name')
nickName?: string
@TableField({
value: 'create_time',
insertFill: () => new Date(),
})
createTime?: Date
@TableField({
value: 'update_time',
insertFill: () => new Date(),
updateFill: () => new Date(),
})
updateTime?: Date
@TableField({
value: 'delete_flag',
insertFill: 0,
})
deleteFlag?: number
/** 非表字段:只用于返回或关联 */
@TableField({ exist: false })
parentTitle?: string完整实体示例见 src/business/entity/user.ts、menu.ts。
二、Mapper / Service 装饰器
@Mapper(tableOrEntity, idFieldOrOptions?)
作用:类装饰器。把 BaseMapper 的 CRUD 混进这个类,并按类名注册到 IoC。
第一个参数
| 写法 | 含义 |
|---|---|
'user' | 表名字符串 |
UserEntity | 读实体上的 @TableName / @TableId,并绑定实体(字段 @Select、fill 才生效) |
第二个参数(可选)
| 写法 | 含义 |
|---|---|
'id'(默认) | 主键列名 |
{ ...MapperTableOptions } | 表级开关 |
MapperTableOptions
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
idField | string | 实体主键列或 'id' | 主键列 |
logicDelete | boolean | 跟全局 .env | 是否逻辑删除 |
logicDeleteField | string | delete_flag | 删除标记列 |
logicDeleteValue | 任意 | 1 | 「已删」取值 |
logicNotDeleteValue | 任意 | 0 | 「未删」取值 |
optimisticLock | boolean | 全局 | 是否乐观锁 |
versionField | string | version | 版本列 |
fill | boolean | true | 是否自动填 create/update 等 |
entity | 类 | 传 Entity 时自动带上 | 用于 fill 和字段 @Select |
@Mapper(UserEntity, { logicDelete: true, fill: true })
export default class UserMapper extends MapperType {}
@Mapper('user', { logicDelete: true })
export default class UserMapper extends MapperType {}推荐传 Entity,否则 selectList 不会自动填充字段级 @Select。
混入的方法:
| 方法 | 含义 | 返回 |
|---|---|---|
selectById(id) | 按主键 | 一行或 null |
selectOne(wrapper?) | 条件取一条(内部 limit 1) | 一行或 null |
selectList(wrapper?, limit?) | 列表;会 hydrate eager 字段 @Select | T[] |
selectCount(wrapper?) | 计数 | number |
selectPage({ current, size }, wrapper?) | 分页;records 同样 hydrate | { records, total, current, size } |
insert(entity) | 插入 | 影响行数 |
updateById(entity) | 按主键更新(entity 必须带 id) | 影响行数 |
update(entity, wrapper) | 按条件更新,wrapper 不能为空 | 影响行数 |
deleteById(id) | 逻辑删除或物理删除 | 影响行数 |
delete(wrapper) | 条件删除,wrapper 不能为空 | 影响行数 |
selectPage 的 current 从 1 开始,size 默认 10。
@Service(MapperClass)
作用:类装饰器。把上面那些 CRUD 以及 Mapper 上自定义 SQL 方法挂到 Service 原型,并按类名注册 Bean。
参数
| 参数 | 类型 | 含义 |
|---|---|---|
MapperClass | Mapper 构造函数 | 要委托的 Mapper |
@Service(UserMapper)
export default class UserService extends BaseService {
async getUser(id: string) {
return this.selectById(id)
}
}Controller 里:@Resource('UserService')。
三、SQL 装饰器(方法级)
写在 Mapper 方法上。#{} 预编译绑定;${} 字符串替换(不要接用户输入)。
占位符怎么取值:
| 来源 | 示例 |
|---|---|
@Param('id') | #{id} |
| 单参数对象 | 展开字段,#{username} |
| 单参数原始值 | 任意 #{xxx} 都映射到这个值 |
| 自动别名 | #{param1}、#{arg0} |
| 路径 | #{user.id} |
#{id, jdbcType=INTEGER} 这种写法会忽略逗号后面的部分,只用 id。
@Param(name)
作用:参数装饰器。给方法参数起名,供 SQL 引用。
| 参数 | 类型 | 含义 |
|---|---|---|
name | string | 占位符名,不能为空 |
@Select('SELECT * FROM user WHERE id = #{id}')
findById(@Param('id') id: string) {}@Select(sql)(方法)
作用:执行查询 SQL。
| 参数 | 类型 | 含义 |
|---|---|---|
sql | string | SELECT 语句,不能为空 |
返回:行数组 RowDataPacket[]。
注意:方法级 @Select 不会自动填实体上的字段 @Select(防止列表 N+1)。需要时自己调 hydrateFieldSelects。
@Select('SELECT * FROM user WHERE username = #{username}')
findByUsername(@Param('username') username: string) {}
@Select('SELECT * FROM user WHERE id = #{id} AND status = #{status}')
findOne(@Param('id') id: string, @Param('status') status: number) {}@Insert(sql)
作用:执行插入。
| 参数 | 类型 | 含义 |
|---|---|---|
sql | string | INSERT 语句 |
返回:insertId(数字)。
@Insert('INSERT INTO user(username, nick_name) VALUES(#{username}, #{nickName})')
insertUser(@Param('username') username: string, @Param('nickName') nickName: string) {}@Update(sql)
作用:执行更新。
返回:affectedRows。
@Update('UPDATE user SET nick_name = #{nickName} WHERE id = #{id}')
updateNick(@Param('id') id: string, @Param('nickName') nickName: string) {}@Delete(sql)
作用:执行 物理删除。逻辑删除请用 deleteById / delete。
返回:affectedRows。
@Delete('DELETE FROM user_token WHERE user_id = #{userId}')
clearToken(@Param('userId') userId: string) {}这些自定义方法会经 @Service(Mapper) 转发到 Service,可直接 this.userService.findByUsername('admin')。
四、字段级 @Select(关联填充)
同一个 @Select,用在实体属性上时是另一套语义:按当前行再查一次 SQL,把结果写到该属性。
非表列请同时加 @TableField({ exist: false })。
签名
@Select(sql: string, options?: FieldSelectOptions)| 参数 | 类型 | 含义 |
|---|---|---|
sql | string | 关联查询 SQL。父行用 #{id}、#{parentId}、#{parent_id} 都能取到(hydrate 会同时提供属性名 / 列名 / camel / snake) |
options | 对象 | 见下表 |
FieldSelectOptions
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
many | boolean | false | true:一对多,结果是数组;false:取第一行(或单个标量) |
column | string | — | 只取这一列。many=true → 标量数组;many=false → 单个标量。不传则 many 时是行对象数组,否则是第一行对象 |
eager | boolean | true | true:selectList / selectPage / selectById 之后自动填充;false:必须手动 hydrateFieldSelects 且 includeLazy: true |
depth | number | 0 | 对嵌套出来的同行实体再填 @Select 的层数。0 不递归。树形慎用 |
示例:标量(eager)
selectById 时自动带上角色名:
@TableField({ exist: false })
@Select(
`SELECT name FROM role
WHERE id = (
SELECT role_id FROM menu_role WHERE menu_id = #{id} LIMIT 1
)`,
{ column: 'name' }
)
roleName?: string示例:一对多(lazy,避免 N+1)
全表查菜单时不要自动查每个节点的 children:
@TableField({ exist: false })
@Select(
`SELECT id, name, path, title, parent_id, sort_num
FROM menu
WHERE parent_id = #{id} AND (delete_flag IS NULL OR delete_flag != 1)
ORDER BY sort_num ASC`,
{ many: true, eager: false, depth: 0 }
)
children?: MenuEntity[]仓库完整示例:src/business/entity/menu.ts。
何时自动填充
| 调用 | 会填哪些字段 @Select |
|---|---|
selectList / selectPage / selectOne / selectById | 仅 eager !== false |
Mapper 方法上的 @Select | 不填 |
eager: false | 不填,直到手动 hydrate |
五、hydrateFieldSelects
作用:按实体上的字段 @Select 原地给 rows 填关联数据。
await hydrateFieldSelects(entity, rows, options?)| 参数 | 类型 | 含义 |
|---|---|---|
entity | 实体类 | 如 MenuEntity |
rows | 对象数组 | 会被原地改写并原样返回 |
options | 对象或 number | 见下;传数字等于 { maxDepth: n } |
HydrateFieldSelectOptions
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
maxDepth | number | 32 | 递归上限 |
includeLazy | boolean | false | true 时连 eager: false 的字段也填 |
properties | string[] | 全部符合条件的字段 | 只填这些属性名 |
import { hydrateFieldSelects } from '@/core/ORM'
import MenuEntity from '@/business/entity/menu'
const rows = await this.selectList(wrapper)
// 只填 eager(selectList 其实已经做过一遍)
await hydrateFieldSelects(MenuEntity, rows)
// 填懒加载的 children
await hydrateFieldSelects(MenuEntity, [row], {
includeLazy: true,
properties: ['children'],
})
await hydrateFieldSelects(MenuEntity, rows, { maxDepth: 2, includeLazy: true })某字段 SQL 失败会抛:[FieldSelect] MenuEntity.children 填充失败: ...。
六、QueryWrapper
链式拼 WHERE,列名走白名单,值全部 ? 绑定。
const w = new QueryWrapper()
.select('id', 'username')
.eq('status', 1)
.like('username', keyword)
.orderByDesc('create_time')比较 / 集合
| 方法 | SQL 含义 |
|---|---|
eq(col, v) | col = ? |
eqOrIsNull(col, v) | col = ? OR col IS NULL |
ne(col, v) | col <> ? |
gt / ge / lt / le | > / >= / < / <= |
like(col, v) | LIKE %v%,会转义 % _ |
in(col, arr) | IN (...);空数组变成 1 = 0 |
between(col, a, b) | BETWEEN ? AND ? |
isNull / isNotNull | IS NULL / IS NOT NULL |
连接与分组
| 方法 | 含义 |
|---|---|
or() | 下一条条件用 OR(默认 AND) |
and() | 下一条用 AND |
nested(fn) | 括号分组:fn 里的条件整体再接到外层 |
apply(sql, params?) | 自定义片段(自己保证列名安全),会再包一层括号 |
wrapper.nested((w) => {
w.like('config_key', keyword)
.or()
.like('config_name', keyword)
})其它
| 方法 | 含义 |
|---|---|
select(...cols) | 指定列;不传恢复 * |
orderByAsc / orderByDesc | 排序,可多次 |
ignoreLogicDelete() | 本查询不加「未删除」条件 |
clone() | 拷贝,避免被 Mapper 改掉 |
last(sql) | 只能是 LIMIT n [OFFSET m] / FOR UPDATE / LOCK IN SHARE MODE |
build() | 得到 { selectSql, whereSql, orderSql, lastSql, params } |
七、逻辑删除 / 乐观锁 / 全局填充
表级 @Mapper(..., { logicDelete, fill, ... }) 覆盖 .env 全局项,见 环境配置。
| 能力 | 行为 |
|---|---|
| 逻辑删除 | delete* 改成更新删除标记;查询默认带未删除条件 |
| 乐观锁 | 更新带 version,冲突抛 OptimisticLockError(业务码一般 409) |
fill: true | 按实体 insertFill/updateFill 以及全局 create/update 字段名填充 |
@Mapper(UserEntity, { logicDelete: true, fill: true })
export default class UserMapper extends MapperType {}八、Db
import Db from '@/core/ORM'
await Db.query('SELECT 1', [])
await Db.transaction(async (conn) => {
// ...
})模块加载时会尝试连 MySQL;失败只打日志,第一次 query 再试。