# Bean Searcher: a read-only ORM where the query comes in as HTTP parameters

> Bean Searcher is a Java library for list retrieval that turns a domain class and a set of request parameters into generated SQL, so filtering, sorting, pagination, joins, and aggregates are expressed as query parameters rather than as hand-written if-else condition building. It integrates as one Spring Boot dependency and requires no changes to existing code.

**troyzhxu/bean-searcher** — 🔥🔥🔥 A read-only ORM for complex queries in Java

- Repository: https://github.com/troyzhxu/bean-searcher
- Website: https://bs.zhxu.cn/en/
- Stars: 1,383 · Forks: 147
- Language: Java
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/troyzhxu-bean-searcher

## The claim is that the frontend owns the query

Bean Searcher is described as a read-only ORM for complex queries in Java, and the framing the project uses is that Bean Searcher is the GraphQL of list retrieval. Entities define search boundaries, parameters drive query logic, and no special protocol is required, just add one dependency.

The GraphQL analogy is doing real work in that sentence, so it is worth being precise about where it holds. GraphQL lets a client specify which fields it wants in one request, and Bean Searcher lets a client specify filter conditions, sort order, pagination, and stats, without a dedicated schema.

The three-part design is stated as declarative search against imperative coding. The entity as declaration, where the SearchBean defines what can be searched and annotations are optional. Parameters as query, where the frontend controls what is searched and one endpoint handles endless combinations. And zero protocol burden, since it works on standard HTTP parameters.

The reason this exists is spelled out in the Why section. MyBatis and Hibernate excel at CRUD, but when it comes to list retrieval with multi-condition filtering, table joins, sorting, and pagination, they often require piles of if-else condition stitching and VO conversion code. The four pain points in the capability table are multi-condition list queries, multi-table joins, frontend dynamic filtering, and non-invasive integration, and the traditional approaches named are if-else SQL concatenation or Specification, manual JOIN or XML mapping, adding fields and changing backend APIs, and Spring Data REST with exposed repositories and restructured URLs.

The integration claim is the one to check first, since it determines the cost of trying it. One dependency, inject `BeanSearcher`, existing code untouched.

The licence is Apache-2.0, the primary language is Java, and the default branch is `dev`.

## The entity declares the boundary, the controller stays one line

The mechanism is visible in a short piece of code, and it is worth reading both halves.

The entity side is an annotation that names the tables and the join condition, plus one field annotation for the joined column:

```java
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
  private long id;
  private String username;
  private int status;
  private int age;
  private String gender;
  private Date joinDate;
  private int roleId;
  @DbField("r.name")
  private String roleName;
  // Getters and setters...
}
```

The `tables` value is a table list with aliases, the `joinCond` is the join expression, and `autoMapTo` says which alias the entity's own fields map to. The `@DbField` on `roleName` points at the joined table's column, which is how a field from another table is projected.

The controller side is then one line:

```java
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private BeanSearcher beanSearcher;              // Inject BeanSearcher

    @GetMapping("/index")
    public SearchResult<User> index(HttpServletRequest request) {
        // Only one line of code written here
        return beanSearcher.search(User.class, MapUtils.flat(request.getParameterMap()), User::getAge);
    }

}
```

Three things in that signature matter. The class argument says what to search. The second argument is the flattened request parameter map, which is where the client's conditions arrive. And the third argument, `User::getAge`, is the field to aggregate, which is what produces the summary.

The important detail is what is not there. There is no `if` on the status parameter, no `if` on the name, no separate count query, no separate sum query, no manual pagination arithmetic. The single line covers retrieval from multiple tables, pagination by any field, combined filtering by any field, sorting by any field, and the summary on the age field.

The parameter names are the query language, and the documentation enumerates them. `page=1 & size=10` for specified pagination. `status=1` for an exact filter. `name=Jac & name-op=sw` for a starts-with match. `name=Jack & name-ic=true` for a case-insensitive exact match. `sort=age & order=desc` for sorting. `onlySelect=username,age` to return only two fields, and `selectExclude=joinDate` to leave one out. The `name-op` and `name-ic` suffixes are the interesting part, because they are how a single parameter name carries both a field and an operator.

## The comparison is against two implementations of the same API

The project does not simply assert that it saves code. It ships a demo project that implements the same API three ways, and the table compares them.

The controller code column gives one line for Bean Searcher and about 280 lines for both MyBatis and Spring Data JPA. The extra files column gives 0 for Bean Searcher, 1 Mapper plus 1 XML for MyBatis, and 2 Entities plus 1 Repository for JPA. The SQL generation row contrasts declarative annotations with manual XML on one side and the Criteria API on the other. And the source row points at three separate directories, `backend-springboot4`, `backend-vs-mybatis`, and `backend-vs-jpa`, so the comparison is checkable rather than asserted.

The explanation of what the other two need is concrete. The MyBatis and JPA versions need handwritten parameter parsing, dynamic condition assembly, three separate queries for list, count, and sum, pagination, sorting, and CSV streaming, all of which the one line in Bean Searcher handles.

The point about the three separate queries is worth dwelling on, because it is the practical reason the alternative implementations are long. A list endpoint that can filter on any field needs a count query for the pagination total, and an aggregate needs a third query, and with dynamic filters each of them has to assemble its own conditions. The one-line version gets that for free because the library is doing all three, and it is the SQL generation that is shared rather than the controller code.

There is also a programmatic route, for cases where the query is not coming from HTTP parameters. The type-safe builder API looks like this:

```java
Map<String, Object> params = MapUtils.builder()
        .selectExclude(User::getJoinDate)                 // Exclude joinDate field
        .field(User::getStatus, 1)                        // Filter: status = 1
        .field(User::getName, "Jack").ic()
```

The method references mean the field names are checked by the compiler rather than by a string, and the chained modifiers such as `ic()` for a case-insensitive match correspond to the `name-ic=true` parameter form.

## The modules tell you what the project has grown into

The top level of the repository is a Maven multi-module project, and the module names describe the scope.

There is `bean-searcher/` which is the core, and `bean-searcher-boot-starter/` which is the Spring Boot integration, the one you add as a dependency. There is `bean-searcher-doc/` for the documentation, `bean-searcher-label/` for what is presumably internationalisation or labelling of returned fields, and `bean-searcher-exporter/` for export, which fits with the CSV streaming the comparison mentions.

Two modules are worth pausing on. `bean-searcher-solon-plugin/` is a plugin for the Solon framework, so this is not only a Spring Boot project. And there is `performance/`, a directory at the top level, which is a benchmark suite rather than a module, and it tells you the author cares about how the generated SQL performs rather than only about how little code you write.

There is also `bean-searcher-demos/`, which holds the three comparison implementations, and `assets/` for the images.

The release history shows steady patch work: v4.8.9 in July 2026, v4.8.11 about two weeks later, and v4.8.12 three days after that. The version number is in the 4.8 range, so the API has been stable across a long line of patch releases rather than breaking on majors, which is what you want from something that sits in the request path of every list endpoint.

The documentation site is at bs.zhxu.cn, with an English version at bs.zhxu.cn/en, and there is a live online demo at demo-bs.zhxu.cn. The project also has a Chinese README alongside the English one, a `TODO.md`, a `CHANGELOG.md`, and a `CODE_OF_CONDUCT.md`.

## What a read-only ORM gives up, and what it asks of you

The limits of this design are worth stating before you adopt it, because they are consequences of the same decision that gives it its value.

First, it is read-only. That is in the name and in the description. There is no insert, no update, and no delete, because the whole model is that the client specifies a read and the library generates the SQL for it. A team that wants one abstraction over its data, reads and writes alike, will find this covers only half of it. That is not a defect, it is the scope, and the naming is honest about it.

Second, the entity carries the query definition. The `@SearchBean` annotation with its `tables`, `joinCond`, and `autoMapTo` puts table names and join conditions in the domain class. For a simple entity that is fine, and the documentation says annotations are optional for single-table search, with zero-annotation working out of the box. For an entity that joins four tables, the annotation is a small SQL fragment sitting in a Java class, and that is a design choice a reviewer will have opinions about. The alternative, a mapper with XML, keeps the SQL out of the entity but adds files and the if-else assembly the project set out to remove.

Third, the frontend now has a query surface. The parameter names and the operator suffixes are a contract, and the project treats them as one. The benefit is that a new filter needs no back end change, which the capability table calls out. The cost is that any field you expose through the entity is now filterable, sortable, and pageable by anyone who can reach the endpoint, and the access control on that has to live somewhere other than in the code that used to build the if-else chain. A team with row-level authorisation per field has to think carefully about what a single parameter-driven endpoint means for their permission model.

Fourth, the generated SQL has to be good. The library builds the statement from parameter names, and the SQL injection surface is therefore in the library's parameter handling rather than in your code, which is the right place for it to be, but it does mean you are trusting a code generator with your table names.

Fifth, and this is the practical point, the one-line version hides a lot of work. The line is short because the library is doing the parameter parsing, the condition assembly, the count query, the aggregate query, the pagination, the sorting, and the CSV export. That is a good trade when the queries are all the same shape, which is exactly the case the project targets. It is a bad trade when each endpoint needs something different, because then you are fighting the generator rather than using it.

## Conclusion

Bean Searcher fits a Java team with many list endpoints whose filters keep changing, especially one on Spring Boot where a front end wants to combine conditions without a back end change per field. It is the wrong choice for write operations, since it is read-only by design, and it is the wrong choice if your application already has a query layer you are happy with. Check three things before you adopt it: that the entities you want to expose match its expectations, since joins are declared with an annotation on the entity rather than a mapper, that the front end can be pointed at a parameter-driven endpoint without a rewrite, and that you read the comparison table, because the line-count claim is the project's own and the demo project is where you would check it.

## FAQ

### What is Bean Searcher and what problem does it solve?

It is a read-only ORM for complex list queries in Java. It is aimed at the case where MyBatis or Hibernate need piles of if-else condition stitching and VO conversion code for multi-condition filtering, joins, sorting, and pagination, and it turns a domain class plus request parameters into generated SQL so one endpoint handles many combinations of filters.

### How do I add Bean Searcher to a Spring Boot project and query it?

Add the starter dependency `cn.zhxu:bean-searcher-boot-starter`, inject `BeanSearcher`, and call `beanSearcher.search(User.class, MapUtils.flat(request.getParameterMap()), User::getAge)` in a controller. The entity can be an existing class, with annotations optional for single-table search, and `@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")` declares a join.

### Which query operations can Bean Searcher do from HTTP parameters?

Retrieval from multiple tables, pagination by any field, combined filtering by any field, sorting by any field, and a summary on a chosen field. The parameter forms include page and size, an exact filter such as status=1, operator suffixes such as name-op=sw for a starts-with match, name-ic=true for a case-insensitive match, sort and order, onlySelect for a field subset, and selectExclude to leave a field out.

### Does Bean Searcher support writing data?

No, it is a read-only ORM. The scope is list retrieval, with the entity declaring what can be searched and request parameters driving the query. The project also ships a type-safe programmatic builder, MapUtils.builder(), for building parameters in code rather than from HTTP.

## Sources

- [License: Apache-2.0](https://github.com/troyzhxu/bean-searcher/blob/dev/LICENSE)
- [Project website](https://bs.zhxu.cn/en/)
- [README](https://github.com/troyzhxu/bean-searcher/blob/dev/README.md)
- [Releases](https://github.com/troyzhxu/bean-searcher/releases)
- [troyzhxu/bean-searcher on GitHub](https://github.com/troyzhxu/bean-searcher)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/troyzhxu-bean-searcher
