Skip to content

ORM 与 QueryWrapper

模块位于 src/core/ORM:基于 mysql2 连接池。实体用装饰器描述表结构,Mapper / Service 提供 CRUD;SQL 装饰器可写自定义语句;字段级 @Select 可按父行填充关联数据。

装饰器从 @/core/ORM@/core/Service 导入:

ts
import {
  TableName,
  TableId,
  TableField,
  Select,
  Insert,
  Update,
  Delete,
  Param,
  QueryWrapper,
  hydrateFieldSelects,
} from '@/core/ORM'
import { Mapper, Service, MapperType } from '@/core/Service'

一、实体装饰器

@TableName(tableName)

作用:类装饰器。声明这个 class 对应哪张数据库表。

参数

参数类型必填含义
tableNamestring表名,不能为空

说明@Mapper(Entity) 会读这里的表名。不写则无法按实体绑定 Mapper。

ts
@TableName('user')
export default class UserEntity {}

@TableId(valueOrOptions?)

作用:属性装饰器。标记主键。selectById / updateById / deleteById 用这个列。

可以怎么传

写法含义
@TableId()列名 = 属性名(如 id
@TableId('user_id')列名显式为 user_id
@TableId({ value: 'user_id' })同上

TableIdOptions

字段类型默认含义
valuestring属性名数据库列名
ts
@TableId()
id!: string

@TableId('user_id')
userId!: string

@TableField(valueOrOptions?)

作用:属性装饰器。把 class 属性映射到表列;可声明插入/更新默认值;也可标成「非表字段」。

可以怎么传

写法含义
@TableField()列名 = 属性名
@TableField('nick_name')列名为 nick_name
@TableField({ value, exist, insertFill, updateFill })完整配置

TableFieldOptions

字段类型默认含义
valuestring属性名数据库列名
existbooleantruefalse:不是表列,不参与 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 列。

ts
@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.tsmenu.ts


二、Mapper / Service 装饰器

@Mapper(tableOrEntity, idFieldOrOptions?)

作用:类装饰器。把 BaseMapper 的 CRUD 混进这个类,并按类名注册到 IoC。

第一个参数

写法含义
'user'表名字符串
UserEntity读实体上的 @TableName / @TableId,并绑定实体(字段 @Select、fill 才生效)

第二个参数(可选)

写法含义
'id'(默认)主键列名
{ ...MapperTableOptions }表级开关

MapperTableOptions

字段类型默认含义
idFieldstring实体主键列或 'id'主键列
logicDeleteboolean跟全局 .env是否逻辑删除
logicDeleteFieldstringdelete_flag删除标记列
logicDeleteValue任意1「已删」取值
logicNotDeleteValue任意0「未删」取值
optimisticLockboolean全局是否乐观锁
versionFieldstringversion版本列
fillbooleantrue是否自动填 create/update 等
entity传 Entity 时自动带上用于 fill 和字段 @Select
ts
@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 字段 @SelectT[]
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 不能为空影响行数

selectPagecurrent 从 1 开始,size 默认 10。


@Service(MapperClass)

作用:类装饰器。把上面那些 CRUD 以及 Mapper 上自定义 SQL 方法挂到 Service 原型,并按类名注册 Bean。

参数

参数类型含义
MapperClassMapper 构造函数要委托的 Mapper
ts
@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 引用。

参数类型含义
namestring占位符名,不能为空
ts
@Select('SELECT * FROM user WHERE id = #{id}')
findById(@Param('id') id: string) {}

@Select(sql)(方法)

作用:执行查询 SQL。

参数类型含义
sqlstringSELECT 语句,不能为空

返回:行数组 RowDataPacket[]

注意:方法级 @Select 不会自动填实体上的字段 @Select(防止列表 N+1)。需要时自己调 hydrateFieldSelects

ts
@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)

作用:执行插入。

参数类型含义
sqlstringINSERT 语句

返回insertId(数字)。

ts
@Insert('INSERT INTO user(username, nick_name) VALUES(#{username}, #{nickName})')
insertUser(@Param('username') username: string, @Param('nickName') nickName: string) {}

@Update(sql)

作用:执行更新。

返回affectedRows

ts
@Update('UPDATE user SET nick_name = #{nickName} WHERE id = #{id}')
updateNick(@Param('id') id: string, @Param('nickName') nickName: string) {}

@Delete(sql)

作用:执行 物理删除。逻辑删除请用 deleteById / delete

返回affectedRows

ts
@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 })

签名

ts
@Select(sql: string, options?: FieldSelectOptions)
参数类型含义
sqlstring关联查询 SQL。父行用 #{id}#{parentId}#{parent_id} 都能取到(hydrate 会同时提供属性名 / 列名 / camel / snake)
options对象见下表

FieldSelectOptions

字段类型默认含义
manybooleanfalsetrue:一对多,结果是数组;false:取第一行(或单个标量)
columnstring只取这一列。many=true → 标量数组;many=false → 单个标量。不传则 many 时是行对象数组,否则是第一行对象
eagerbooleantruetrueselectList / selectPage / selectById 之后自动填充;false:必须手动 hydrateFieldSelectsincludeLazy: true
depthnumber0对嵌套出来的同行实体再填 @Select 的层数。0 不递归。树形慎用

示例:标量(eager)

selectById 时自动带上角色名:

ts
@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:

ts
@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 / selectByIdeager !== false
Mapper 方法上的 @Select不填
eager: false不填,直到手动 hydrate

五、hydrateFieldSelects

作用:按实体上的字段 @Select 原地给 rows 填关联数据。

ts
await hydrateFieldSelects(entity, rows, options?)
参数类型含义
entity实体类MenuEntity
rows对象数组会被原地改写并原样返回
options对象或 number见下;传数字等于 { maxDepth: n }

HydrateFieldSelectOptions

字段类型默认含义
maxDepthnumber32递归上限
includeLazybooleanfalsetrue 时连 eager: false 的字段也填
propertiesstring[]全部符合条件的字段只填这些属性名
ts
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,列名走白名单,值全部 ? 绑定。

ts
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 / isNotNullIS NULL / IS NOT NULL

连接与分组

方法含义
or()下一条条件用 OR(默认 AND)
and()下一条用 AND
nested(fn)括号分组:fn 里的条件整体再接到外层
apply(sql, params?)自定义片段(自己保证列名安全),会再包一层括号
ts
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 字段名填充
ts
@Mapper(UserEntity, { logicDelete: true, fill: true })
export default class UserMapper extends MapperType {}

八、Db

ts
import Db from '@/core/ORM'

await Db.query('SELECT 1', [])
await Db.transaction(async (conn) => {
  // ...
})

模块加载时会尝试连 MySQL;失败只打日志,第一次 query 再试。


相关章节

基于 VitePress 构建