Fluent DSL 指引
本页说明在后端 Java 服务中如何使用 MetaFactory 提供的 Fluent DSL 访问 ORM 能力。
目标
- 面向后端开发者提供一套 Java 风格的元数据 CRUD 入口
- 避免直接手写 MQL JSON,同时继续复用
MetaQLManager + SqlManager + Dao现有内核
在整体 ORM 中的位置
当前 ORM 体系里,Fluent DSL 的定位是“后端 Java API”,它与注解、MQL、事件/扩展机制之间的分工如下:
- ORM 注解负责声明实体元数据
- MQL 负责前端和平台协议侧的 JSON 数据访问
- Fluent DSL 负责后端 Java 服务中的链式 CRUD 与轻量高级查询
- 事件、动态数据源、查询过滤/字段填充 SPI 负责把平台规则注入到执行链路
因此,Fluent DSL 不是 MQL 的字符串包装器,而是面向 Java 服务代码的独立入口。
何时使用
- 需要在 Java 服务代码中按实体名或实体类做元数据查询、保存、更新、删除时使用
- 需要复用现有动态数据源、视图模板参数、
$ctx/$fn/$parent这类内核能力时使用
何时不使用
- 前端页面直接走平台通用数据接口时,继续使用
MetaController + MQL - 已有服务已经稳定依赖
BaseService + 实体类且没有元数据通用化诉求时,可继续沿用原模式 - 查询已经明显转向 SQL-first,且更适合直接维护完整 SQL / MyBatis 时,不建议强行转成 Fluent DSL
入口
字符串实体名:
List<Map<String, Object>> users = MetaFactory.query("User")
.select(new String[]{"id", "name", "mobilePhone"})
.where(Filter.eq("delStatus", 0))
.order(Order.desc("updateAt"))
.list();
实体类:
List<Map<String, Object>> users = MetaFactory.query(User.class)
.select(new String[]{"id", "name"})
.page(1, 20)
.list();
接入与装配
这一节把“依赖、Bean、元数据准备”串起来,确保你在独立 Spring Boot 工程里可以直接使用 Fluent DSL。
对应示例工程:
- 仓库:geelato-hello-example
- ORM 接入示例:geelato-sample-orm
依赖
最小只需要引入:
<dependency>
<groupId>cn.geelato</groupId>
<artifactId>geelato-orm</artifactId>
</dependency>
数据库驱动(MySQL / PostgreSQL 等)由业务工程按自身数据库类型自行引入。
Dao Bean(必须)
ORM 会在 Spring 容器中存在 Dao Bean 时,自动装配 MetaCommandExecutor,从而让 MetaFactory.*().list/save/delete 可执行。
最小示例:
@Configuration
public class OrmDaoConfiguration {
@Bean
public Dao primaryDao(JdbcTemplate jdbcTemplate) {
return new Dao(jdbcTemplate);
}
}
多 Dao 场景如何选
自动解析优先绑定 dynamicDao(只有它能支撑 useDataSource(connectId) 切库),不存在时回退 primaryDao,再回退唯一的 Dao Bean,无需显式配置。仅当需要绑定其他自定义 Dao 时:
geelato:
orm:
dao-bean-name: myDao
元数据准备(@Entity)
默认会扫描 Spring Boot 启动类所在包及子包内所有 @Entity 类并注册元数据。
动态数据源
Fluent DSL 支持在链式调用中显式切换数据源:
List<Map<String, Object>> rows = MetaFactory.query("DevDbConnect")
.useDataSource("portal")
.page(1, 10)
.list();
当你的工程中存在名为 primaryJdbcTemplate 的 Bean 时,ORM 会自动装配动态数据源相关 Bean(dynamicDataSource、dynamicJdbcTemplate、dynamicDao),供框架内动态数据源链路使用。
快速开始(从 0 到 CRUD)
一个最短示例链路:
@Entity(name = "TestUser", table = "test_user")
public class TestUserEntity {
@Id
@Col(name = "id", dataType = "BIGINT")
private String id;
@Col(name = "name", dataType = "VARCHAR", charMaxlength = 128)
private String name;
}
String id = MetaFactory.insert("TestUser")
.value("name", "Alice")
.save();
Map<String, Object> row = MetaFactory.query("TestUser")
.where(Filter.eq("id", id))
.one();
MetaFactory.update("TestUser")
.value("id", id)
.value("name", "Bob")
.save();
MetaFactory.delete("TestUser")
.where(Filter.eq("id", id))
.delete();
查询示例
单条查询:
Map<String, Object> user = MetaFactory.query("User")
.where(Filter.eq("id", "1912345678901234567"))
.one();
分页查询:
PageResult<Map<String, Object>> page = MetaFactory.query("User")
.select(new String[]{"id", "name", "updateAt"})
.where(Filter.like("name", "张"))
.order(Order.desc("updateAt"))
.page(1, 10)
.page();
单列查询:
List<String> ids = MetaFactory.query("User")
.select(new String[]{"id"})
.where(Filter.eq("delStatus", 0))
.oneColumn(String.class);
查询结果类型(自动解包与显式类型重载):
未设置 wrapperResult(...) 时,若查询结果每行只有一列,list()/one()/page() 会自动返回该列裸值,不再包装成 Map:
List<String> names = MetaFactory.query("User")
.select(new String[]{"name"})
.where(Filter.eq("delStatus", 0))
.list();
多列查询行为不变,仍返回 Map<String, Object>;裸值不做类型转换。受 Java 泛型擦除限制,无参 list() 无法感知声明类型,自动解包依据是结果集列数;需要类型转换或映射成对象时,使用显式类型重载 list(Class)/one(Class):
| 目标类型 | 行为 |
|---|---|
| 简单类型(String、数值、Boolean、Date、java.time 等) | 要求单列,并做值转换(如 Long 转 Integer/String) |
Map.class | 行 Map 原样返回 |
| 自定义类(DTO/实体) | 行数据映射为对象实例,支持下划线列名到驼峰字段(email_address 转 emailAddress) |
List<Integer> ages = MetaFactory.sql("select age from platform_user where del_status = ?")
.param(0)
.list(Integer.class);
List<UserOrgDto> orgs = MetaFactory.sql("select o.id, o.name, oru.default_org as defaultOrg from platform_org_r_user oru ...")
.param("U1001")
.list(UserOrgDto.class);
MetaFactory.sql(...) / MetaFactory.query(...) / MetaFactory.procedure(...) 三个入口的 list()/one()(含分页 page() 的记录)均遵循以上规则;oneColumn(Class) 保留用于执行器层单列查询。设置了 wrapperResult(...) 时优先走 wrapper;单列查询若确实需要保留 Map,可用 .wrapperResult(row -> row)。
复杂转换仍推荐 wrapperResult:
List<UserSimpleDto> users = MetaFactory.query("User")
.where(Filter.eq("delStatus", 0))
.wrapperResult(row -> new UserSimpleDto(
String.valueOf(row.get("id")),
maskMobilePhone(String.valueOf(row.get("mobilePhone")))
))
.list();
写入示例
新增:
String userId = MetaFactory.insert("User")
.value("name", "测试用户")
.value("mobilePhone", "13800000000")
.save();
默认字段会在保存前自动补齐:
- 新增场景默认对齐 MQL 规则,自动补
createAt / creator / creatorName / tenantCode / buId / deptId / updateAt / updater / updaterName / deleteAt - 更新场景默认自动补
updateAt / updater / updaterName - 这些规则已经改为通过
FluentSaveFieldValueFillerSPI 注入;平台默认实现位于geelato-web-platform - 运行时遵循统一规则:
0个实现跳过,1个实现按isEnabled()决定是否执行,多实现直接报错 - 如需扩展宿主项目自己的规则,建议阅读:查询过滤与字段填充 SPI 扩展
更新:
String userId = MetaFactory.update("User")
.value("id", "1912345678901234567")
.value("name", "新名称")
.save();
删除:
int affected = MetaFactory.delete("User")
.where(Filter.eq("id", "1912345678901234567"))
.delete();
基于实体对象的便捷重载
除了按字段名逐个 .value(field, value) 构建外,MetaFactory 还提供一组直接传入实体对象的重载:按实体对象的非空属性预填 SQL,省去手工拼字段值。需要切换数据源时,仍通过返回构建器的 .useDataSource(...) 链式指定。
约定:仅采集实体对象中非
null的映射属性;null字段不参与写入/条件(插入回退默认值,更新不覆盖已有列)。实体需是带 getter/setter 的 JavaBean(例如继承IdEntity并补齐访问器)。
直接保存(一行执行,自动判定插入/更新):
User user = new User();
user.setName("测试用户");
user.setMobilePhone("13800000000");
// 主键为空 → 插入,主键由框架自动生成
String id = MetaFactory.save(user);
user.setId(id);
user.setName("新名称");
// 主键非空 → 按主键更新
MetaFactory.save(user);
按实体对象构建(返回构建器,可继续链式):
// 插入构建器
MetaFactory.insert(user).useDataSource("portal").save();
// 更新构建器(主键非空时自动追加 where(主键))
MetaFactory.update(user).useDataSource("portal").save();
// 按属性等值查询:非空字段作为条件
List<Map<String, Object>> rows = MetaFactory.query(user)
.order(Order.desc("createAt"))
.list();
说明:
save(entity)是唯一“一行直接执行”的便捷方法,返回主键;其余对象重载返回构建器,与insert(Class)/update(Class)/query(Class)一致- 主键判定:主键为空(
null或空白字符串)→ 插入;主键非空 → 按主键更新 - 对象重载与字段名版底层完全等价,复用同一套默认字段填充、UID 生成、动态数据源能力
高级能力
多表 join:
List<Map<String, Object>> orders = MetaFactory.query("Order")
.select(new String[]{"id", "code"})
.selectRef("userId->name", "userName")
.list();
自定义 join on:
List<Map<String, Object>> rows = MetaFactory.query("Order")
.as("o")
.select(new String[]{"id", "code"})
.selectExpr("u.name", "userName")
.leftJoin("User", "u", on -> on.eqField("userId", "u.id"))
.groupBy("id", "code", "u.name")
.havingSql("count(*) > 0")
.page(1, 20)
.page();
动态数据源:
List<Map<String, Object>> rows = MetaFactory.query("DevDbConnect")
.useDataSource("portal")
.page(1, 10)
.list();
跳过注入过滤:
// 跳过本次查询的注入过滤(租户隔离、数据权限等都不会附加)
// 仅适用于系统后台、定时任务等可信场景;存在跨租户/越权读数风险
List<Map<String, Object>> rows = MetaFactory.query("platform_user")
.disableInjectFilter()
.list();
disableInjectFilter() 会跳过本次查询的全部注入器。若某个注入器(如租户隔离)需要保证不可被单次查询绕过,可在其实现中将 FluentQueryFilterInjector.isForceInject() 覆盖为 true,此时 disableInjectFilter() 对该注入器失效。详见 查询过滤与字段填充 SPI 扩展。
视图模板参数:
List<Map<String, Object>> rows = MetaFactory.query("SomeViewEntity")
.viewParams(Map.of("customerId", "C001"))
.page(1, 10)
.list();
上下文与函数值引用:
String id = MetaFactory.insert("Notice")
.value("creator", ValueRefs.ctx("userId"))
.value("createAt", ValueRefs.fnNowDateTime())
.save();
MySQL 存储过程:
List<Map<String, Object>> rows = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.useDataSource("portal")
.list();
原生 SQL 直通执行:
List<Map<String, Object>> rows = MetaFactory.sql("select id, name from platform_user where del_status = ?")
.param(0)
.useDataSource("portal")
.list();
父子嵌套保存:
String parentId = MetaFactory.insert("App")
.value("name", "demo-app")
.child("AppVersion", child -> child
.value("appId", ValueRefs.parent("id"))
.value("code", "v1"))
.save();
多表 join 补充说明
- 自动关联 join 适合“主实体存在外键元数据”的场景,
selectRef("userId->name", "userName")的含义是主表外键字段映射到关联表字段,并输出为结果别名 - 自动关联 join 会基于元数据外键自动补
left join,业务侧不需要手写on - 若只写
selectRef("userId->name"),结果列名默认沿用远端字段名;建议在接口对外返回时显式设置别名,避免与主表字段重名
List<Map<String, Object>> orders = MetaFactory.query("Order")
.select(new String[]{"id", "code", "amount"})
.selectRef("userId->name", "userName")
.selectRef("userId->mobilePhone", "userMobile")
.where(Filter.eq("delStatus", 0))
.order(Order.desc("updateAt"))
.list();
- 自定义 join 适合“没有外键元数据”或“需要显式控制关联方式”的场景
- 推荐固定一套别名约定:主表先调用
.as("o"),关联表使用短别名,如"u"、"d"、"t" selectExpr(...)、groupBy(...)、havingSql(...)中都使用同一套别名leftJoin/innerJoin/rightJoin的on当前推荐优先使用eqField(...)
List<Map<String, Object>> rows = MetaFactory.query("Order")
.as("o")
.select(new String[]{"id", "code"})
.selectExpr("u.name", "userName")
.selectExpr("d.name", "deptName")
.leftJoin("User", "u", on -> on.eqField("o.userId", "u.id"))
.leftJoin("Dept", "d", on -> on.eqField("u.deptId", "d.id"))
.groupBy("id", "code", "u.name", "d.name")
.havingSql("count(*) > 0")
.page(1, 20)
.page();
存储过程补充说明
- 当前存储过程能力定位为 MySQL 场景下的轻量调用封装
- 适用于过程只接收
IN参数、只返回单个结果集、业务侧主要通过list()或one()读取结果的场景 - 参数通过
.in(name, value)按调用顺序加入,占位符顺序与.in(...)的书写顺序一致
Map<String, Object> row = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.one();
过程结果映射成对象,可直接使用显式类型重载;复杂转换仍可复用 wrapperResult(...):
List<OrderSimpleDto> rows = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.list(OrderSimpleDto.class);
List<OrderSimpleDto> wrapped = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.wrapperResult(row -> {
OrderSimpleDto dto = new OrderSimpleDto();
dto.setId(String.valueOf(row.get("id")));
dto.setCode(String.valueOf(row.get("code")));
return dto;
})
.list();
原生 SQL 直通补充说明
- 当业务侧已经持有完整 SQL,且不希望再拆分成
MetaQuery/MetaInsert/MetaUpdate时,可使用MetaFactory.sql(...) - 该入口属于“直通执行”能力,ORM 只负责动态数据源切换、参数顺序绑定,以及
list()/one()/queryForObject()/execute()终止执行 - 该入口不会为原生 SQL 自动补元数据字段、外键 join、默认审计字段或条件表达式转换
- 单列查询结果会自动解包为裸值;
list(Class)/one(Class)支持类型转换与 DTO 映射(规则见“查询示例”一节)
Map<String, Object> row = MetaFactory.sql("select id, name from platform_user where id = ?")
.param("U1001")
.one();
List<String> emails = MetaFactory.sql("select email_address from platform_user_email_account where user_id = ?")
.param("U1001")
.list();
Long total = MetaFactory.sql("select count(1) from platform_user where del_status = ?")
.param(0)
.queryForObject(Long.class);
int affected = MetaFactory.sql("update platform_notice set status = ? where id = ?")
.params("read", "N1001")
.execute();
调试与排障
toSql()用于查看当前查询或存储过程最终生成的 SQLtoCountSql()只适用于分页查询,常用于排查分页总数不准确的问题- 排查 join 问题时,优先检查主表别名、join 别名、
selectExpr/groupBy/havingSql是否一致 - 排查存储过程问题时,优先检查
.in(...)参数顺序、当前数据源、过程结果集数量 - 排查原生 SQL 问题时,优先检查 SQL 本身是否可直接执行、占位符数量是否与
.param/.params一致、终止方法是否选对
推荐使用边界
- 简单单表 CRUD、带少量关联字段的列表页、轻量聚合查询,优先使用 Fluent DSL
- 已存在外键元数据时,优先使用
selectRef(...) - 需要显式控制关联表、别名和
on条件时,再使用leftJoin/innerJoin/rightJoin - 已经存在成熟 SQL、报表 SQL、临时排障 SQL,且业务方明确接受“自己维护完整 SQL”时,可使用
MetaFactory.sql(...) - 超复杂跨组过滤、递归 CTE、窗口函数、多结果集存储过程,继续保留 MQL / SQL Key / MyBatis