org.springframework.data.r2dbc.query.QueryMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-data-r2dbc-dsl Show documentation
Show all versions of spring-data-r2dbc-dsl Show documentation
Spring Data DSL module for Web querying and Criteria building
/*
* Copyright 2019-2021 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.r2dbc.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.SettableValue;
import org.springframework.data.relational.core.dialect.Escaper;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
import org.springframework.data.relational.core.query.ValueFunction;
import org.springframework.data.relational.core.sql.*;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Pair;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.r2dbc.core.Parameter;
import org.springframework.r2dbc.core.binding.BindMarker;
import org.springframework.r2dbc.core.binding.BindMarkers;
import org.springframework.r2dbc.core.binding.Bindings;
import org.springframework.r2dbc.core.binding.MutableBindings;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Maps {@link CriteriaDefinition} and {@link Sort} objects considering mapping metadata and dialect-specific
* conversion.
*
* @author Mark Paluch
* @author Roman Chigvintsev
*/
public class QueryMapper {
private final R2dbcConverter converter;
private final R2dbcDialect dialect;
private final MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext;
/**
* Creates a new {@link QueryMapper} with the given {@link R2dbcConverter}.
*
* @param dialect
* @param converter must not be {@literal null}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public QueryMapper(R2dbcDialect dialect, R2dbcConverter converter) {
Assert.notNull(converter, "R2dbcConverter must not be null!");
Assert.notNull(dialect, "R2dbcDialect must not be null!");
this.converter = converter;
this.dialect = dialect;
this.mappingContext = (MappingContext) converter.getMappingContext();
}
/**
* Render a {@link SqlIdentifier} for SQL usage.
* The resulting String might contain quoting characters.
*
* @param identifier the identifier to be rendered.
* @return an identifier String.
* @since 1.1
*/
public String toSql(SqlIdentifier identifier) {
Assert.notNull(identifier, "SqlIdentifier must not be null");
return identifier.toSql(this.dialect.getIdentifierProcessing());
}
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
*
* @param sort must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return
*/
public Sort getMappedObject(Sort sort, @Nullable RelationalPersistentEntity> entity) {
if (entity == null) {
return sort;
}
List mappedOrder = new ArrayList<>();
for (Sort.Order order : sort) {
Field field = createPropertyField(entity, SqlIdentifier.unquoted(order.getProperty()), this.mappingContext);
mappedOrder.add(
Sort.Order.by(toSql(field.getMappedColumnName())).with(order.getNullHandling()).with(order.getDirection()));
}
return Sort.by(mappedOrder);
}
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
*
* @param sort must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return
* @since 1.1
*/
public List getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity> entity) {
List mappedOrder = new ArrayList<>();
for (Sort.Order order : sort) {
Field field = createPropertyField(entity, SqlIdentifier.unquoted(order.getProperty()), this.mappingContext);
OrderByField orderBy = OrderByField.from(table.column(field.getMappedColumnName()))
.withNullHandling(order.getNullHandling());
mappedOrder.add(order.isAscending() ? orderBy.asc() : orderBy.desc());
}
return mappedOrder;
}
/**
* Map the {@link Expression} object to apply field name mapping using {@link Class the type to read}.
*
* @param expression must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link Expression}.
* @since 1.1
*/
public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity> entity) {
if (entity == null || expression instanceof AsteriskFromTable) {
return expression;
}
if (expression instanceof Column) {
Column column = (Column) expression;
Field field = createPropertyField(entity, column.getName());
Table table = column.getTable();
Column columnFromTable = table.column(field.getMappedColumnName());
return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable;
}
if (expression instanceof SimpleFunction) {
SimpleFunction function = (SimpleFunction) expression;
List arguments = function.getExpressions();
List mappedArguments = new ArrayList<>(arguments.size());
for (Expression argument : arguments) {
mappedArguments.add(getMappedObject(argument, entity));
}
SimpleFunction mappedFunction = SimpleFunction.create(function.getFunctionName(), mappedArguments);
return function instanceof Aliased ? mappedFunction.as(((Aliased) function).getAlias()) : mappedFunction;
}
throw new IllegalArgumentException(String.format("Cannot map %s", expression));
}
/**
* Map a {@link CriteriaDefinition} object into {@link Condition} and consider value/{@code NULL} {@link Bindings}.
*
* @param markers bind markers object, must not be {@literal null}.
* @param criteria criteria definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link BoundCondition}.
* @since 1.1
*/
public BoundCondition getMappedObject(BindMarkers markers, CriteriaDefinition criteria, Table table,
@Nullable RelationalPersistentEntity> entity) {
Assert.notNull(markers, "BindMarkers must not be null!");
Assert.notNull(criteria, "CriteriaDefinition must not be null!");
Assert.notNull(table, "Table must not be null!");
MutableBindings bindings = new MutableBindings(markers);
if (criteria.isEmpty()) {
throw new IllegalArgumentException("Cannot map empty Criteria");
}
Condition mapped = unroll(criteria, table, entity, bindings);
return new BoundCondition(bindings, mapped);
}
/**
* Map a {@link Criteria} object into {@link Condition} and consider value/{@code NULL} {@link Bindings}.
*
* @param markers bind markers object, must not be {@literal null}.
* @param criteria criteria definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link BoundCondition}.
* @deprecated since 1.1.
*/
@Deprecated
public BoundCondition getMappedObject(BindMarkers markers, Criteria criteria, Table table,
@Nullable RelationalPersistentEntity> entity) {
Assert.notNull(markers, "BindMarkers must not be null!");
Assert.notNull(criteria, "Criteria must not be null!");
Assert.notNull(table, "Table must not be null!");
MutableBindings bindings = new MutableBindings(markers);
if (criteria.isEmpty()) {
throw new IllegalArgumentException("Cannot map empty Criteria");
}
Condition mapped = unroll(criteria, table, entity, bindings);
return new BoundCondition(bindings, mapped);
}
private Condition unroll(CriteriaDefinition criteria, Table table, @Nullable RelationalPersistentEntity> entity,
MutableBindings bindings) {
CriteriaDefinition current = criteria;
// reverse unroll criteria chain
Map forwardChain = new HashMap<>();
while (current.hasPrevious()) {
forwardChain.put(current.getPrevious(), current);
current = current.getPrevious();
}
// perform the actual mapping
Condition mapped = getCondition(current, bindings, table, entity);
while (forwardChain.containsKey(current)) {
CriteriaDefinition criterion = forwardChain.get(current);
Condition result = null;
Condition condition = getCondition(criterion, bindings, table, entity);
if (condition != null) {
result = combine(criterion, mapped, criterion.getCombinator(), condition);
}
if (result != null) {
mapped = result;
}
current = criterion;
}
if (mapped == null) {
throw new IllegalStateException("Cannot map empty Criteria");
}
return mapped;
}
@Nullable
private Condition unrollGroup(List extends CriteriaDefinition> criteria, Table table,
CriteriaDefinition.Combinator combinator, @Nullable RelationalPersistentEntity> entity,
MutableBindings bindings) {
Condition mapped = null;
for (CriteriaDefinition criterion : criteria) {
if (criterion.isEmpty()) {
continue;
}
Condition condition = unroll(criterion, table, entity, bindings);
mapped = combine(criterion, mapped, combinator, condition);
}
return mapped;
}
@Nullable
private Condition getCondition(CriteriaDefinition criteria, MutableBindings bindings, Table table,
@Nullable RelationalPersistentEntity> entity) {
if (criteria.isEmpty()) {
return null;
}
if (criteria.isGroup()) {
Condition condition = unrollGroup(criteria.getGroup(), table, criteria.getCombinator(), entity, bindings);
return condition == null ? null : Conditions.nest(condition);
}
return mapCondition(criteria, bindings, table, entity);
}
private Condition combine(CriteriaDefinition criteria, @Nullable Condition currentCondition,
CriteriaDefinition.Combinator combinator, Condition nextCondition) {
if (currentCondition == null) {
currentCondition = nextCondition;
} else if (combinator == CriteriaDefinition.Combinator.INITIAL) {
currentCondition = currentCondition.and(Conditions.nest(nextCondition));
} else if (combinator == CriteriaDefinition.Combinator.AND) {
currentCondition = currentCondition.and(nextCondition);
} else if (combinator == CriteriaDefinition.Combinator.OR) {
currentCondition = currentCondition.or(nextCondition);
} else {
throw new IllegalStateException("Combinator " + combinator + " not supported");
}
return currentCondition;
}
private Condition mapCondition(CriteriaDefinition criteria, MutableBindings bindings, Table table,
@Nullable RelationalPersistentEntity> entity) {
Field propertyField = createPropertyField(entity, criteria.getColumn(), this.mappingContext);
Column column = table.column(propertyField.getMappedColumnName());
TypeInformation> actualType = propertyField.getTypeHint().getRequiredActualType();
Object mappedValue;
Class> typeHint;
if (criteria.getValue() instanceof SettableValue) {
SettableValue settableValue = (SettableValue) criteria.getValue();
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue);
} else if (criteria.getValue() instanceof Parameter) {
Parameter parameter = (Parameter) criteria.getValue();
mappedValue = convertValue(parameter.getValue(), propertyField.getTypeHint());
typeHint = getTypeHint(mappedValue, actualType.getType(), parameter);
} else if (criteria.getValue() instanceof ValueFunction) {
ValueFunction