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

#282 - Support of query derivation #295

Closed
wants to merge 36 commits into from

Conversation

rchigvintsev
Copy link
Contributor

@rchigvintsev rchigvintsev commented Feb 3, 2020

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

@mp911de
Copy link
Member

mp911de commented Feb 3, 2020

Thanks for your PR. It will take a bit to review this change. Looking at a few operators, there are some facilities such as SimpleFunction.create("UPPER", column). Probably we require some extensions to Spring Data Relational's SQL model.

@mp911de mp911de added the type: enhancement A general enhancement label Feb 3, 2020
@rchigvintsev
Copy link
Contributor Author

@mp911de I tried to use SimpleFunction but it is taken into account only in SELECT expressions, but not in WHERE conditions. Some changes in spring-data-relational definitely won't be superfluous. In the source code I left several TODOs related to required improvements.

Copy link
Contributor

@schauder schauder left a 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;
Copy link
Contributor

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.
Copy link
Contributor

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();
Copy link
Contributor

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 {
Copy link
Contributor

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() {
Copy link
Contributor

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.

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 relied on integration tests here, but yes, you're right, unit tests are required too.

@rchigvintsev
Copy link
Contributor Author

rchigvintsev commented Mar 12, 2020

@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?

@schauder
Copy link
Contributor

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.

Copy link
Member

@mp911de mp911de left a 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)
Copy link
Member

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());
Copy link
Member

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.

Copy link
Contributor Author

@rchigvintsev rchigvintsev Mar 17, 2020

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:

  1. at first we should expose Dialect because we'll need it to create correct bind markers; in my opinion ReactiveDataAccessStrategy interface is a good candidate (if so I'm not sure whether default method is required since this interface has only one implementation);
  2. pass Dialect to the constructor of ParameterMetadataProvider
public class PartTreeR2dbcQuery extends AbstractR2dbcQuery {
    ...
    protected BindableQuery createQuery(RelationalParameterAccessor accessor) {
        ...
        ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(accessor, likeEscaper,
				dataAccessStrategy.getDialect());
...
  1. in ParameterMetadataProvider create BindMarkers using current BindMarkersFactory
class ParameterMetadataProvider {
    ...
    private ParameterMetadataProvider(Parameters<?, ?> parameters,
            @Nullable Iterator<Object> bindableParameterValueIterator, LikeEscaper likeEscaper, R2dbcDialect dialect) {
        ...
        this.bindMarkers = dialect.getBindMarkersFactory().create();
    }
...
  1. 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();
    }
...
  1. use placeholder to create bind marker
class ConditionFactory {
...
    @NonNull
    private BindMarker createBindMarker(ParameterMetadata parameterMetadata) {
        return SQL.bindMarker(parameterMetadata.getPlaceholder());
    }
...
  1. 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;
    }
...

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'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 {
Copy link
Member

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) {
Copy link
Member

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()) {
Copy link
Member

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()));
Copy link
Member

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 {
Copy link
Member

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 {
Copy link
Member

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) {
Copy link
Member

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.

@mp911de
Copy link
Member

mp911de commented Mar 20, 2020

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.

@rchigvintsev
Copy link
Contributor Author

@mp911de OK. By the way could you leave some comment related to issue with named parameters in ConditionFactory please?

@mp911de
Copy link
Member

mp911de commented Mar 20, 2020

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 BindMarkersFactory that creates BindMarkers and BindMarker. BindMarkers is associated with a state (e.g. counter) so the way to use that API is creating a BindMarker object per bindable parameter (or element in the collection to bind) and keep track of both, the BindMarker and the actual value.

We have a similar setup in DefaultStatementMapper. I'm wondering whether it would actually make sense to reuse the Criteria API and StatementMapper to accomplish query derivation.

There are three reasons for that idea:

  1. We have only a single approach to construct queries
  2. By reusing the criteria API and StatmentMapper we ensure that Dialect does not leak outside the strategy class (which could be implemented by a different library such as jOOQ and they have their own mechanics to deal with dialect specifics)
  3. We make sure that all operations that can be used from derived queries are also available through the Criteria API. If we miss an operation right now, then we can extend the Criteria API

Let me know what you think.

@rchigvintsev
Copy link
Contributor Author

@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 ConditionFactory. But perhaps in the new light of requirement of integration StatementMapper with Criteria API this solution does not make sense anymore.
When I thought about ways of building SQL of course I considered StatementMapper and I didn't find an easy way to create Criteria from Condition for SelectSpec so I choose to use StatementBuilder. Perhaps we need an option to pass Condition to SelectSpec directly and avoid unnecessary conversions since Criteria finally becomes a Condition with help of QueryMapper.

@rchigvintsev
Copy link
Contributor Author

@mp911de I believe it is my misunderstanding. If by "Criteria API" you mean API that is provided by org.springframework.data.r2dbc.query.Criteria class so in my opinion the best approach is to transform ConditionFactory to CriteriaFactory and use Criteria instead of Conditions but improvements in spring-data-relational will still be required.

@rchigvintsev
Copy link
Contributor Author

@mp911de Continuing the idea of using Criteria instead of Conditions and CriteriaFactory there is still issue with bind markers. It looks like Criteria API operates only with actual parameter values. For performance reasons it makes sense to keep SelectSpec reusable so Criteria API should have a way to specify bind marker instead of actual value (pass marker of bind marker as a value? :))

@mp911de
Copy link
Member

mp911de commented Mar 20, 2020

The spec objects are lightweight objects. For now, let's not consider caching until we really require caching of query objects.

@rchigvintsev
Copy link
Contributor Author

@mp911de I made a few changes in query building mechanism according to your suggestions. Please review them.
Notes:

  • I temporarily moved classes that are supposed to be in spring-data-relational from ConditionFactory to QueryMapper;
  • I have a little concern regarding to case-insensitive comparisons in query conditions - when IgnoreCaseType is WHEN_POSSIBLE there is a theoretical possibility when database field will be uppercased but parameter won't (and vice versa); for example, if I use spring-data-jpa I can write method findByFirstNameAllIgnoreCase(StringBuilder firstName) that will be translated (simplified) to SELECT * FROM table WHERE UPPER(first_name) = :firstName provided that "firstName" property is a string; finally on the try to use this method I will get an exception thrown by Hibernate so I don't know is it a real scenario or not; here in this PR I don't apply UPPER function when at least one part of expression doesn't support it.

@mp911de
Copy link
Member

mp911de commented Mar 27, 2020

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.

@rchigvintsev
Copy link
Contributor Author

@mp911de OK. Feel free to let me know if you need some help with spring-data-relational or spring-data-jdbc.

@mp911de mp911de added this to the 1.1 RC1 (Neumann) milestone Mar 27, 2020
mp911de pushed a commit to spring-projects/spring-data-relational that referenced this pull request Mar 27, 2020
mp911de added a commit to spring-projects/spring-data-relational that referenced this pull request Mar 27, 2020
Use AssertJ version specified by Spring Data Build.

Original pull request: spring-projects/spring-data-r2dbc#295.
mp911de added a commit to spring-projects/spring-data-relational that referenced this pull request Mar 27, 2020
…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.
mp911de pushed a commit that referenced this pull request Mar 27, 2020
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.
mp911de added a commit that referenced this pull request Mar 27, 2020
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.
mp911de added a commit that referenced this pull request Mar 27, 2020
Original pull request: #295.
@mp911de
Copy link
Member

mp911de commented Mar 27, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants