How Method Names Become Queries
A Mini Method-Name Parser
Example
At startup Spring Data splits each method name into a subject (findBy, countBy), property conditions and an optional OrderBy, then builds JPQL. This simplified parser shows the idea:
import java.util.*;
public class Main {
public static void main(String[] args) {
String[] methods = {
"findBySku", "findByCategoryAndPriceLessThan", "findByNameContainingIgnoreCase",
"findByPriceBetweenOrderByPriceAsc", "findByStockGreaterThanOrderByCreatedAtDesc",
"countByCategory", "findByDescriptionIsNull" };
for (String m : methods) {
System.out.println(m);
System.out.println(" -> " + DerivedQuery.toJpql("Product", m));
}
}
}
class DerivedQuery {
static final String[][] OPS = {
{ "LessThan", "<" }, { "GreaterThan", ">" }, { "Between", "between" },
{ "Containing", "like" }, { "IsNull", "is null" } };
static String toJpql(String entity, String method) {
String select, rest;
if (method.startsWith("findBy")) { select = "select p from " + entity + " p"; rest = method.substring(6); }
else if (method.startsWith("countBy")) { select = "select count(p) from " + entity + " p"; rest = method.substring(7); }
else throw new IllegalArgumentException("Unsupported prefix: " + method);
String order = "";
int ob = rest.indexOf("OrderBy");
if (ob >= 0) {
String o = rest.substring(ob + 7);
rest = rest.substring(0, ob);
boolean desc = o.endsWith("Desc");
String prop = o.endsWith("Desc") || o.endsWith("Asc") ? o.substring(0, o.length() - (desc ? 4 : 3)) : o;
order = " order by p." + lower(prop) + (desc ? " desc" : " asc");
}
int[] param = { 1 };
List<String> conditions = new ArrayList<>();
for (String part : rest.split("And(?=[A-Z])")) conditions.add(condition(part, param));
return select + " where " + String.join(" and ", conditions) + order;
}
static String condition(String part, int[] n) {
boolean ignoreCase = part.endsWith("IgnoreCase");
if (ignoreCase) part = part.substring(0, part.length() - "IgnoreCase".length());
for (String[] op : OPS) {
if (!part.endsWith(op[0])) continue;
String prop = wrap("p." + lower(part.substring(0, part.length() - op[0].length())), ignoreCase);
switch (op[0]) {
case "Between": n[0] += 2; return prop + " between ?" + (n[0] - 2) + " and ?" + (n[0] - 1);
case "IsNull": return prop + " is null";
case "Containing": return prop + " like " + wrap("?" + n[0]++, ignoreCase) + " /* %value% */";
default: return prop + " " + op[1] + " ?" + n[0]++;
}
}
return wrap("p." + lower(part), ignoreCase) + " = " + wrap("?" + n[0]++, ignoreCase);
}
static String wrap(String s, boolean upper) { return upper ? "upper(" + s + ")" : s; }
static String lower(String s) { return Character.toLowerCase(s.charAt(0)) + s.substring(1); }
}Output
findBySku -> select p from Product p where p.sku = ?1 findByCategoryAndPriceLessThan -> select p from Product p where p.category = ?1 and p.price < ?2 findByNameContainingIgnoreCase -> select p from Product p where upper(p.name) like upper(?1) /* %value% */ findByPriceBetweenOrderByPriceAsc -> select p from Product p where p.price between ?1 and ?2 order by p.price asc findByStockGreaterThanOrderByCreatedAtDesc -> select p from Product p where p.stock > ?1 order by p.createdAt desc countByCategory -> select count(p) from Product p where p.category = ?1 findByDescriptionIsNull -> select p from Product p where p.description is null
The real parser (PartTree in Spring Data) supports many more keywords, nested properties (findByCategoryName on a relationship), and validates every property against the entity at startup.
Exercise
Exercise: Write derived query methods for an OrderRepository (entity Order with Long userId, OrderStatus status, BigDecimal totalAmount, LocalDateTime placedAt): (1) all orders of a user, newest first; (2) orders with a given status placed between two dates; (3) the number of orders of a user with a given status; (4) whether a user has any order with total above an amount.
Show answer
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserIdOrderByPlacedAtDesc(Long userId);
List<Order> findByStatusAndPlacedAtBetween(OrderStatus status,
LocalDateTime from, LocalDateTime to);
long countByUserIdAndStatus(Long userId, OrderStatus status);
boolean existsByUserIdAndTotalAmountGreaterThan(Long userId, BigDecimal amount);
}