Querydsl - Usage Maturity Model
01 Sep 2014www.querydsl.com
Level 4 - Delegates
Level 3 - Projections
Level 2 - Collections
Level 1 - Predicates
Level 0 - No usage (Swamp of POJO)
Predicates
Predicates aren’t the thing. They’re the thing that gets us to the thing.
Describes logical composable expressions about an entity that are separate from operators acting on the entity itself.
Predicates can be represented as Specifications, e.g. “isBonusAboveThreshold”, that describes an explicit constraint.
Before
boolean isBonusSalary = salaryDetail.getSalaryName().equalsIgnoreCase("Bonus");
boolean isGreaterThanThreshold = salaryDetail.getSalary().compareTo(payThreshold) >= 0;
boolean isBonusSalary && isGreaterThanThreshold;
After
import static QSalaryDetail;
BooleanExpression isBonusSalary = salaryDetail.salaryName.equalsIgnoreCase("Bonus");
BooleanExpression isGreaterThanThreshold = salaryDetail.salary.goe(payThreshold);
BooleanExpression isBonusAboveThreshold = isBonusSalary.and(isGreaterThanThreshold);
Types
com.mysema.query.types.expr
com.mysema.query.types.path
BooleanBuilder is a mutable predicate instance.
import static QSalaryDetail;
BooleanBuilder isRelevantSalaryName = new BooleanBuilder();
for (String salaryName : relevantSalaryNames) {
isRelevantSalaryName.or(salaryDetail.salaryName.eq(salaryName));
}
isRelevantSalaryName.and(salaryDetail.salary.gt(thresholdForPayPeriod));
CaseBuilder is the expression produced by a matching predicate or a default expression.
import static QSalaryDetail;
StringExpression caseSalaryName = new CaseBuilder()
.when(salaryDetail.isSalaryRelevant()
.and(salaryDetail.salary.goe(thresholdForPayPeriod)))
.then(salaryDetail.salaryName)
.otherwise("other");
Collections
CollQueryFactory
com.mysema.query.collections
Simply aggregate or ‘fold’ a collection. Even the Guava library doesn’t advocate higher-order functional programming to simplify this imperative Java.
Before
public BigDecimal sum(List<SalaryDetail> salaryDetails) {
BigDecimal sum = BigDecimal.ZERO;
for (SalaryDetail salaryDetail : salaryDetails) {
sum = sum.add(salaryDetail.getSalary());
}
return sum;
};
After
import static QSalaryDetail;
BigDecimal sum = CollQueryFactory
.from(salaryDetail, salaryDetails)
.singleResult(salaryDetail.salary.sum());
Replace this nested filter that maps an input collection of salaries to an output collection of their unique names.
Before
private List<String> uniqueSalaryNames(Collection<EmployeeSalary> employeeSalaries) {
Set<String> result = Sets.newHashSet();
for (EmployeeSalary salary : employeeSalaries) {
for (SalaryDetail detail : salary.getSalaryDetails()) {
if (RelevantSalaryUtil.isSalaryRelevant(detail.getSalaryName())) {
result.add(detail.getSalaryName());
}
}
}
return newArrayList(result);
}
After
import static QEmployeeSalary;
import static QSalaryDetail;
List<String> uniqueSalaryNames = CollQueryFactory
.from(employeeSalary, employeeSalaries)
.innerJoin(employeeSalary.salaryDetails, salaryDetail)
.where(salaryDetail.isSalaryRelevant())
.distinct()
.list(salaryDetail.salaryName);
ResultTransformer
com.mysema.query
A post-processor transformer for aggregation that works with com.mysema.query.group classes.
These examples take a collection of salaries and returns an aggregate collection where salaries with the same name will be grouped into a new projection containing the total salary of that group.
import static QSalaryDetail;
import static GroupBy;
Map<String, BigDecimal> aggregatedSalaries =
CollQueryFactory.from(salaryDetail, salaryDetails)
.transform(groupBy(caseSalaryName)
.as(sum(salaryDetail.salary)));
List<SalaryDetail> aggregatedSalaries = CollQueryFactory
.from(salaryDetail, salaryDetails)
.orderBy(salaryDetail.salaryName.asc())
.transform(groupBy(salaryDetail.salaryName)
.list(create(salaryDetail.salaryName,
sum(salaryDetail.salary))));
Projections
@QueryProjection
com.mysema.query.annotations
This can be used to select the columns for the View Model, within the JPA environment it can provide a detached model, or DTO layer.
import static QEmployeeSalary;
List<PresentableSalary> projection = CollQueryFactory
.from(employeeSalary, employeeSalaries)
.list(new QPresentableSalary(employeeSalary.employeeRef,
employeeSalary.payDate, employeeSalary.salaryDetails));
public class PresentableSalary implements Serializable {
private final Long employeeRef;
private final List<SalaryDetail> salaryDetails;
private final LocalDate payDate;
@QueryProjection
public PresentableSalary(Long employeeRef,
LocalDate payDate,
List<SalaryDetail> salaryDetails) {
this.employeeRef = employeeRef;
this.payDate = payDate;
this.salaryDetails = salaryDetails;
}
public List<SalaryDetail> salaryDetails() {
return salaryDetails;
}
public LocalDate getPayDate() {
return payDate;
}
public Long employeeRef() {
return this.employeeRef;
}
}
MappingProjection
Optionally use a template to support the construction of different projections from a resultset.
com.mysema.query.types
public class PresentableSalaryProjection extends MappingProjection<PresentableSalary> {
public PresentableSalaryProjection() {
super(PresentableSalary.class,
employee.employeeRef,
payroll.payDate,
salary.salaryDetails);
}
@Override
protected PresentableSalary map(Tuple row) {
return new PresentableSalary(
row.get(employee.employeeRef),
row.get(payroll.payDate),
row.get(salary.salaryDetails));
}
}
The @QueryProjection can also be placed on the Entity constructor itself and, in this example, is generated as the method QSalaryDetail.create().
@QueryProjection
public SalaryDetail(String salaryName, BigDecimal salary) {
this.salaryName = salaryName;
this.salary = salary;
}
Delegates
@QueryDelegate
com.mysema.query.annotations
Making your own DSL. Add the business concepts into the query model, these could be temporal, flags, comparisons.
Instead of static’helper’ methods to create business logic constraints, consider using annotated delegate methods to provide query extensions.
The delegate method is exposed directly in the query and expresses the intent of the constraint explicitly.
from...where(QSalaryDetail.salaryDetail.isSalaryRelevant())
Replace the ‘static utility’ below with a Query Delegate.
Before
public class RelevantSalaryUtil {
public static final String NON_RELEVANT_SALARY = "other";
public static boolean isSalaryRelevant(String salaryName) {
return !NON_RELEVANT_SALARY.equals(salaryName);
}
}
After
@QueryDelegate(SalaryDetail.class)
public static BooleanExpression isSalaryRelevant(QSalaryDetail detail) {
return detail.salaryName.notEqualsIgnoreCase("other");
}