Skip to main content

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)
.comment("Query order")
.purpose("Load order details")
.executeForOne(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())
.withStatusIs(OrderStatus.SHIPPED)
.withTotalAmountGreaterThan(BigDecimal.valueOf(100))
.comment("Query orders").purpose("Load data")
.executeForList(userContext);

The explicit merchant predicate keeps this comparison's business query shape equivalent across frameworks. In a multi-tenant TeaQL application it only narrows the operation; mandatory tenant and permission enforcement belongs to runtime policy installed in trusted 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:

withStatusIs(...)
withTotalAmountGreaterThan(...)
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)
.comment("Query orders").purpose("Load data")
.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()
.comment("Query orders").purpose("Load data")
.executeForList(userContext);

Locale-aware sorting can also be expressed as generated domain methods:

SmartList<Customer> customers = Q.customers().orderByNameAscendingUsingGBK()
.comment("Query customers").purpose("Load data")
.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()
.comment("Query orders").purpose("Load data")
.executeForOne(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()
.withQuantityGreaterThan(BigDecimal.ZERO)
.selectProduct(Q.products().selectName())
.orderByDisplayOrderAscending()
.top(100))
.comment("Query orders").purpose("Load data")
.executeForOne(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())
.comment("Query orders").purpose("Load data")
.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)
.comment("Query orders").purpose("Load data")
.executeForOne(userContext);

Another use case is returning an aggregate-root subclass with domain behavior:

FulfillmentOrder order = Q.orders()
.filterById(orderId)
.selectShipment()
.selectPayment()
.returnType(FulfillmentOrder.class)
.comment("Query orders").purpose("Load data")
.executeForOne(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())
.withStatusIs(OrderStatus.SHIPPED)
.count())
.toInt();

Group by:

SmartList<Order> statusSummary = Q.orders()
.filterByMerchant(userContext.getMerchant())
.count()
.groupByStatus()
.comment("Query orders").purpose("Load data")
.executeForList(userContext);

Sum:

BigDecimal totalAmount = userContext
.aggregation(
Q.orders()
.filterByMerchant(userContext.getMerchant())
.withCreateTimeBetween(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).comment("Query orders").purpose("Load data")
.executeForOne(userContext);

order.updateStatus(OrderStatus.SHIPPED)
.updateShipTime(LocalDateTime.now())
.auditAs("Ship customer order")
.save(userContext);

Saving an object graph:

Order order = new Order()
.updateCustomer(customer)
.updateStatus(OrderStatus.NEW)
.updateTotalAmount(totalAmount);

order.appendOrderLineList(lines);
order.auditAs("Save 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()
.comment("Query orders").purpose("Load data")
.executeForOne(userContext);

if (!userContext.canReadOrder(order)) {
throw new AccessDeniedException();
}

order.ship();
order.auditAs("Save 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​

ScenarioTeaQL Advantage
Single-table loadSimilar simplicity, with context-aware execution
FilteringGenerated domain methods read like business language
PaginationSame request chain as graph loading
SortingGenerated per-field and locale-aware variants
Multi-level loadNested select...(...) and select...List(...)
Nested customizationChild request can be customized inline
PolymorphismtryEnhanceChildren() and enhanceChild(...)
DDD return typeQ.objects().returnType(Class).comment("Query objects").purpose("Load data") dynamic return
StatisticsAggregation uses the same request language
Updatesave(userContext) applies runtime policies
Safe expressionE expressions reduce null-check noise
Technical customizationUserContext 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.