From 7802f6e19e9bdc90be46a21df5c378da66438a71 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 27 Mar 2020 09:29:31 +0100 Subject: [PATCH] DATAJDBC-513 - Introduce Query, Criteria and Update Objects for Spring Data Relational. --- .../data/relational/core/query/Criteria.java | 695 ++++++++++++++++++ .../core/query/CriteriaDefinition.java | 140 ++++ .../data/relational/core/query/Query.java | 260 +++++++ .../data/relational/core/query/Update.java | 108 +++ .../relational/core/query/package-info.java | 6 + .../relational/core/query/CriteriaTests.java | 25 + .../core/query/CriteriaUnitTests.java | 293 ++++++++ 7 files changed, 1527 insertions(+) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/package-info.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java new file mode 100644 index 00000000..a6b3d3ab --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java @@ -0,0 +1,695 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Central class for creating queries. It follows a fluent API style so that you can easily chain together multiple + * criteria. Static import of the {@code Criteria.property(…)} method will improve readability as in + * {@code where(property(…).is(…)}. + *

+ * The Criteria API supports composition with a {@link #empty() NULL object} and a {@link #from(List) static factory + * method}. Example usage: + * + *

+ * Criteria.from(Criteria.where("name").is("Foo"), Criteria.from(Criteria.where("age").greaterThan(42)));
+ * 
+ * + * rendering: + * + *
+ * WHERE name = 'Foo' AND age > 42
+ * 
+ * + * @author Mark Paluch + * @author Oliver Drotbohm + * @author Roman Chigvintsev + * @since 2.0 + */ +public class Criteria implements CriteriaDefinition { + + static final Criteria EMPTY = new Criteria(SqlIdentifier.EMPTY, Comparator.INITIAL, null); + + private final @Nullable Criteria previous; + private final Combinator combinator; + private final List group; + + private final @Nullable SqlIdentifier column; + private final @Nullable Comparator comparator; + private final @Nullable Object value; + private final boolean ignoreCase; + + private Criteria(SqlIdentifier column, Comparator comparator, @Nullable Object value) { + this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false); + } + + private Criteria(@Nullable Criteria previous, Combinator combinator, List group, + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { + this(previous, combinator, group, column, comparator, value, false); + } + + private Criteria(@Nullable Criteria previous, Combinator combinator, List group, + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase) { + + this.previous = previous; + this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator; + this.group = group; + this.column = column; + this.comparator = comparator; + this.value = value; + this.ignoreCase = ignoreCase; + } + + private Criteria(@Nullable Criteria previous, Combinator combinator, List group) { + + this.previous = previous; + this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator; + this.group = group; + this.column = null; + this.comparator = null; + this.value = null; + this.ignoreCase = false; + } + + + /** + * Static factory method to create an empty Criteria. + * + * @return an empty {@link Criteria}. + */ + public static Criteria empty() { + return EMPTY; + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link List Criterias}. + * + * @return new {@link Criteria}. + */ + public static Criteria from(Criteria... criteria) { + + Assert.notNull(criteria, "Criteria must not be null"); + Assert.noNullElements(criteria, "Criteria must not contain null elements"); + + return from(Arrays.asList(criteria)); + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link List Criterias}. + * + * @return new {@link Criteria}. + */ + public static Criteria from(List criteria) { + + Assert.notNull(criteria, "Criteria must not be null"); + Assert.noNullElements(criteria, "Criteria must not contain null elements"); + + if (criteria.isEmpty()) { + return EMPTY; + } + + if (criteria.size() == 1) { + return criteria.get(0); + } + + return EMPTY.and(criteria); + } + + /** + * Static factory method to create a Criteria using the provided {@code column} name. + * + * @param column Must not be {@literal null} or empty. + * @return a new {@link CriteriaStep} object to complete the first {@link Criteria}. + */ + public static CriteriaStep where(String column) { + + Assert.hasText(column, "Column name must not be null or empty!"); + + return new DefaultCriteriaStep(SqlIdentifier.unquoted(column)); + } + + /** + * Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code column} name. + * + * @param column Must not be {@literal null} or empty. + * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. + */ + public CriteriaStep and(String column) { + + Assert.hasText(column, "Column name must not be null or empty!"); + + SqlIdentifier identifier = SqlIdentifier.unquoted(column); + return new DefaultCriteriaStep(identifier) { + @Override + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(Criteria.this, Combinator.AND, Collections.emptyList(), identifier, comparator, value); + } + }; + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link Criteria} group. + * + * @param criteria criteria object. + * @return a new {@link Criteria} object. + * @since 1.1 + */ + public Criteria and(Criteria criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + return and(Collections.singletonList(criteria)); + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link Criteria} group. + * + * @param criteria criteria objects. + * @return a new {@link Criteria} object. + * @since 1.1 + */ + public Criteria and(List criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + return new Criteria(Criteria.this, Combinator.AND, criteria); + } + + /** + * Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code column} name. + * + * @param column Must not be {@literal null} or empty. + * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. + */ + public CriteriaStep or(String column) { + + Assert.hasText(column, "Column name must not be null or empty!"); + + SqlIdentifier identifier = SqlIdentifier.unquoted(column); + return new DefaultCriteriaStep(identifier) { + @Override + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(Criteria.this, Combinator.OR, Collections.emptyList(), identifier, comparator, value); + } + }; + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code OR} using the provided {@link Criteria} group. + * + * @param criteria criteria object. + * @return a new {@link Criteria} object. + * @since 1.1 + */ + public Criteria or(Criteria criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + return or(Collections.singletonList(criteria)); + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code OR} using the provided {@link Criteria} group. + * + * @param criteria criteria object. + * @return a new {@link Criteria} object. + * @since 1.1 + */ + public Criteria or(List criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + return new Criteria(Criteria.this, Combinator.OR, criteria); + } + + /** + * Creates a new {@link Criteria} with the given "ignore case" flag. + * + * @param ignoreCase {@literal true} if comparison should be done in case-insensitive way + * @return a new {@link Criteria} object + */ + public Criteria ignoreCase(boolean ignoreCase) { + if (this.ignoreCase != ignoreCase) { + return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase); + } + return this; + } + + /** + * @return the previous {@link Criteria} object. Can be {@literal null} if there is no previous {@link Criteria}. + * @see #hasPrevious() + */ + @Nullable + public Criteria getPrevious() { + return previous; + } + + /** + * @return {@literal true} if this {@link Criteria} has a previous one. + */ + public boolean hasPrevious() { + return previous != null; + } + + /** + * @return {@literal true} if this {@link Criteria} is empty. + * @since 1.1 + */ + @Override + public boolean isEmpty() { + + if (!doIsEmpty()) { + return false; + } + + Criteria parent = this.previous; + + while (parent != null) { + + if (!parent.doIsEmpty()) { + return false; + } + + parent = parent.previous; + } + + return true; + } + + private boolean doIsEmpty() { + + if (this.comparator == Comparator.INITIAL) { + return true; + } + + if (this.column != null) { + return false; + } + + for (Criteria criteria : group) { + + if (!criteria.isEmpty()) { + return false; + } + } + + return true; + } + + /** + * @return {@literal true} if this {@link Criteria} is empty. + */ + public boolean isGroup() { + return !this.group.isEmpty(); + } + + /** + * @return {@link Combinator} to combine this criteria with a previous one. + */ + public Combinator getCombinator() { + return combinator; + } + + public List getGroup() { + return group; + } + + /** + * @return the column/property name. + */ + @Nullable + public SqlIdentifier getColumn() { + return column; + } + + /** + * @return {@link Comparator}. + */ + @Nullable + public Comparator getComparator() { + return comparator; + } + + /** + * @return the comparison value. Can be {@literal null}. + */ + @Nullable + public Object getValue() { + return value; + } + + /** + * Checks whether comparison should be done in case-insensitive way. + * + * @return {@literal true} if comparison should be done in case-insensitive way + */ + @Override + public boolean isIgnoreCase() { + return ignoreCase; + } + + /** + * Interface declaring terminal builder methods to build a {@link Criteria}. + */ + public interface CriteriaStep { + + /** + * Creates a {@link Criteria} using equality. + * + * @param value must not be {@literal null}. + */ + Criteria is(Object value); + + /** + * Creates a {@link Criteria} using equality (is not). + * + * @param value must not be {@literal null}. + */ + Criteria not(Object value); + + /** + * Creates a {@link Criteria} using {@code IN}. + * + * @param values must not be {@literal null}. + */ + Criteria in(Object... values); + + /** + * Creates a {@link Criteria} using {@code IN}. + * + * @param values must not be {@literal null}. + */ + Criteria in(Collection values); + + /** + * Creates a {@link Criteria} using {@code NOT IN}. + * + * @param values must not be {@literal null}. + */ + Criteria notIn(Object... values); + + /** + * Creates a {@link Criteria} using {@code NOT IN}. + * + * @param values must not be {@literal null}. + */ + Criteria notIn(Collection values); + + /** + * Creates a {@link Criteria} using less-than ({@literal <}). + * + * @param value must not be {@literal null}. + */ + Criteria lessThan(Object value); + + /** + * Creates a {@link Criteria} using less-than or equal to ({@literal <=}). + * + * @param value must not be {@literal null}. + */ + Criteria lessThanOrEquals(Object value); + + /** + * Creates a {@link Criteria} using greater-than({@literal >}). + * + * @param value must not be {@literal null}. + */ + Criteria greaterThan(Object value); + + /** + * Creates a {@link Criteria} using greater-than or equal to ({@literal >=}). + * + * @param value must not be {@literal null}. + */ + Criteria greaterThanOrEquals(Object value); + + /** + * Creates a {@link Criteria} using {@code LIKE}. + * + * @param value must not be {@literal null}. + */ + Criteria like(Object value); + + /** + * Creates a {@link Criteria} using {@code NOT LIKE}. + * + * @param value must not be {@literal null} + * @return a new {@link Criteria} object + */ + Criteria notLike(Object value); + + /** + * Creates a {@link Criteria} using {@code IS NULL}. + */ + Criteria isNull(); + + /** + * Creates a {@link Criteria} using {@code IS NOT NULL}. + */ + Criteria isNotNull(); + + /** + * Creates a {@link Criteria} using {@code IS TRUE}. + * + * @return a new {@link Criteria} object + */ + Criteria isTrue(); + + /** + * Creates a {@link Criteria} using {@code IS FALSE}. + * + * @return a new {@link Criteria} object + */ + Criteria isFalse(); + } + + /** + * Default {@link CriteriaStep} implementation. + */ + static class DefaultCriteriaStep implements CriteriaStep { + + private final SqlIdentifier property; + + DefaultCriteriaStep(SqlIdentifier property) { + this.property = property; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#is(java.lang.Object) + */ + @Override + public Criteria is(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.EQ, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#not(java.lang.Object) + */ + @Override + public Criteria not(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.NEQ, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.lang.Object[]) + */ + @Override + public Criteria in(Object... values) { + + Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values, "Values must not contain a null value!"); + + if (values.length > 1 && values[1] instanceof Collection) { + throw new InvalidDataAccessApiUsageException( + "You can only pass in one argument of type " + values[1].getClass().getName()); + } + + return createCriteria(Comparator.IN, Arrays.asList(values)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.util.Collection) + */ + @Override + public Criteria in(Collection values) { + + Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values.toArray(), "Values must not contain a null value!"); + + return createCriteria(Comparator.IN, values); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.lang.Object[]) + */ + @Override + public Criteria notIn(Object... values) { + + Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values, "Values must not contain a null value!"); + + if (values.length > 1 && values[1] instanceof Collection) { + throw new InvalidDataAccessApiUsageException( + "You can only pass in one argument of type " + values[1].getClass().getName()); + } + + return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.util.Collection) + */ + @Override + public Criteria notIn(Collection values) { + + Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values.toArray(), "Values must not contain a null value!"); + + return createCriteria(Comparator.NOT_IN, values); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThan(java.lang.Object) + */ + @Override + public Criteria lessThan(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.LT, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThanOrEquals(java.lang.Object) + */ + @Override + public Criteria lessThanOrEquals(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.LTE, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThan(java.lang.Object) + */ + @Override + public Criteria greaterThan(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.GT, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThanOrEquals(java.lang.Object) + */ + @Override + public Criteria greaterThanOrEquals(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.GTE, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#like(java.lang.Object) + */ + @Override + public Criteria like(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + return createCriteria(Comparator.LIKE, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notLike(java.lang.Object) + */ + @Override + public Criteria notLike(Object value) { + Assert.notNull(value, "Value must not be null!"); + return createCriteria(Comparator.NOT_LIKE, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNull() + */ + @Override + public Criteria isNull() { + return createCriteria(Comparator.IS_NULL, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNotNull() + */ + @Override + public Criteria isNotNull() { + return createCriteria(Comparator.IS_NOT_NULL, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isTrue() + */ + @Override + public Criteria isTrue() { + return createCriteria(Comparator.IS_TRUE, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isFalse() + */ + @Override + public Criteria isFalse() { + return createCriteria(Comparator.IS_FALSE, null); + } + + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(this.property, comparator, value); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java new file mode 100644 index 00000000..8fbe7ae3 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java @@ -0,0 +1,140 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Interface defining a criteria definition object. A criteria definition may chain multiple predicates and may also + * represent a group of nested criteria objects. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface CriteriaDefinition { + + /** + * Static factory method to create an empty Criteria. + * + * @return an empty {@link Criteria}. + */ + static CriteriaDefinition empty() { + return Criteria.EMPTY; + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link List Criterias}. + * + * @return new {@link Criteria}. + */ + static CriteriaDefinition from(Criteria... criteria) { + + Assert.notNull(criteria, "Criteria must not be null"); + Assert.noNullElements(criteria, "Criteria must not contain null elements"); + + return from(Arrays.asList(criteria)); + } + + /** + * Create a new {@link Criteria} and combine it as group with {@code AND} using the provided {@link List Criterias}. + * + * @return new {@link Criteria}. + * @since 1.1 + */ + static CriteriaDefinition from(List criteria) { + + Assert.notNull(criteria, "Criteria must not be null"); + Assert.noNullElements(criteria, "Criteria must not contain null elements"); + + if (criteria.isEmpty()) { + return Criteria.EMPTY; + } + + if (criteria.size() == 1) { + return criteria.get(0); + } + + return Criteria.EMPTY.and(criteria); + } + + /** + * @return {@literal true} if this {@link Criteria} is empty. + */ + boolean isGroup(); + + List getGroup(); + + /** + * @return the column/property name. + */ + @Nullable + SqlIdentifier getColumn(); + + /** + * @return {@link Criteria.Comparator}. + */ + @Nullable + Comparator getComparator(); + + /** + * @return the comparison value. Can be {@literal null}. + */ + @Nullable + Object getValue(); + + /** + * Checks whether comparison should be done in case-insensitive way. + * + * @return {@literal true} if comparison should be done in case-insensitive way + */ + boolean isIgnoreCase(); + + /** + * @return the previous {@link CriteriaDefinition} object. Can be {@literal null} if there is no previous + * {@link CriteriaDefinition}. + * @see #hasPrevious() + */ + @Nullable + CriteriaDefinition getPrevious(); + + /** + * @return {@literal true} if this {@link Criteria} has a previous one. + */ + boolean hasPrevious(); + + /** + * @return {@literal true} if this {@link Criteria} is empty. + */ + boolean isEmpty(); + + /** + * @return {@link Combinator} to combine this criteria with a previous one. + */ + Combinator getCombinator(); + + enum Combinator { + INITIAL, AND, OR; + } + + enum Comparator { + INITIAL, EQ, NEQ, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_LIKE, NOT_IN, IN, IS_TRUE, IS_FALSE + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java new file mode 100644 index 00000000..313dece0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java @@ -0,0 +1,260 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Query object representing {@link Criteria}, columns, {@link Sort}, and limit/offset for a SQL query. {@link Query} is + * created with a fluent API creating immutable objects. + * + * @author Mark Paluch + * @since 2.0 + * @see Criteria + * @see Sort + * @see Pageable + */ +public class Query { + + private final @Nullable CriteriaDefinition criteria; + + private final List columns; + private final Sort sort; + private final int limit; + private final long offset; + + /** + * Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}. + * + * @param criteria must not be {@literal null}. + * @return a new {@link Query} for the given {@link Criteria}. + */ + public static Query query(CriteriaDefinition criteria) { + return new Query(criteria); + } + + /** + * Creates a new {@link Query} using the given {@link Criteria}. + * + * @param criteria must not be {@literal null}. + */ + private Query(@Nullable CriteriaDefinition criteria) { + this(criteria, Collections.emptyList(), Sort.unsorted(), -1, -1); + } + + private Query(@Nullable CriteriaDefinition criteria, List columns, Sort sort, int limit, long offset) { + + this.criteria = criteria; + this.columns = columns; + this.sort = sort; + this.limit = limit; + this.offset = offset; + } + + /** + * Create a new empty {@link Query}. + * + * @return + */ + public static Query empty() { + return new Query(null); + } + + /** + * Add columns to the query. + * + * @param columns + * @return a new {@link Query} object containing the former settings with {@code columns} applied. + */ + public Query columns(String... columns) { + + Assert.notNull(columns, "Columns must not be null"); + + return withColumns(Arrays.stream(columns).map(SqlIdentifier::unquoted).collect(Collectors.toList())); + } + + /** + * Add columns to the query. + * + * @param columns + * @return a new {@link Query} object containing the former settings with {@code columns} applied. + */ + public Query columns(Collection columns) { + + Assert.notNull(columns, "Columns must not be null"); + + return withColumns(columns.stream().map(SqlIdentifier::unquoted).collect(Collectors.toList())); + } + + /** + * Add columns to the query. + * + * @param columns + * @return a new {@link Query} object containing the former settings with {@code columns} applied. + * @since 1.1 + */ + public Query columns(SqlIdentifier... columns) { + + Assert.notNull(columns, "Columns must not be null"); + + return withColumns(Arrays.asList(columns)); + } + + /** + * Add columns to the query. + * + * @param columns + * @return a new {@link Query} object containing the former settings with {@code columns} applied. + */ + private Query withColumns(Collection columns) { + + Assert.notNull(columns, "Columns must not be null"); + + List newColumns = new ArrayList<>(this.columns); + newColumns.addAll(columns); + return new Query(this.criteria, newColumns, this.sort, this.limit, offset); + } + + /** + * Set number of rows to skip before returning results. + * + * @param offset + * @return a new {@link Query} object containing the former settings with {@code offset} applied. + */ + public Query offset(long offset) { + return new Query(this.criteria, this.columns, this.sort, this.limit, offset); + } + + /** + * Limit the number of returned documents to {@code limit}. + * + * @param limit + * @return a new {@link Query} object containing the former settings with {@code limit} applied. + */ + public Query limit(int limit) { + return new Query(this.criteria, this.columns, this.sort, limit, this.offset); + } + + /** + * Set the given pagination information on the {@link Query} instance. Will transparently set {@code offset} and + * {@code limit} as well as applying the {@link Sort} instance defined with the {@link Pageable}. + * + * @param pageable + * @return a new {@link Query} object containing the former settings with {@link Pageable} applied. + */ + public Query with(Pageable pageable) { + + if (pageable.isUnpaged()) { + return this; + } + + assertNoCaseSort(pageable.getSort()); + + return new Query(this.criteria, this.columns, this.sort.and(sort), pageable.getPageSize(), pageable.getOffset()); + } + + /** + * Add a {@link Sort} to the {@link Query} instance. + * + * @param sort + * @return a new {@link Query} object containing the former settings with {@link Sort} applied. + */ + public Query sort(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + if (sort.isUnsorted()) { + return this; + } + + assertNoCaseSort(sort); + + return new Query(this.criteria, this.columns, this.sort.and(sort), this.limit, this.offset); + } + + /** + * Return the {@link Criteria} to be applied. + * + * @return + */ + public Optional getCriteria() { + return Optional.ofNullable(this.criteria); + } + + /** + * Return the columns that this query should project. + * + * @return + */ + public List getColumns() { + return columns; + } + + /** + * Return {@literal true} if the {@link Query} has a sort parameter. + * + * @return {@literal true} if sorted. + * @see Sort#isSorted() + */ + public boolean isSorted() { + return sort.isSorted(); + } + + public Sort getSort() { + return sort; + } + + /** + * Return the number of rows to skip. + * + * @return + */ + public long getOffset() { + return this.offset; + } + + /** + * Return the maximum number of rows to be return. + * + * @return + */ + public int getLimit() { + return this.limit; + } + + private static void assertNoCaseSort(Sort sort) { + + for (Sort.Order order : sort) { + if (order.isIgnoreCase()) { + throw new IllegalArgumentException(String.format( + "Given sort contained an Order for %s with ignore case;" + "Ignore case sorting is not supported", + order.getProperty())); + } + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java new file mode 100644 index 00000000..aac9cb2a --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Class to easily construct SQL update assignments. + * + * @author Mark Paluch + * @author Oliver Drotbohm + * @since 2.0 + */ +public class Update { + + private static final Update EMPTY = new Update(Collections.emptyMap()); + + private final Map columnsToUpdate; + + private Update(Map columnsToUpdate) { + this.columnsToUpdate = columnsToUpdate; + } + + /** + * Static factory method to create an {@link Update} from {@code assignments}. + * + * @param assignments must not be {@literal null}. + * @return + */ + public static Update from(Map assignments) { + return new Update(new LinkedHashMap<>(assignments)); + } + + /** + * Static factory method to create an {@link Update} using the provided column. + * + * @param column must not be {@literal null}. + * @param value can be {@literal null}. + * @return + */ + public static Update update(String column, @Nullable Object value) { + return EMPTY.set(column, value); + } + + /** + * Update a column by assigning a value. + * + * @param column must not be {@literal null}. + * @param value can be {@literal null}. + * @return + */ + public Update set(String column, @Nullable Object value) { + + Assert.hasText(column, "Column for update must not be null or blank"); + + return addMultiFieldOperation(SqlIdentifier.unquoted(column), value); + } + + /** + * Update a column by assigning a value. + * + * @param column must not be {@literal null}. + * @param value can be {@literal null}. + * @return + * @since 1.1 + */ + public Update set(SqlIdentifier column, @Nullable Object value) { + return addMultiFieldOperation(column, value); + } + + /** + * Returns all assignments. + * + * @return + */ + public Map getAssignments() { + return Collections.unmodifiableMap(this.columnsToUpdate); + } + + private Update addMultiFieldOperation(SqlIdentifier key, @Nullable Object value) { + + Assert.notNull(key, "Column for update must not be null"); + + Map updates = new LinkedHashMap<>(this.columnsToUpdate); + updates.put(key, value); + + return new Update(updates); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/package-info.java new file mode 100644 index 00000000..34fedea5 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/package-info.java @@ -0,0 +1,6 @@ +/** + * Query and update support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.relational.core.query; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java new file mode 100644 index 00000000..5a2de5cb --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import static org.junit.Assert.*; + +/** + * @author Mark Paluch + */ +public class CriteriaTests { + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java new file mode 100644 index 00000000..3fd4fe05 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; +import static org.springframework.data.relational.core.query.Criteria.*; + +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Unit tests for {@link Criteria}. + * + * @author Mark Paluch + * @author Jens Schauder + * @author Roman Chigvintsev + */ +public class CriteriaUnitTests { + + @Test // DATAJDBC-513 + public void fromCriteria() { + + Criteria nested1 = where("foo").isNotNull(); + Criteria nested2 = where("foo").isNull(); + Criteria criteria = Criteria.from(nested1, nested2); + + assertThat(criteria.isGroup()).isTrue(); + assertThat(criteria.getGroup()).containsExactly(nested1, nested2); + assertThat(criteria.getPrevious()).isEqualTo(Criteria.empty()); + } + + @Test // DATAJDBC-513 + public void fromCriteriaOptimized() { + + Criteria nested = where("foo").is("bar").and("baz").isNotNull(); + Criteria criteria = Criteria.from(nested); + + assertThat(criteria).isSameAs(nested); + } + + @Test // DATAJDBC-513 + public void isEmpty() { + + assertSoftly(softly -> { + + Criteria empty = empty(); + Criteria notEmpty = where("foo").is("bar"); + + assertThat(empty.isEmpty()).isTrue(); + assertThat(notEmpty.isEmpty()).isFalse(); + + assertThat(Criteria.from(notEmpty).isEmpty()).isFalse(); + assertThat(Criteria.from(notEmpty, notEmpty).isEmpty()).isFalse(); + + assertThat(Criteria.from(empty).isEmpty()).isTrue(); + assertThat(Criteria.from(empty, empty).isEmpty()).isTrue(); + + assertThat(Criteria.from(empty, notEmpty).isEmpty()).isFalse(); + assertThat(Criteria.from(notEmpty, empty).isEmpty()).isFalse(); + }); + } + + @Test // DATAJDBC-513 + public void andChainedCriteria() { + + Criteria criteria = where("foo").is("bar").and("baz").isNotNull(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("baz")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IS_NOT_NULL); + assertThat(criteria.getValue()).isNull(); + assertThat(criteria.getPrevious()).isNotNull(); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + + criteria = criteria.getPrevious(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // DATAJDBC-513 + public void andGroupedCriteria() { + + Criteria criteria = where("foo").is("bar").and(where("foo").is("baz")); + + assertThat(criteria.isGroup()).isTrue(); + assertThat(criteria.getGroup()).hasSize(1); + assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + + criteria = criteria.getPrevious(); + + assertThat(criteria).isNotNull(); + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // DATAJDBC-513 + public void orChainedCriteria() { + + Criteria criteria = where("foo").is("bar").or("baz").isNotNull(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("baz")); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.OR); + + criteria = criteria.getPrevious(); + + assertThat(criteria).isNotNull(); + assertThat(criteria.getPrevious()).isNull(); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // DATAJDBC-513 + public void orGroupedCriteria() { + + Criteria criteria = where("foo").is("bar").or(where("foo").is("baz")); + + assertThat(criteria.isGroup()).isTrue(); + assertThat(criteria.getGroup()).hasSize(1); + assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.OR); + + criteria = criteria.getPrevious(); + + assertThat(criteria).isNotNull(); + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // DATAJDBC-513 + public void shouldBuildEqualsCriteria() { + + Criteria criteria = where("foo").is("bar"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test + public void shouldBuildEqualsIgnoreCaseCriteria() { + Criteria criteria = where("foo").is("bar").ignoreCase(true); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + assertThat(criteria.isIgnoreCase()).isTrue(); + } + + @Test // DATAJDBC-513 + public void shouldBuildNotEqualsCriteria() { + + Criteria criteria = where("foo").not("bar"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.NEQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // DATAJDBC-513 + public void shouldBuildInCriteria() { + + Criteria criteria = where("foo").in("bar", "baz"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IN); + assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); + } + + @Test // DATAJDBC-513 + public void shouldBuildNotInCriteria() { + + Criteria criteria = where("foo").notIn("bar", "baz"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.NOT_IN); + assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); + } + + @Test // DATAJDBC-513 + public void shouldBuildGtCriteria() { + + Criteria criteria = where("foo").greaterThan(1); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.GT); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // DATAJDBC-513 + public void shouldBuildGteCriteria() { + + Criteria criteria = where("foo").greaterThanOrEquals(1); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.GTE); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // DATAJDBC-513 + public void shouldBuildLtCriteria() { + + Criteria criteria = where("foo").lessThan(1); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.LT); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // DATAJDBC-513 + public void shouldBuildLteCriteria() { + + Criteria criteria = where("foo").lessThanOrEquals(1); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.LTE); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // DATAJDBC-513 + public void shouldBuildLikeCriteria() { + + Criteria criteria = where("foo").like("hello%"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.LIKE); + assertThat(criteria.getValue()).isEqualTo("hello%"); + } + + @Test + public void shouldBuildNotLikeCriteria() { + Criteria criteria = where("foo").notLike("hello%"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.NOT_LIKE); + assertThat(criteria.getValue()).isEqualTo("hello%"); + } + + @Test // DATAJDBC-513 + public void shouldBuildIsNullCriteria() { + + Criteria criteria = where("foo").isNull(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IS_NULL); + } + + @Test // DATAJDBC-513 + public void shouldBuildIsNotNullCriteria() { + + Criteria criteria = where("foo").isNotNull(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IS_NOT_NULL); + } + + @Test // DATAJDBC-513 + public void shouldBuildIsTrueCriteria() { + + Criteria criteria = where("foo").isTrue(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IS_TRUE); + } + + @Test // DATAJDBC-513 + public void shouldBuildIsFalseCriteria() { + + Criteria criteria = where("foo").isFalse(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IS_FALSE); + } +}