Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamic query restrictions #895

Merged
merged 36 commits into from
Dec 4, 2024

Conversation

njr-11
Copy link
Contributor

@njr-11 njr-11 commented Nov 5, 2024

This PR contains some ideas that I was experimenting with locally for dynamic query restrictions #829 and hadn't been planning to propose until after a lot more discussion occurred. However, because code changes are already being proposed and I wanted to illustrate how this should tie into the static metamodel, I decided to share this prematurely so everyone can see what I was originally aiming for with the pattern.

@Repository
public interface Products extends CrudRepository<Product, Long> {
    List<Product> search(String namePattern,
                         Restriction<Product> restriction,
                         Order<Product> order);
}

Usage with single restriction,

    List<Product> found = products.search(pattern,
                                          _Product.price.lessThan(50.0f),
                                          Order.by(_Product.price.desc(),
                                                   _Product.name.asc()));

Usage with multiple restrictions,

    List<Product> found = products.search(pattern,
                                          Restrict.all(_Product.price.greaterThan(25.0f),
                                                       _Product.price.lessThan(50.0f),
                                                       _Product.name.startsWith(pattern).ignoreCase(),
                                                       _Product.image.notNull()),
                                          Order.by(_Product.price.desc(),
                                                   _Product.name.asc()));

This was just the basics, and doesn't yet include not/negation, or Pattern/Range, which might make parts of it unnecessary. It also just occurred to me that if we had a TextRestriction subclass, which I did not include in this, we could omit the *IgnoreCase methods and have .ignoreCase() instead.

@njr-11 njr-11 force-pushed the 829-dynamic-query-restrictions branch 2 times, most recently from da5b807 to 52d42e9 Compare November 6, 2024 19:11
@njr-11 njr-11 force-pushed the 829-dynamic-query-restrictions branch from 59e75cc to 0f9b1d3 Compare November 8, 2024 22:35
@njr-11 njr-11 force-pushed the 829-dynamic-query-restrictions branch from 0f9b1d3 to d41c721 Compare November 8, 2024 23:12
@njr-11
Copy link
Contributor Author

njr-11 commented Nov 8, 2024

After a lot of difficulty reviewing another PR that combines several different new parts of Jakarta Data, I decided to just finish off updating this PR with how I expect Restrictions to work.

I was originally thinking that Restrictions would only come from the static metamodel, but others have expressed a desire that users be able to create them like they can for Sort. I'm okay with that, but not by giving users access to record/implementation class constructors where they can populate them with any combinations of things they like. With this PR, all implementation classes are made non-public and kept out of the API. There are only 2 places where users obtain restrictions:

  • static metamodel attributes (basic restrictions only)
  • the Restrict class (both composite and basic restrictions)

This also aims to de-emphasize interfaces that the end user shouldn't care about (and only Data Providers need, such as Operator and the Composite restriction) out of the main view by placing them in inner classes.

There is no JavaDoc in this on purpose. The idea is to iterate over the design first, and later spend time on JavaDoc and other things.

Other spec function like Pattern, Range, Is, ... are intentionally kept out so we can focus only on getting Restriction correct.

Restriction could justifiably be placed in the jakarta.data package alongside some other special classes that are used as repository parameters (Sort, Order, Limit) although we might want to consider if it is worth it to have a separate jakarta.data.restrict package to separate it out like we do with jakarta.data.page for PageRequest, which is the other special parameter type. I think the latter is a good place for it, so that's where I put it in this PR.

greater/lessThan methods require a Comparable parameter (lots of things can be sortable, but I think they are all Comparable). TextAttribute methods, and their equivalents on Restrict, require a String parameter. I added a commented out method showing what it might be appropriate to do with a Pattern parameter as well if that gets added. But ignore that if you don't want to think about Pattern yet.

I would like to see more error checking for values supplied to Restriction factory methods in general, and identify if we can use any more precise types that would help keep invalid objects out, but we can save that for when we are further along.

@gavinking
Copy link
Contributor

Restriction could justifiably be placed in the jakarta.data package

This is where I would have put it.

Copy link
Contributor Author

@njr-11 njr-11 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a commit to cover most of Gavin's review comments.
Also, I made an update to ensure ignoreCase is only available to String values, and not to numbers, dates, and so forth.

@njr-11
Copy link
Contributor Author

njr-11 commented Nov 9, 2024

Restriction could justifiably be placed in the jakarta.data package

This is where I would have put it.

That's fine. I have moved it to jakarta.data in the review fixes commit.

@gavinking
Copy link
Contributor

Added a commit to cover most of Gavin's review comments.

Thanks!

@njr-11
Copy link
Contributor Author

njr-11 commented Nov 13, 2024

Switched methods such as startsWith(prefixPattern) to startsWith(prefix) and automatically add escaping to spare the user from that level of detail/complexity.

@njr-11 njr-11 force-pushed the 829-dynamic-query-restrictions branch from a910b1f to 2b8a88c Compare November 13, 2024 20:35
return new TextRestrictionRecord<>(field, Operator.LIKE, ESCAPED, p);
}

public static <T> Restriction<T> not(Object value, String field) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear that not means, notEquals´. Please either, use neorNotEquals`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can update that. Given that we have a equalTo method, the equivalent here will be notEqualTo

return new TextRestrictionRecord<>(field, NOT, Operator.EQUAL, value);
}

public static <T> TextRestriction<T> notContains(String substring, String field) {
Copy link
Contributor

@otaviojava otaviojava Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove all of those no method factory sequences?

It would be great if we have only two options: by the method instead negateand a single method factory not.

It is pretty similar to the Java Predicate: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/Predicate.html

TextRestriction<Book> = TextRestriction.not(TextRestriction.contains(...));
TextRestriction<Book> = TextRestriction.contains(...).negate();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@otaviojava JDQL has operators like not between, not like, etc, so it makes sense to me for such things to be reflected here. And it's significantly less verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Gavin on this.

Being able to write code such as,

products.search(_Product.description.notContains("refurbished"));

is much more straightforward and readable than

products.search(_Product.description.contains("refurbished").negated());

or

products.search(Restrict.not(_Product.description.contains("refurbished")));

* @throws IllegalArgumentException if the same character is supplied for
* both wildcard types.
*/
private static String toLikeEscaped(char charWildcard,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once everybody agreed with Gavin's Pattern class, why not centralize this behavior in that class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's much better to have a nice class which models this basic concept. It's a much semantically stronger construct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, if Pattern gets added, this utility method can move to there. Also note that the method is private, so the documentation on it is only for our own benefit and will not show in the API, so users won't even notice once it is moved.

public interface Restriction<T> {
boolean isNegated();

enum Operator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this one from the inner class; it is good, mainly for the Javadoc.

Copy link
Contributor Author

@njr-11 njr-11 Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I could move it to a top level class, although note that it currently is not something that applications use. Its current purpose is for Jakarta Data providers to know which type of restriction it is, so I had intentionally made in an inner class to keep it more out of the way of application developers. If you are looking ahead to the possibility of reusing it for an @Is annotation, then it would become something that is directly used by applications.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@is annotation

That is what I do have on my mind.

@otaviojava
Copy link
Contributor

@njr-11 done the first step: njr-11#1

@njr-11
Copy link
Contributor Author

njr-11 commented Nov 21, 2024

Thanks for contributing the unit tests. That helps to confirm the correctness of the changes.
I was thinking maybe we should wait on adding JavaDoc until after this PR is merged, so that we don't need to rewrite it if anyone asks for further changes to the initial PR.

@otaviojava
Copy link
Contributor

Thanks for contributing the unit tests. That helps to confirm the correctness of the changes. I was thinking maybe we should wait on adding JavaDoc until after this PR is merged, so that we don't need to rewrite it if anyone asks for further changes to the initial PR.

It does make sense.
I will focus on the Unit test to find some issues on it.

@otaviojava
Copy link
Contributor

@njr-11 one more PR against yours.

@njr-11
Copy link
Contributor Author

njr-11 commented Nov 25, 2024

@njr-11 one more PR against yours.

@otaviojava I don't see another PR against this anywhere.

@otaviojava
Copy link
Contributor

@njr-11 one more PR against yours.

@otaviojava I don't see another PR against this anywhere.

This one here:
njr-11#2

Create more test scenarios to the new interfaces
@njr-11
Copy link
Contributor Author

njr-11 commented Nov 26, 2024

@njr-11 one more PR against yours.

@otaviojava I don't see another PR against this anywhere.

This one here: njr-11#2

I see it now and have accepted your PR/commits for more unit tests. Thanks for adding the test coverage.

@otaviojava
Copy link
Contributor

@njr-11 one more PR against yours.

@otaviojava I don't see another PR against this anywhere.

This one here: njr-11#2

I see it now and have accepted your PR/commits for more unit tests. Thanks for adding the test coverage.

@njr-11

This is the last one:

njr-11#3

I put the validation on field making the field mandatory

@otaviojava
Copy link
Contributor

As a first step, IMHO, it is good to go.

@njr-11 @gavinking, what do you think?

@otaviojava otaviojava merged commit a4cd44d into jakartaee:main Dec 4, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants