jakartaee / data

Data-API
Apache License 2.0
108 stars 30 forks source link

Ability to define query restrictions dynamically #829

Open njr-11 opened 3 months ago

njr-11 commented 3 months ago

Replaces #825

Users will sometimes need to define queries dynamically instead of statically in response to what they see happen at run time. For example, a user specifies various customizable search options and in response, the application dynamically adds restrictions to a query to filter for the matching results.

An application ought to be able to define the same kind of query restrictions dynamically that it can already define statically (with patterns like Query by Method Name).

Query restrictions should make use of the Jakarta Data Static Metamodel and generally be agnostic to the database as much as possible, other than the same sort of limitations that Query By Method Name and the other static patterns have on which operations certain classes of NoSQL databases are incapable of.

The Jakarta Data Static Metamodel will be expanded to represent conditions that are possible per attribute type. These conditions can be supplied as an extra repository method parameter, similar to what is currently possible with Order.by/Sorts.

Example with one static and one dynamic restriction:

Page<Product> page1 = products.search(namePattern,
                                      _Product.price.lessThan(100.0),
                                      Order.by(_Product.price.desc(),
                                               _Product.id.asc()));

For multiple dynamic restrictions,

One possible way suggested by Otavio in 825

List<Product> list = products.search(
                         namePattern,
                         _Product.price.greaterThan(min)
                             .and(_Product.price.lessThan(max))
                             .and(_Product.color.equal(BLUE)
                                 .or(_Product.color.equal(GREEN))),
                         Order.by(_Product.name.asc(),
                                  _Product.id.asc()));

Another possible way (slightly more wordy, possibly more readable)

List<Product> list = products.search(
                         namePattern,
                         Require.all(_Product.price.greaterThan(min),
                                     _Product.price.lessThan(max),
                                     Require.any(_Product.color.equal(BLUE),
                                                 _Product.color.equal(GREEN))),
                         Order.by(_Product.name.asc(),
                                  _Product.id.asc()));

Or we could think of another way to cover and/or/not. That part can be figured out later. The basic idea here will be to add the restrictions to the static metamodel and decide how type safe to make them. For example, requiring Number for numeric attributes, or possibly more precise, or just Object for everything, ...

gavinking commented 3 months ago
                         _Product.price.greaterThan(min)
                             .and(_Product.price.lessThan(max))
                             .and(_Product.color.equal(BLUE)
                                 .or(_Product.color.equal(GREEN))),

This to me is highly ambiguous. Conventionally, and has a higher precedence than or, but it's not at all clear what the precedence of the operators is here. Best to make it completely explicit as in your second example.

hantsy commented 3 months ago

For dynamically building the query criteria, I would like to consider the JPAStreamer and DotNet Core Entity Framework as references.

An example of JPASteamer home page, https://jpastreamer.org/

jpaStreamer.stream(User.class)
    .filter(User$.age.equal("22"))
    .sorted(User$.name.reversed())
    .skip(10)
    .limit(5)
    .forEach(System.out::println); 

Especially since Java has a great Stream API for filtering, transforming, and aggregating.

Use generated metamodels to assist in implementing the Stream API.

The Repository concept is a container for a collection of entities, should be streamable, and can be converted to Stream seamlessly.

products.stream().filter(Product$.name.like("").and(...))
.map(result -> another POJO)
.collect()/forEach/toList

In the implementation, translate it and execute it on the DB driver API level(Jdbc or NOSQL drivers).

gavinking commented 3 months ago

@hantsy JPAstreamer is very cute and I don't hate it at all. It does indeed have a sort-of approximately similar scope to JDQL, in that it looks like it works well for queries with no joins or cartesian products. So it looks like in principle you could establish a welldefined mapping between the two things.

But on the other hand I'm not sure how cleanly it really fits with the notion of repositories and query methods. It's a sort-of different paradigm. If you want to use JPAstreamer for your queries, I'm not clear on why you would want to have a repository to begin with.

Anyway, I think it's something we can discuss. If we're already proposing to introduce restrictions like _Product.price.greaterThan(min) it might not be that much more work to make them also work with a stream-like API.

My main concern that we would maybe end up with a worse JPAstreamer with no support for e.g. group by. It's not clear that having a worse JPAstreamer would improve the ecosystem. [And I dunno how the person or people behind JPAstreamer would feel about us wading into this area.]