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);
}