DynamicDataService
DynamicDataService is the host-managed service for reading and writing dynamic model tables (mt_<modelCode>). It enforces tenant scope, parameterizes every query (no string concatenation, no injection surface), applies soft-delete semantics, and emits ab_data_change_log rows. Plugins should call this service rather than raw JDBC or MyBatis mappers.
For list/grid queries reaching the front-end, the matching HTTP endpoint is GET /api/dynamic/{pageKey}/list — use the same parameter contract on both sides to avoid drift (red line §5).
Concept
Every model with autoPublishModels:true has a mt_<modelCode> table created by SchemaManagementServiceImpl. Each row carries a database IDENTITY auto-increment id (BIGINT, primary key) and a ULID pid (VARCHAR(32), NOT NULL UNIQUE) — id is generated by the database on insert, pid is populated by the platform's UniqueIdGenerator on insert. Multi-tenant data is scoped by tenant_id; the service injects the filter from MetaContext.
For background workers without an HTTP MetaContext, see BackgroundDataAccessor SPI.
Interface signature
package com.auraboot.framework.meta.service;
import com.auraboot.framework.meta.dto.*;
import java.util.List;
import java.util.Map;
public interface DynamicDataService {
/** Paginated list — used by /api/dynamic/{pageKey}/list. */
PaginationResult<Map<String, Object>> list(String modelCode, DynamicQueryRequest request);
/** Read a single row by recordId (pid); returns null when not found. */
Map<String, Object> getById(String modelCode, String recordId);
/** Create; returns the full created record (including system fields). */
Map<String, Object> create(String modelCode, Map<String, Object> data);
/** Update by recordId; returns the full updated record. */
Map<String, Object> update(String modelCode, String recordId, Map<String, Object> data);
/** Delete by recordId. */
void delete(String modelCode, String recordId);
/** Group / aggregate — used by DataProviderExtension and dashboards. */
Map<String, Object> aggregate(String modelCode, AggregateRequest aggregateRequest);
}Only the most commonly used methods are shown above. The full interface also includes
batchCreate/batchUpdate/batchDelete,executeCustomQuery,getStats, relation operations (getRelationData/createRelations/removeRelations),validate,getFieldOptions, import/export, andsaveWithRelations— seeDynamicDataService.javain the repo for the source of truth.
DynamicQueryRequest is the pagination DTO (@Data @Builder):
public class DynamicQueryRequest {
private Integer pageNum; // page number
private Integer pageSize; // page size
private List<QueryCondition> conditions; // query conditions
private List<SortField> sortFields; // sort fields (multi-field)
private String keyword; // search keyword
private Map<String, Object> extraParams; // extra params
private String viewId; // SavedView pid (optional)
private Long cursor; // keyset pagination cursor (optional)
}QueryCondition uses the Operator enum (no .eq()-style static factories); valid operators are: EQ / NE / GT / GE / LT / LE / LIKE / NOT_LIKE / IN / NOT_IN / IS_NULL / IS_NOT_NULL / BETWEEN / NOT_BETWEEN.
Implementation example
A scheduled rollup that aggregates monthly closed-won revenue per sales rep and writes into a summary model:
package com.acme.crm.rollup;
import com.auraboot.framework.meta.service.DynamicDataService;
import com.auraboot.framework.meta.dto.DynamicQueryRequest;
import com.auraboot.framework.meta.dto.QueryCondition;
import com.auraboot.framework.meta.dto.QueryCondition.Operator;
import com.auraboot.framework.meta.dto.PaginationResult;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Component
public class MonthlyRevenueRollup {
@Autowired private DynamicDataService data;
@Scheduled(cron = "0 30 0 1 * *") // 1st of month, 00:30
public void run() {
LocalDate firstOfPrev = LocalDate.now().minusMonths(1).withDayOfMonth(1);
LocalDate firstOfThis = firstOfPrev.plusMonths(1);
DynamicQueryRequest q = DynamicQueryRequest.builder()
.pageSize(500)
.conditions(List.of(
QueryCondition.builder().fieldName("status").operator(Operator.EQ).value("closed_won").build(),
QueryCondition.builder().fieldName("close_date").operator(Operator.GE).value(firstOfPrev).build(),
QueryCondition.builder().fieldName("close_date").operator(Operator.LT).value(firstOfThis).build()
))
.build();
PaginationResult<Map<String, Object>> res = data.list("crm_deal", q);
Map<Object, Double> byRep = res.getRecords().stream().collect(
groupingBy(r -> r.get("owner_id"),
summingDouble(r -> ((Number) r.get("amount")).doubleValue())));
byRep.forEach((rep, total) -> data.create("crm_revenue_rollup", Map.of(
"month", firstOfPrev,
"owner_id", rep,
"total", total
)));
}
}Registration
Host-managed singleton bean — @Autowired it. There is no SPI; this is a service interface only. The implementation lives in auraboot/platform (com.auraboot.framework.meta.service.impl.DynamicDataServiceImpl).
Common pitfalls
- Guessing parameter names. The front-end REST pagination (
GET /api/dynamic/{pageKey}/list) uses query paramspageNum/pageSize/filters/keyword/sortField/sortOrder(filtersis a JSON string ofQueryCondition[]); at the service layer these becomeDynamicQueryRequest'sconditions/sortFields. Verify againstDynamicController(red line §5). - Raw JDBC for "performance". Bypasses tenant filter, soft-delete, and change-log emission. Never do this — see red line §15 (e.g. the
Long.parseLong(ULID)incident). - Confusing
idandpid.idis a database IDENTITY auto-incrementBIGINT;pidis a ULIDString(VARCHAR(32)). TherecordIdargument to the service'sgetById/update/deleteis thepid.Long.parseLong(pid)crashes. Use the right type. - Writing inside a background worker without tenant context.
mt_<modelCode>'stenant_idisNOT NULL, but Kafka consumers / scheduled tasks run outside the request context with no implicit tenant on the thread — a missing tenant fails the write. UseBackgroundDataAccessor, which takes an explicittenantIdper call (e.g.create(long tenantId, String modelCode, Map<String,Object> data)); for idempotent inserts usetryCreate, which returnsOptional.empty()on a unique-constraint conflict.
Related
- REST API — HTTP equivalent
- BackgroundDataAccessor SPI
- MetadataRegistry service