Code-Level Comparison Across Java Data Access Tools
Overview
This page compares TeaQL with common Java data-access approaches at the code level.
The comparison focuses on real development scenarios:
- Single-table loading
- Filtering
- Pagination
- Sorting
- Multi-level graph loading
- Nested query customization
- DDD-style return types
- Statistics and aggregation
- Updating data
- Safe expressions
- Technical customization through
UserContext
The tools compared are:
- TeaQL
- QueryDSL
- Spring Data JPA
- easy-query
- MyBatis-Plus
- Fluent MyBatis
- MyBatis-Flex
- EntityQueryable
- FastORM
The examples use an order model:
Order
-> Customer
-> OrderLine[]
-> Product
-> Shipment
-> Payment
The code snippets are intentionally compact. Exact APIs may differ by version, project conventions, and integration style. The goal is to compare the shape of code that teams usually write.
1. Single-Table Data Loading
TeaQL
Order order = Q.orders()
.filterById(orderId)
.execute(userContext);
QueryDSL
QOrder order = QOrder.order;
Order result = queryFactory
.selectFrom(order)
.where(order.id.eq(orderId))
.fetchOne();
Spring Data JPA
Order order = orderRepository
.findById(orderId)
.orElse(null);
easy-query
Order order = easyEntityQuery
.queryable(Order.class)
.where(o -> o.id().eq(orderId))
.singleOrNull();
MyBatis-Plus
Order order = orderMapper.selectById(orderId);
Fluent MyBatis
OrderEntity order = orderMapper.findOne(
new OrderQuery()
.where.id().eq(orderId)
.end());
MyBatis-Flex
Order order = orderMapper.selectOneById(orderId);
EntityQueryable
Order order = entityQueryable
.from(Order.class)
.where(o -> o.getId().equals(orderId))
.firstOrNull();
FastORM
Order order = fastOrm
.find(Order.class, orderId);
Comparison
For simple loading, most tools are concise.
TeaQL's difference appears when the same request starts growing into domain graph loading, permission-aware execution, validation, audit, locale, cache, routing, and reusable business query fragments.
2. Filtering
TeaQL
SmartList<Order> orders = Q.orders()
.filterByMerchant(userContext.getMerchant())
.whichStatusIs(OrderStatus.SHIPPED)
.whichTotalAmountGreaterThan(BigDecimal.valueOf(100))
.executeForList(userContext);
QueryDSL
QOrder order = QOrder.order;
List<Order> orders = queryFactory
.selectFrom(order)
.where(
order.merchant.eq(userContext.getMerchant()),
order.status.eq(OrderStatus.SHIPPED),
order.totalAmount.gt(BigDecimal.valueOf(100)))
.fetch();
Spring Data JPA
List<Order> orders =
orderRepository.findByMerchantAndStatusAndTotalAmountGreaterThan(
userContext.getMerchant(),
OrderStatus.SHIPPED,
BigDecimal.valueOf(100));
For more dynamic cases:
Specification<Order> spec = (root, query, cb) -> cb.and(
cb.equal(root.get("merchant"), userContext.getMerchant()),
cb.equal(root.get("status"), OrderStatus.SHIPPED),
cb.greaterThan(root.get("totalAmount"), BigDecimal.valueOf(100))
);
List<Order> orders = orderRepository.findAll(spec);
MyBatis-Plus
List<Order> orders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.eq(Order::getMerchantId, userContext.getMerchant().getId())
.eq(Order::getStatus, OrderStatus.SHIPPED)
.gt(Order::getTotalAmount, BigDecimal.valueOf(100)));
MyBatis-Flex
QueryWrapper query = QueryWrapper.create()
.where(ORDER.MERCHANT_ID.eq(userContext.getMerchant().getId()))
.and(ORDER.STATUS.eq(OrderStatus.SHIPPED))
.and(ORDER.TOTAL_AMOUNT.gt(BigDecimal.valueOf(100)));
List<Order> orders = orderMapper.selectListByQuery(query);
Comparison
TeaQL generated methods are domain-linguistic:
whichStatusIs(...)
whichTotalAmountGreaterThan(...)
filterByMerchant(...)
This is useful for teams using AI assistance because the code itself carries business meaning and requires less prompt context.
3. Pagination
TeaQL
SmartList<Order> page = Q.orders()
.filterByMerchant(userContext.getMerchant())
.orderByCreateTimeDescending()
.top(20)
.executeForList(userContext);
Spring Data JPA
Page<Order> page = orderRepository.findByMerchant(
userContext.getMerchant(),
PageRequest.of(0, 20, Sort.by("createTime").descending()));
QueryDSL
List<Order> page = queryFactory
.selectFrom(order)
.where(order.merchant.eq(userContext.getMerchant()))
.orderBy(order.createTime.desc())
.offset(0)
.limit(20)
.fetch();
MyBatis-Plus
Page<Order> page = orderMapper.selectPage(
Page.of(1, 20),
Wrappers.<Order>lambdaQuery()
.eq(Order::getMerchantId, userContext.getMerchant().getId())
.orderByDesc(Order::getCreateTime));
Comparison
Pagination itself is common. TeaQL keeps it in the same generated request chain used for graph loading and domain filtering.
4. Sorting
TeaQL
SmartList<Order> orders = Q.orders()
.filterByMerchant(userContext.getMerchant())
.orderByCreateTimeDescending()
.orderByCodeAscending()
.executeForList(userContext);
Locale-aware sorting can also be expressed as generated domain methods:
SmartList<Customer> customers = Q.customers()
.orderByNameAscendingUsingGBK()
.executeForList(userContext);
QueryDSL
List<Order> orders = queryFactory
.selectFrom(order)
.where(order.merchant.eq(userContext.getMerchant()))
.orderBy(order.createTime.desc(), order.code.asc())
.fetch();
Spring Data JPA
List<Order> orders = orderRepository.findByMerchant(
userContext.getMerchant(),
Sort.by(
Sort.Order.desc("createTime"),
Sort.Order.asc("code")));
Comparison
TeaQL sorting is generated per field and can include locale-specific variants. That keeps language and collation behavior close to domain API usage.
5. Multi-Level Data Loading
TeaQL
Order order = Q.orders()
.filterById(orderId)
.selectCustomer(Q.customers().selectName())
.selectOrderLineList(
Q.orderLines()
.selectProduct(Q.products().selectSku().selectName())
.orderByDisplayOrderAscending())
.selectShipment()
.selectPayment()
.execute(userContext);
QueryDSL
Order order = queryFactory
.selectFrom(qOrder)
.leftJoin(qOrder.customer).fetchJoin()
.leftJoin(qOrder.shipment).fetchJoin()
.leftJoin(qOrder.payment).fetchJoin()
.where(qOrder.id.eq(orderId))
.fetchOne();
List<OrderLine> lines = queryFactory
.selectFrom(qOrderLine)
.leftJoin(qOrderLine.product).fetchJoin()
.where(qOrderLine.order.id.eq(orderId))
.orderBy(qOrderLine.displayOrder.asc())
.fetch();
Spring Data JPA
@EntityGraph(attributePaths = {
"customer",
"shipment",
"payment",
"orderLineList",
"orderLineList.product"
})
Optional<Order> findById(Long id);
Or custom JPQL:
@Query("""
select distinct o
from Order o
left join fetch o.customer
left join fetch o.shipment
left join fetch o.payment
left join fetch o.orderLineList l
left join fetch l.product
where o.id = :id
""")
Order loadOrderGraph(Long id);
MyBatis-Plus / MyBatis-Flex / Fluent MyBatis
In MyBatis-style tools, teams commonly combine mapper queries, result maps, manual composition, or service-level loading:
Order order = orderMapper.selectById(orderId);
Customer customer = customerMapper.selectById(order.getCustomerId());
List<OrderLine> lines = orderLineMapper.selectByOrderId(orderId);
Shipment shipment = shipmentMapper.selectByOrderId(orderId);
Payment payment = paymentMapper.selectByOrderId(orderId);
order.setCustomer(customer);
order.setOrderLineList(lines);
order.setShipment(shipment);
order.setPayment(payment);
Comparison
TeaQL makes graph shape part of the request:
selectCustomer(...)
selectOrderLineList(...)
selectProduct(...)
This is different from tools where graph loading is spread across fetch joins, entity graphs, result maps, or service composition.
6. Nested Query Customization
TeaQL
Order order = Q.orders()
.filterById(orderId)
.selectOrderLineList(
Q.orderLines()
.whichQuantityGreaterThan(BigDecimal.ZERO)
.selectProduct(Q.products().selectName())
.orderByDisplayOrderAscending()
.top(100))
.execute(userContext);
QueryDSL
Nested collection customization usually requires additional queries or projections:
Order order = queryFactory
.selectFrom(qOrder)
.where(qOrder.id.eq(orderId))
.fetchOne();
List<OrderLine> lines = queryFactory
.selectFrom(qOrderLine)
.leftJoin(qOrderLine.product).fetchJoin()
.where(
qOrderLine.order.id.eq(orderId),
qOrderLine.quantity.gt(BigDecimal.ZERO))
.orderBy(qOrderLine.displayOrder.asc())
.limit(100)
.fetch();
order.setOrderLineList(lines);
Spring Data JPA
EntityGraph can load relationships, but filtering and ordering nested collections commonly moves into JPQL, specifications, or separate repository methods.
@Query("""
select l
from OrderLine l
join fetch l.product
where l.order.id = :orderId
and l.quantity > 0
order by l.displayOrder asc
""")
List<OrderLine> loadPositiveLines(Long orderId);
Comparison
TeaQL nested selectors accept another TeaQL request, so child collection shape is customized in the same chain as the parent request.
7. Polymorphic Child Enhancement
TeaQL
Q.orderReports()
.tryEnhanceChildren()
.enhanceChild(
Q.paymentReports()
.selectPayment()
.selectPaymentItemList())
.enhanceChild(
Q.shipmentReports()
.selectShipment()
.selectTrackingEventList())
.executeForList(userContext);
Typical ORM / Mapper Approach
Many data-access tools can represent inheritance or discriminator columns, but deep subtype-specific child loading is usually handled with:
- ORM inheritance mapping
- Separate subtype repositories
- Mapper result maps
- Manual service composition
- Post-load enrichment
Example service composition:
List<OrderReport> reports = orderReportRepository.findByOrderId(orderId);
for (OrderReport report : reports) {
if (report instanceof PaymentReport paymentReport) {
paymentReport.setPayment(paymentRepository.findByReportId(report.getId()));
paymentReport.setPaymentItemList(paymentItemRepository.findByReportId(report.getId()));
}
if (report instanceof ShipmentReport shipmentReport) {
shipmentReport.setShipment(shipmentRepository.findByReportId(report.getId()));
shipmentReport.setTrackingEventList(eventRepository.findByReportId(report.getId()));
}
}
Comparison
TeaQL makes subtype enhancement explicit in the request. This is important for report-like models where base records need subtype-specific fields and nested lists.
8. DDD Return Types
TeaQL supports dynamic return type selection through generated requests.
TeaQL
OrderSummary summary = Q.orders()
.filterById(orderId)
.selectCustomer(Q.customers().selectName())
.selectOrderLineList(Q.orderLines().selectProduct())
.returnType(OrderSummary.class)
.execute(userContext);
Another use case is returning an aggregate-root subclass with domain behavior:
FulfillmentOrder order = Q.orders()
.filterById(orderId)
.selectShipment()
.selectPayment()
.returnType(FulfillmentOrder.class)
.execute(userContext);
order.confirmShipment(userContext);
Spring Data JPA
DTO projection:
public interface OrderSummaryView {
Long getId();
String getCode();
BigDecimal getTotalAmount();
}
OrderSummaryView findProjectedById(Long id);
Class projection:
@Query("""
select new com.example.OrderSummary(o.id, o.code, o.totalAmount)
from Order o
where o.id = :id
""")
OrderSummary loadSummary(Long id);
QueryDSL
OrderSummary summary = queryFactory
.select(Projections.constructor(
OrderSummary.class,
qOrder.id,
qOrder.code,
qOrder.totalAmount))
.from(qOrder)
.where(qOrder.id.eq(orderId))
.fetchOne();
MyBatis-Style Tools
OrderSummary summary = orderMapper.selectOrderSummary(orderId);
The mapper SQL or XML usually defines the exact return shape.
Comparison
TeaQL's returnType(Class) keeps DDD-oriented return selection in the domain request chain. Teams can return richer aggregate subclasses or read-model classes without moving the query intent into SQL strings or separate mapper definitions.
9. Statistics and Aggregation
TeaQL
int shippedCount = userContext
.aggregation(
Q.orders()
.filterByMerchant(userContext.getMerchant())
.whichStatusIs(OrderStatus.SHIPPED)
.count())
.toInt();
Group by:
SmartList<Order> statusSummary = Q.orders()
.filterByMerchant(userContext.getMerchant())
.count()
.groupByStatus()
.executeForList(userContext);
Sum:
BigDecimal totalAmount = userContext
.aggregation(
Q.orders()
.filterByMerchant(userContext.getMerchant())
.whichCreateTimeBetween(start, end)
.sumTotalAmount())
.toBigDecimal();
QueryDSL
Long shippedCount = queryFactory
.select(qOrder.id.count())
.from(qOrder)
.where(
qOrder.merchant.eq(userContext.getMerchant()),
qOrder.status.eq(OrderStatus.SHIPPED))
.fetchOne();
Group by:
List<Tuple> rows = queryFactory
.select(qOrder.status, qOrder.id.count())
.from(qOrder)
.where(qOrder.merchant.eq(userContext.getMerchant()))
.groupBy(qOrder.status)
.fetch();
Spring Data JPA
@Query("""
select count(o)
from Order o
where o.merchant = :merchant
and o.status = :status
""")
long countByMerchantAndStatus(Merchant merchant, OrderStatus status);
Group by:
@Query("""
select o.status, count(o)
from Order o
where o.merchant = :merchant
group by o.status
""")
List<Object[]> countByStatus(Merchant merchant);
MyBatis-Style Tools
Long shippedCount = orderMapper.countByMerchantAndStatus(
userContext.getMerchant().getId(),
OrderStatus.SHIPPED);
The SQL is usually written in mapper XML, annotation SQL, or wrapper APIs.
Comparison
TeaQL keeps statistics in the same generated request style as entity loading. This makes it easier to reuse filters and business query fragments.
10. Updating Data
TeaQL
Order order = Q.orders()
.filterById(orderId)
.execute(userContext);
order.updateStatus(OrderStatus.SHIPPED)
.updateShipTime(LocalDateTime.now())
.save(userContext);
Saving an object graph:
Order order = new Order()
.updateCustomer(customer)
.updateStatus(OrderStatus.NEW)
.updateTotalAmount(totalAmount);
order.appendOrderLineList(lines);
order.save(userContext);
Spring Data JPA
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
orderRepository.save(order);
MyBatis-Plus
Order order = orderMapper.selectById(orderId);
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
orderMapper.updateById(order);
MyBatis-Flex
Order order = orderMapper.selectOneById(orderId);
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
orderMapper.update(order);
Comparison
TeaQL's save(userContext) gives the runtime a chance to apply:
- Validation
- ID generation
- Version handling
- Audit trail
- Permission checks
- Cache invalidation
- Read/write routing
The update is not just persistence. It is execution through the project context.
11. Safe Expressions
TeaQL
BigDecimal maxPressure = E.order(order)
.getTrailer()
.getLiquidContainerList()
.first()
.getMaximumWorkingPressure()
.eval();
Another example:
String customerName = E.order(order)
.getCustomer()
.getName()
.eval();
Java Optional
BigDecimal maxPressure = Optional.ofNullable(order)
.map(Order::getTrailer)
.map(Truck::getLiquidContainerList)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(LiquidContainer::getMaximumWorkingPressure)
.orElse(null);
Plain Java Null Checks
BigDecimal maxPressure = null;
if (order != null && order.getTrailer() != null) {
List<LiquidContainer> containers = order.getTrailer().getLiquidContainerList();
if (containers != null && !containers.isEmpty()) {
maxPressure = containers.get(0).getMaximumWorkingPressure();
}
}
Comparison
TeaQL E expressions make deep navigation readable and consistent with Q query expressions. This is useful when creating commands, reports, DTOs, and validation logic from loaded graphs.
12. Technical Customization Through UserContext
TeaQL
public class CustomUserContext extends UserContext {
public String currentTenantCode() {
return currentUser().getTenantCode();
}
public boolean canReadOrder(Order order) {
return permissionService().canReadOrder(currentUser(), order);
}
public String nextOrderNumber() {
return commonIdGenerator().nextCommonId("Order", this);
}
public void withOrderLock(String orderId, Runnable task) {
distributedLockService()
.executeWithLock(currentTenantCode() + ":order:" + orderId, task);
}
public DataSourceKey readDataSource(String entityType) {
return routingPolicy().routeRead(this, entityType);
}
}
Service code stays business-focused:
public void shipOrder(CustomUserContext userContext, String orderId) {
userContext.withOrderLock(orderId, () -> {
Order order = Q.orders()
.filterById(orderId)
.selectOrderLineList()
.execute(userContext);
if (!userContext.canReadOrder(order)) {
throw new AccessDeniedException();
}
order.ship();
order.save(userContext);
});
}
Other Tools
Most other tools leave these concerns to surrounding application layers:
Controller
-> Service
-> Permission service
-> Transaction manager
-> Repository / Mapper / Query DSL
-> Cache service
-> Audit service
-> Lock service
This is flexible, but teams must design and enforce the integration pattern themselves.
Comparison
TeaQL intentionally makes UserContext the runtime boundary. That gives one consistent place for:
- Current user
- Tenant
- Permission checks
- ID generation
- Cache
- Audit trail
- Distributed locking
- Read/write splitting
- i18n
- Logging
- Request and response metadata
Summary Table
| Scenario | TeaQL Advantage |
|---|---|
| Single-table load | Similar simplicity, with context-aware execution |
| Filtering | Generated domain methods read like business language |
| Pagination | Same request chain as graph loading |
| Sorting | Generated per-field and locale-aware variants |
| Multi-level load | Nested select...(...) and select...List(...) |
| Nested customization | Child request can be customized inline |
| Polymorphism | tryEnhanceChildren() and enhanceChild(...) |
| DDD return type | Q.objects().returnType(Class) dynamic return |
| Statistics | Aggregation uses the same request language |
| Update | save(userContext) applies runtime policies |
| Safe expression | E expressions reduce null-check noise |
| Technical customization | UserContext centralizes cross-cutting behavior |
Practical Conclusion
Many Java data-access tools are excellent at querying tables or mapping objects.
TeaQL is different because it combines:
- Generated domain-language APIs
- Q expressions for query
- E expressions for safe navigation
- Entity save semantics
- Nested graph loading
- Polymorphic enhancement
- DDD-style return types
- UserContext-driven runtime customization
This makes TeaQL especially useful for complex domain systems where code readability, AI-assisted development, team collaboration, and runtime customization matter as much as raw data access.