DEV Community

Troy
Troy

Posted on

REST-style GraphQL β€” one line of Java handles filtering + sorting + pagination + stats + CSV export.

(Demo's in Chinese but you'll get the idea in 30 seconds) πŸ‘‡
https://demo-bs.zhxu.cn/

A backend engineer's confession: you just wrote 100 lines of Java code to do exactly one thing β€” glue a few HTTP parameters into a SQL string.


One Page of Requirements

My PM sent me a mockup. Pretty standard admin panel stuff:

  • Paginated table
  • Four filter fields at the top: name, age range, department, hire date
  • Sort by any column
  • Footer stats: total count, average age

"Easy, right? Can we ship this afternoon?" he asked.

"Sure," I said.

Then I opened IntelliJ.


If You Use MyBatis, Here's What the Next 30 Minutes Looks Like

<select id="searchUsers" resultType="UserVO">
  SELECT u.*, d.name as deptName
  FROM user u LEFT JOIN dept d ON u.dept_id = d.id
  <where>
    <if test="name != null and name != ''">
      AND u.name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="ageMin != null">
      AND u.age >= #{ageMin}
    </if>
    <if test="ageMax != null">
      AND u.age &lt;= #{ageMax}
    </if>
    <if test="dept != null and dept != ''">
      AND d.name = #{dept}
    </if>
    <if test="entryDateStart != null">
      AND u.entry_date >= #{entryDateStart}
    </if>
    <if test="entryDateEnd != null">
      AND u.entry_date &lt;= #{entryDateEnd}
    </if>
  </where>
  <if test="sortField != null and sortOrder != null">
    ORDER BY ${sortField} ${sortOrder}
  </if>
  LIMIT #{offset}, #{size}
</select>
Enter fullscreen mode Exit fullscreen mode

And you're not done. You still need: a stats SQL, a Controller to parse parameters, a Service layer for pagination logic, a Mapper interface, a VO class just to hold the results β€” plus careful ${} sanitization to prevent SQL injection.

Four filter conditions. Almost 100 lines of code.

Then the PM says: "Oh, by the way β€” make department multi-select. And add a gender filter."

You take a deep breath and keep writing.

This isn't a MyBatis problem. Try JPA β€” you won't suffer any less:

Specification<User> spec = (root, query, cb) -> {
  List<Predicate> predicates = new ArrayList<>();
  Join<User, Department> deptJoin = root.join("dept", JoinType.LEFT);

  if (StringUtils.isNotBlank(name)) {
    predicates.add(cb.like(root.get("name"), "%" + name + "%"));
  }
  if (ageMin != null) predicates.add(cb.ge(root.get("age"), ageMin));
  if (ageMax != null) predicates.add(cb.le(root.get("age"), ageMax));
  if (StringUtils.isNotBlank(dept)) {
    predicates.add(cb.equal(deptJoin.get("name"), dept));
  }
  return cb.and(predicates.toArray(new Predicate[0]));
};
Enter fullscreen mode Exit fullscreen mode

MyBatis wraps if-else in XML. JPA wraps if-else in Predicate chains. Same problem, different syntax β€” you're still manually translating parameters into query conditions, line by line.

In 2026, frontend devs are doing vibe coding β€” describe intent, let tools handle the details. Meanwhile, backend list queries are still stuck in the artisanal stone age: hand-written XML, hand-stitched Predicates, hand-typed if-else chains. Not because we want to. Because there was never a tool that let us vibe.

The root cause of all this pain: you've mashed "declaration" and "execution" together.


A Different Mindset

Let's step back for a second.

Your database tables are already defined. What users want to search is up to them.

So why do you β€” the translator between HTTP parameters and SQL β€” have to write every single if-else by hand?

What if a smart middle layer existed that just… knew?

  • It knows what fields your entity has β†’ that's your search boundary
  • It knows what parameters the frontend sent β†’ that's your search intent
  • Combine the two β†’ generate the SQL

This idea isn't mine. In the API world, it has a name:

GraphQL β€” clients specify what fields they want. The server returns exactly those fields. One request replaces multiple REST calls.

So… what about list queries?

GraphQL Bean Searcher
Domain API data queries Database list retrieval
Client controls Which fields to return Which fields + filters + sort order + page size
Protocol POST + GraphQL body Standard HTTP params (GET/POST)
Core idea Declare what data you want Declare search boundary, let params drive the query
Adoption cost Change your API layer, add schema One dependency. Zero refactoring.

In one sentence: GraphQL lets the frontend control data fields in one request. Bean Searcher lets the frontend control filtering, sorting, pagination, and stats β€” using the URL params you already know.


The GraphQL of List Retrieval

You define one entity:

@SearchBean(tables = "user u, dept d", where = "u.dept_id = d.id", autoMapTo = "u")
public class UserVO {
  private Long id;
  private String name;
  private Integer age;
  private String gender;
  @DbField("d.name")
  private String deptName;
  private LocalDate entryDate;
  // getters & setters...
}
Enter fullscreen mode Exit fullscreen mode

Then, the entire search endpoint is one line of code:

@GetMapping("/user/search")
public SearchResult<UserVO> search(HttpServletRequest request) {
  return beanSearcher.search(UserVO.class, MapUtils.flat(request.getParameterMap()));
}
Enter fullscreen mode Exit fullscreen mode

Frontend sends a GET request:

GET /user/search?name=Li&age-0=20&age-1=30&age-op=bt&sort=age&order=desc
Enter fullscreen mode Exit fullscreen mode

And the response:

{
  "dataList": [
    { "id": 1, "name": "Li Wei", "age": 25, "deptName": "Engineering", "entryDate": "2023-03-01" },
    { "id": 2, "name": "Li Ming", "age": 28, "deptName": "Product", "entryDate": "2022-11-15" }
  ],
  "totalCount": 47,
  "summaries": [ 1350 ]
}
Enter fullscreen mode Exit fullscreen mode

Pagination. Table joins. Multi-condition filtering. Sorting. Stats. One endpoint. Everything. No XML. No if-else. No VO conversion layer.

This is Bean Searcher β€” a framework I've been using for three years and can't stop recommending.


What It Actually Does

One sentence explains 90% of the code it saves:

Entity classes declare the search boundary. HTTP parameters drive the query logic.

This is vibe coding for list queries. You declare "what can be searched." The frontend declares "what should be searched." All the translation in between β€” the framework handles it.

Traditional (MyBatis / JPA) Bean Searcher
Define filter conditions Write <if> in XML / stitch QueryWrapper in Java Parameter names map directly to field names
Add a new filter Change XML / Java code β†’ recompile β†’ redeploy Frontend sends a new parameter. Zero backend changes.
Multi-table joins Hand-write JOIN SQL Entity class declares relationships
Return results Need a VO conversion layer The SearchBean is the VO
Security Write your own validation Anti-injection, pagination caps, deep-offset throttling β€” all on by default

A metaphor: traditional ORMs are imperative β€” you tell the framework every step. Bean Searcher is declarative β€” you declare the boundary (what fields exist), and the frontend declares the intent (what to filter/sort/paginate).

You're not writing queries anymore. You're declaring search boundaries.


A Question You're Probably Thinking

"Doesn't this force the frontend to pass too many parameters?"

I get this question constantly. The answer: how many parameters the frontend sends depends on product complexity, not on your backend framework. Period.

If the product only needs a fuzzy search box? Frontend sends ?name=Li. That's it. No name-op. No name-ic. You can even use it with zero annotations β€” a plain POJO gets its fields auto-mapped to database columns (camelCase β†’ snake_case).

Remember: annotations are for constraints and fine-tuning, not mandatory setup. A single-table entity is searchable out of the box, no annotations required.


Is It the Enemy of MyBatis/JPA?

Absolutely not.

MyBatis handles writes. Bean Searcher handles list reads. Different tools, different jobs, peaceful coexistence.

Just as GraphQL wasn't created to kill REST β€” it just makes API queries more flexible β€” Bean Searcher wasn't created to kill MyBatis. It just makes list retrieval stop being painful.

When to use
MyBatis / JPA Writes, transactions, complex business logic
Bean Searcher Admin dashboards, data exports, report queries, any "dynamic multi-filter" scenario
The relationship Complementary, not competing. Just add one dependency.

Real Project Experience

I've used Bean Searcher in three production projects (Spring Boot 2/3/4, Solon 3/4). The biggest change isn't "less code" β€” it's a different way of thinking.

Before: I'd get a list query requirement and my brain would go "OK, how do I write this SQL? How do I stitch the parameters? What object do I pass for pagination? What about sorting?"

Now: I get the same requirement and think "What fields does the page need? Which tables do they come from? Which fields should the frontend be able to filter on?"

You stop being a SQL stitcher. You become a domain modeler.

2026's hottest term is vibe coding β€” describe your intent, let tools handle the details. Frontend devs have Cursor, v0, and bolt to vibe with. Why should backend list queries still be done the old-fashioned way?

When I explain this to colleagues, their first reaction is always the same: "So it's like… GraphQL, but for Java backends?"

"Exactly. Or put another way β€” it's vibe coding for list queries. Declare the boundary, drive with parameters, one line of code."


Try It

If you've read this far and recognized every pain point I described β€” you should try this.

No refactoring your project. No replacing your ORM. No changing your architecture. It's a completely non-invasive framework that coexists with MyBatis, JPA, and Spring Data JDBC.

If this solves a real pain point for you, star the repo. Help more Java engineers trapped in if-else hell find their way out.

After all β€” your life is worth more than writing if-else.

Top comments (0)