跳到主要内容

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。

对应示例工程:

依赖

最小只需要引入:

<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(dynamicDataSourcedynamicJdbcTemplatedynamicDao),供框架内动态数据源链路使用。

快速开始(从 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_addressemailAddress
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
  • 这些规则已经改为通过 FluentSaveFieldValueFiller SPI 注入;平台默认实现位于 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/rightJoinon 当前推荐优先使用 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() 用于查看当前查询或存储过程最终生成的 SQL
  • toCountSql() 只适用于分页查询,常用于排查分页总数不准确的问题
  • 排查 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

推荐继续阅读