Fluent DSL Guide
This page explains how backend Java services use the Geelato Framework Fluent DSL through MetaFactory.
Sample projects:
- Repository: geelato-hello-example
- ORM integration sample: geelato-sample-orm
Goal
- provide a Java-style metadata CRUD entry for backend developers
- avoid direct MQL JSON construction while still reusing the existing
MetaQLManager + SqlManager + Daoexecution kernel
When to Use It
- when backend services need metadata query, insert, update, or delete by entity name or entity class
- when the service wants to reuse built-in capabilities such as dynamic datasource switching, view parameters, and
$ctx/$fn/$parentvalue references
When Not to Use It
- when frontend pages already go through the platform data API based on
MetaController + MQL - when an existing service already works well with
BaseService + entity classand does not need metadata-driven unification
Entry Points
By entity name:
List<Map<String, Object>> users = MetaFactory.query("User")
.select(new String[]{"id", "name", "mobilePhone"})
.where(Filter.eq("delStatus", 0))
.order(Order.desc("updateAt"))
.list();
By entity class:
List<Map<String, Object>> users = MetaFactory.query(User.class)
.select(new String[]{"id", "name"})
.page(1, 20)
.list();
Query Examples
Single row:
Map<String, Object> user = MetaFactory.query("User")
.where(Filter.eq("id", "1912345678901234567"))
.one();
Paged query:
PageResult<Map<String, Object>> page = MetaFactory.query("User")
.select(new String[]{"id", "name", "updateAt"})
.where(Filter.like("name", "zhang"))
.order(Order.desc("updateAt"))
.page(1, 10)
.page();
Single-column query:
List<String> ids = MetaFactory.query("User")
.select(new String[]{"id"})
.where(Filter.eq("delStatus", 0))
.oneColumn(String.class);
Result wrapping:
List<String> names = MetaFactory.query("User")
.select(new String[]{"name"})
.wrapperResult(row -> String.valueOf(row.get("name")))
.list();
Write Examples
Insert:
String userId = MetaFactory.insert("User")
.value("name", "test user")
.value("mobilePhone", "13800000000")
.save();
Default fields are filled before save:
- inserts align with the current MQL rules and fill fields such as
createAt,creator,creatorName,tenantCode,buId,deptId,updateAt,updater,updaterName, anddeleteAt - updates fill
updateAt,updater, andupdaterName - the behavior comes from the built-in default filler and can be customized by overriding
SaveDefaultValueFiller
Update:
String userId = MetaFactory.update("User")
.value("id", "1912345678901234567")
.value("name", "new name")
.save();
Delete:
int affected = MetaFactory.delete("User")
.where(Filter.eq("id", "1912345678901234567"))
.delete();
Advanced Capabilities
Referenced field join:
List<Map<String, Object>> orders = MetaFactory.query("Order")
.select(new String[]{"id", "code"})
.selectRef("userId->name", "userName")
.list();
Custom join:
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();
Dynamic datasource:
List<Map<String, Object>> rows = MetaFactory.query("DevDbConnect")
.useDataSource("portal")
.page(1, 10)
.list();
View parameters:
List<Map<String, Object>> rows = MetaFactory.query("SomeViewEntity")
.viewParams(Map.of("customerId", "C001"))
.page(1, 10)
.list();
Context and function value references:
String id = MetaFactory.insert("Notice")
.value("creator", ValueRefs.ctx("userId"))
.value("createAt", ValueRefs.fnNowDateTime())
.save();
MySQL procedure:
List<Map<String, Object>> rows = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.useDataSource("portal")
.list();
Native SQL passthrough:
List<Map<String, Object>> rows = MetaFactory.sql("select id, name from platform_user where del_status = ?")
.param(0)
.useDataSource("portal")
.list();
Parent-child nested save:
String parentId = MetaFactory.insert("App")
.value("name", "demo-app")
.child("AppVersion", child -> child
.value("appId", ValueRefs.parent("id"))
.value("code", "v1"))
.save();
Join Notes
selectRef(...)fits the case where the main entity already has foreign-key metadata- the framework can derive the
left joinautomatically, so the service code does not need to write the join condition - if you omit an explicit alias in
selectRef(...), the result column uses the remote field name by default
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();
- custom join fits cases without foreign-key metadata or cases that need explicit join control
- use a stable alias convention, such as
.as("o")for the main table and short aliases such asuordfor joined tables - keep aliases consistent across
selectExpr(...),groupBy(...), andhavingSql(...)
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();
Procedure Notes
- current procedure support is intentionally lightweight and mainly targets MySQL-style
INparameters plus one result set - parameters are added in call order through
.in(name, value) - the
nameis mainly for readability; runtime binding is positional
Map<String, Object> row = MetaFactory.procedure("proc_query_user_orders")
.in("userId", "U1001")
.in("status", 1)
.one();
Object wrapping still works:
List<OrderSimpleDto> rows = 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();
Native SQL Notes
- use
MetaFactory.sql(...)when the service already owns the final SQL and only wants to reuse the execution chain - this path provides datasource switching, parameter binding, and terminal execution methods
- it does not add metadata-aware joins, default audit fields, or expression conversion
Map<String, Object> row = MetaFactory.sql("select id, name from platform_user where id = ?")
.param("U1001")
.one();
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();
Debugging Tips
- use
toSql()to inspect the generated SQL or procedure call - use
toCountSql()when checking pagination count issues - for join issues, verify aliases and foreign-key metadata first
- for procedure issues, verify parameter order, datasource choice, and result-set shape first
- for native SQL, verify the SQL itself, placeholder count, and terminal method choice first
Recommended Boundary
- use Fluent DSL first for standard single-table CRUD, light references, and lightweight aggregation
- use
selectRef(...)first when foreign-key metadata already exists - use explicit join only when the metadata path is not enough
- use
MetaFactory.sql(...)when the team intentionally owns the SQL text - keep MQL, SQL Key, or MyBatis for very complex filtering, recursive SQL, window functions, or multi-result-set procedures