-
Notifications
You must be signed in to change notification settings - Fork 132
#282 - Support of query derivation #295
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
Conversation
Thanks for your PR. It will take a bit to review this change. Looking at a few operators, there are some facilities such as |
@mp911de I tried to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks very good.
I noted some smaller stuff.
The change also needs rebasing onto master since we introduced SqlIdentifier
in the meantime. (Sorry for that.)
I created an issue for adding essential infrastructure to spring-data-relational: https://jira.spring.io/browse/DATAJDBC-502
It is probably easiest if you would do these changes as well.
But feel free to let us know if you want us to do this.
@@ -38,6 +34,10 @@ | |||
import org.springframework.lang.Nullable; | |||
import org.springframework.util.Assert; | |||
|
|||
import java.util.ArrayList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make sure your formatter is configured as described in https://github.com/spring-projects/spring-data-build/tree/master/etc/ide
@@ -0,0 +1,307 @@ | |||
/* | |||
* Copyright 2018-2020 the original author or authors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The copyright for new files should be only for the current year.
} | ||
|
||
private static boolean parameterIsCollectionLike(RelationalParameters.RelationalParameter parameter) { | ||
return Iterable.class.isAssignableFrom(parameter.getType()) || parameter.getType().isArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We learned the hard way that we shouldn't use Iterable
here but Collection
. Also we should check first if the type of the parameter matches the type of the property. See https://jira.spring.io/browse/DATAJPA-1682
/** | ||
* @author Roman Chigvintsev | ||
*/ | ||
public class LikeEscaperTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test names should be named ....UnitTests
or ....IntegrationTests
this applies to all tests.
*/ | ||
public class LikeEscaperTest { | ||
@Test | ||
public void ignoresNulls() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this class deserves some additional tests to ensure the escaping actually works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I relied on integration tests here, but yes, you're right, unit tests are required too.
@schauder I don't mind to make changes in 'spring-data-relational' too. Also I'm planning to add query caching (in the similar way as it is done in 'spring-data-jpa') along with corrections to this PR. Is it OK or should I create another request? |
Sounds great. Just don't let the PR grow to much. I rather have something working on master then something fast somewhere in the process. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a review pass from a R2DBC perspective. Thanks for the great work, the approach looks really good. I left a few comments about aspects to consider and some R2DBC specifics that make it easier to work with various database dialects.
BindMarker secondBindMarker = createBindMarker(parameterMetadataProvider.next(part)); | ||
|
||
// TODO: why do not we have BETWEEN condition? | ||
return Conditions.isGreaterOrEqualTo(pathExpression, firstBindMarker) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good candidate for an enhancement in Spring Data Relational. On a related note: We support in some stores also a Range
data type (findByDateBetween(Range<LocalDate>)
) that allows for more fine-grained control of inclusion/exclusion regarding boundaries.
@NotNull | ||
private BindMarker createBindMarker(ParameterMetadata parameterMetadata) { | ||
if (parameterMetadata.getName() != null) { | ||
return SQL.bindMarker(":" + parameterMetadata.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using named parameters manifests the requirement that named parameter processing is enabled in DatabaseClient
. We tried to not stick with such an assumption. Ideally, we generate directly SQL that does not require post-processing by accessing Dialect
when generating SQL to use the proper BindMarkersFactory
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can suggest the following solution:
- at first we should expose
Dialect
because we'll need it to create correct bind markers; in my opinionReactiveDataAccessStrategy
interface is a good candidate (if so I'm not sure whether default method is required since this interface has only one implementation); - pass
Dialect
to the constructor ofParameterMetadataProvider
public class PartTreeR2dbcQuery extends AbstractR2dbcQuery {
...
protected BindableQuery createQuery(RelationalParameterAccessor accessor) {
...
ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(accessor, likeEscaper,
dataAccessStrategy.getDialect());
...
- in
ParameterMetadataProvider
createBindMarkers
using currentBindMarkersFactory
class ParameterMetadataProvider {
...
private ParameterMetadataProvider(Parameters<?, ?> parameters,
@Nullable Iterator<Object> bindableParameterValueIterator, LikeEscaper likeEscaper, R2dbcDialect dialect) {
...
this.bindMarkers = dialect.getBindMarkersFactory().create();
}
...
- save parameter placeholder in
ParameterMetadata
class ParameterMetadataProvider {
...
public ParameterMetadata next(Part part) {
...
ParameterMetadata metadata = ParameterMetadata.builder()
.type(parameter.getType())
.partType(part.getType())
.name(parameterName)
.isNullParameter(getParameterValue() == null && Part.Type.SIMPLE_PROPERTY.equals(part.getType()))
.placeholder(getPlaceholder(parameterName))
.likeEscaper(likeEscaper)
.build();
...
}
private String getPlaceholder(String parameterName) {
return bindMarkers.next(parameterName).getPlaceholder();
}
...
- use placeholder to create bind marker
class ConditionFactory {
...
@NonNull
private BindMarker createBindMarker(ParameterMetadata parameterMetadata) {
return SQL.bindMarker(parameterMetadata.getPlaceholder());
}
...
- always use placeholder to bind parameter value
class PartTreeBindableQuery implements BindableQuery {
...
public <T extends DatabaseClient.BindSpec<T>> T bind(T bindSpec) {
T bindSpecToUse = bindSpec;
int index = 0;
for (Object value : accessor.getValues()) {
ParameterMetadata metadata = parameterMetadataProvider.getParameterMetadata(index++);
Class<?> parameterType = metadata.getType();
if (value == null) {
bindSpecToUse = bindSpecToUse.bindNull(metadata.getPlaceholder(), parameterType);
} else {
bindSpecToUse = bindSpecToUse.bind(metadata.getPlaceholder(), metadata.prepare(value));
}
}
return bindSpecToUse;
}
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just realized that my approach won't work for databases where question mark is used as a placeholder (MySQL for example). Maybe during parameter value binding I should check whether placeholder is '?' and use binding by index if it is true.
* | ||
* @author Roman Chigvintsev | ||
*/ | ||
public class LikeEscaper { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type looks as it would be a good candidate for Spring Data Relational, to be defined as part of a Dialect
. Please note that SQL Server (probably the only exception for now) uses square brackets ([]
) for a regex-y style declaring character ranges. We should design the class with this behavior in mind. Turning this class into an interface and adding a default implementation for %
would be a good first step. In the SQL Server-specific dialect, we can add an adaption for square brackets.
String parameterName = metadata.getName(); | ||
Class<?> parameterType = metadata.getType(); | ||
|
||
if (parameterName != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way how parameters get bound in R2DBC varies from database to database. Postgres and SQL Server use named parameters ($1
or @MyParam
) while MySQL uses anonymous bind markers (?
) that are only settable via index. Our Dialect
abstraction along with BindMarkers
caters already for these differences. We don't require parameter names from the interface declaration here.
Collection<? extends Expression> selectExpressions = getSelectionExpressions(fromTable); | ||
SelectFromAndJoin selectBuilder = StatementBuilder.select(selectExpressions).from(fromTable); | ||
|
||
if (tree.isExistsProjection()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: PartTree
exposes a isLimiting()
method that allows imposing a limit on the actual query (findTop3
). It would make sense to consider that feature while being at query derivation.
if (String.class.equals(type)) { | ||
switch (partType) { | ||
case STARTING_WITH: | ||
return String.format("%s%%", likeEscaper.escape(value.toString())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As much as I like String.format()
, the number of %
contributes to the fact that the code is less comprehensible. For these methods, I'm a fan of plain "%"+value+"%"
.
*/ | ||
@Builder | ||
@AllArgsConstructor | ||
class ParameterMetadata { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please let's not use Lombok in new code as we try to reduce Lombok usage.
* <p> | ||
* Results in a rendered function: {@code UPPER(<expression>)}. | ||
*/ | ||
private class Upper implements Expression { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make use of SimpleFunction
so that we are not required to introduce new types for each function we want to support? It makes sense to expose that functionality as Functions.upper/lower
in Spring Data Relational.
public Object prepare(Object value) { | ||
Assert.notNull(value, "Value must not be null!"); | ||
if (String.class.equals(type)) { | ||
switch (partType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: Carrying out PartTree
details to the outside of the query creator isn't ideal. Maybe we can find a better approach during the merge.
…o be able to build SQL manually
…GATING_SIMPLE_PROPERTY' conditions
…'NOT CONTAINING' condition
Not necessary to keep all the commits as we would have asked you right before the merge to squash all commits into a single one. Let us know when we should have another look. |
@mp911de OK. By the way could you leave some comment related to issue with named parameters in |
I'm addressing the binding issue in a separate comment as the thread above is already quite lengthy. Bind markers follow their own strategy and therefore, we have a We have a similar setup in There are three reasons for that idea:
Let me know what you think. |
@mp911de Maybe there is some misunderstanding here. I asked you to review the solution that I proposed above - in comments to your note regarding bind markers and |
@mp911de I believe it is my misunderstanding. If by "Criteria API" you mean API that is provided by |
@mp911de Continuing the idea of using |
The spec objects are lightweight objects. For now, let's not consider caching until we really require caching of query objects. |
… instead of 'StatementBuilder'
@mp911de I made a few changes in query building mechanism according to your suggestions. Please review them.
|
Thanks a lot. @schauder and I discussed how to proceed with this PR and figured it would make a lot of sense to migrate most of the types into Spring Data Relational so we have already the infrastructure in place that would be required for Spring Data JDBC's query derivation. I'm going to pick up your pull request for merge. |
@mp911de OK. Feel free to let me know if you need some help with spring-data-relational or spring-data-jdbc. |
… Relational. Original pull request: spring-projects/spring-data-r2dbc#295.
Use AssertJ version specified by Spring Data Build. Original pull request: spring-projects/spring-data-r2dbc#295.
…nd SQL generation. We now support Conditions.between, notBetween, and notLike as additional criteria conditions and support case-insensitive comparisons. For LIKE escaping we pick up the Escaper configured at Dialect level. The newly introduced ValueFunction allows string transformation before computing a value by applying the Escaper to the raw value. Escaping is required for StartingWith, Contains and EndsWith PartTree operations. Original pull request: spring-projects/spring-data-r2dbc#295.
We now support query derivation for R2DBC repositories: interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> { Flux<Person> findByFirstname(String firstname); Flux<Person> findByFirstname(Publisher<String> firstname); Mono<Person> findByFirstnameAndLastname(String firstname, String lastname); Flux<Person> findFirstByLastnameLike(String pattern); } Original pull request: #295.
Move query derivation infrastructure to Spring Data Relational. Adapt to newly introduced ValueFunction for deferred value mapping. Use query derivation in integration tests. Tweak javadoc, add since and author tags, reformat code. Related ticket: https://jira.spring.io/browse/DATAJDBC-514 Original pull request: #295.
Thanks a lot for your contribution. That's merged and polished now. During the merge, we decided to move more components over to Spring Data Relational so that we just need to glue things together to get query derivation for Spring Data JDBC. |
This PR introduces basic query derivation without support of embedded properties and collection properties. I decided not to scatter changes among different projects so to get a clean implementation of query derivation some changes in the spring-data-relational project would be required.
Issue: #282