Improve JPA Provider portability - RSQL (#2131)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-12-09 17:36:57 +02:00
committed by GitHub
parent 527d8e5964
commit 13e2bc81d7
13 changed files with 87 additions and 102 deletions

View File

@@ -64,6 +64,7 @@ import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
import org.eclipse.hawkbit.repository.jpa.Jpa;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
@@ -748,24 +749,17 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
}
/**
* Sets {@link Target#getLastTargetQuery()} by native SQL in order to avoid
* raising opt lock revision as this update is not mission critical and in
* fact only written by {@link ControllerManagement}, i.e. the target
* itself.
* Sets {@link Target#getLastTargetQuery()} by native SQL in order to avoid raising opt lock revision as this update is not mission-critical
* and in fact only written by {@link ControllerManagement}, i.e. the target itself.
*/
private void setLastTargetQuery(final String tenant, final long currentTimeMillis, final List<String> chunk) {
final Map<String, String> paramMapping = new HashMap<>(chunk.size());
for (int i = 0; i < chunk.size(); i++) {
paramMapping.put("cid" + i, chunk.get(i));
}
final Query updateQuery = entityManager.createNativeQuery(
"UPDATE sp_target SET last_target_query = #last_target_query WHERE controller_id IN ("
+ formatQueryInStatementParams(paramMapping.keySet()) + ") AND tenant = #tenant");
"UPDATE sp_target SET last_target_query = " + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "last_target_query " +
"WHERE controller_id IN (" + Jpa.formatNativeQueryInClause("cid", chunk) + ")" +
" AND tenant = " + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "tenant");
paramMapping.forEach(updateQuery::setParameter);
updateQuery.setParameter("last_target_query", currentTimeMillis);
Jpa.setNativeQueryInParameter(updateQuery, "cid", chunk);
updateQuery.setParameter("tenant", tenant);
final int updated = updateQuery.executeUpdate();
@@ -1108,4 +1102,4 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
}
}
}
}

View File

@@ -104,6 +104,12 @@ public abstract class AbstractJpaBaseEntity implements BaseEntity {
}
}
@Column(name = "created_at", updatable = false, nullable = false)
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
@LastModifiedBy
public void setLastModifiedBy(final String lastModifiedBy) {
if (isController()) {
@@ -113,13 +119,7 @@ public abstract class AbstractJpaBaseEntity implements BaseEntity {
this.lastModifiedBy = lastModifiedBy;
}
// maybe needed to have correct createdAt value in the database
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
// seems needed to have correct lastModifiedBy value in the database
@Column(name = "last_modified_by", nullable = false, length = USERNAME_FIELD_LENGTH)
@Access(AccessType.PROPERTY)
public String getLastModifiedBy() {
return lastModifiedBy;

View File

@@ -25,28 +25,24 @@ import org.springframework.transaction.annotation.Transactional;
* Repository for {@link SoftwareModuleType}.
*/
@Transactional(readOnly = true)
public interface SoftwareModuleTypeRepository
extends BaseEntityRepository<JpaSoftwareModuleType> {
public interface SoftwareModuleTypeRepository extends BaseEntityRepository<JpaSoftwareModuleType> {
/**
* @param key to search for
* @return all {@link SoftwareModuleType}s in the repository with given
* {@link SoftwareModuleType#getKey()}
* @return all {@link SoftwareModuleType}s in the repository with given {@link SoftwareModuleType#getKey()}
*/
Optional<SoftwareModuleType> findByKey(String key);
/**
* @param name to search for
* @return all {@link SoftwareModuleType}s in the repository with given
* {@link SoftwareModuleType#getName()}
* @return all {@link SoftwareModuleType}s in the repository with given {@link SoftwareModuleType#getName()}
*/
Optional<SoftwareModuleType> findByName(String name);
/**
* Deletes all {@link TenantAwareBaseEntity} of a given tenant. For safety
* reasons (this is a "delete everything" query after all) we add the tenant
* manually to query even if this will by done by {@link EntityManager}
* anyhow. The DB should take care of optimizing this away.
* Deletes all {@link TenantAwareBaseEntity} of a given tenant. For safety reasons (this is a "delete everything" query
* after all) we add the tenant manually to query even if this is done by {@link EntityManager} anyhow. The DB should take
* care of optimizing this away.
*
* @param tenant to delete data from
*/
@@ -54,4 +50,4 @@ public interface SoftwareModuleTypeRepository
@Transactional
@Query("DELETE FROM JpaSoftwareModuleType t WHERE t.tenant = :tenant")
void deleteByTenant(@Param("tenant") String tenant);
}
}

View File

@@ -17,11 +17,9 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Path;
@@ -185,7 +183,7 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
final String keyValue = graph[graph.length - 1];
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
return equal((Path<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
}
final String keyFieldName = queryField.getEnumValue().getSubEntityMapTuple().map(Entry::getKey)
@@ -225,7 +223,7 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), ""));
}
final Expression<String> stringExpression = pathOfString(fieldPath);
final Path<String> stringExpression = pathOfString(fieldPath);
if (isPattern(transformedValueStr)) { // a pattern, use like
return like(stringExpression, toSQL(transformedValueStr));
} else {
@@ -311,14 +309,10 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
}
private Object convertValueIfNecessary(
final ComparisonNode node, final A fieldName, final Path<?> fieldPath, final String value) {
// in case the value of an RSQL query e.g. type==application is an
// enum we need to handle it separately because JPA needs the
// correct java-type to build an expression. So String and numeric
// values JPA can do it by its own but not for classes like enums.
// So we need to transform the given value string into the enum
// class.
private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final Path<?> fieldPath, final String value) {
// in case the value of an RSQL query e.g. type==application is an enum we need to handle it separately because JPA needs the
// correct java-type to build an expression. So String and numeric values JPA can do it by its own but not for classes like enums.
// So we need to transform the given value string into the enum class.
final Class<?> javaType = fieldPath.getJavaType();
if (javaType != null && javaType.isEnum()) {
return transformEnumValue(node, javaType, value);
@@ -327,7 +321,7 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
return convertFieldConverterValue(node, fieldName, value);
}
if (Boolean.TYPE.equals(javaType)) {
if (Boolean.TYPE.equals(javaType) || Boolean.class.equals(javaType)) {
return convertBooleanValue(node, javaType, value);
}
@@ -368,7 +362,7 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
@SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate toNotExistsSubQueryPredicate(final QuertPath queryField, final Path<?> fieldPath,
final Function<Expression<String>, Predicate> subQueryPredicateProvider) {
final Function<Path<String>, Predicate> subQueryPredicateProvider) {
// if a subquery the field's parent joins are not actually used
if (!inOr) {
// so, if not in or (hence not reused) we remove them. Parent shall be a Join
@@ -383,24 +377,27 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
.where(cb.and(
cb.equal(root.get(queryField.getEnumValue().identifierFieldName()),
subqueryRoot.get(queryField.getEnumValue().identifierFieldName())),
subQueryPredicateProvider.apply(getExpressionToCompare(queryField.getEnumValue(),
getFieldPath(subqueryRoot, queryField)))))));
subQueryPredicateProvider.apply(
getExpressionToCompare(queryField.getEnumValue(),
getFieldPath(subqueryRoot, queryField)))))));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Expression<String> getExpressionToCompare(final A enumField, final Path fieldPath) {
private Path<String> getExpressionToCompare(final A enumField, final Path fieldPath) {
if (!enumField.isMap()) {
return pathOfString(fieldPath);
}
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return (Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).value());
return (Path<String>) (((MapJoin<?, ?, ?>) fieldPath).value());
}
return enumField.getSubEntityMapTuple()
.map(Entry::getValue)
.map(valueFieldName -> fieldPath.<String> get(valueFieldName))
.orElseThrow(() -> new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed. Neither of those could be found!"));
.orElseThrow(() ->
new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed." +
" Neither of those could be found!"));
}
private String toSQL(final String transformedValue) {
@@ -437,33 +434,50 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
return children;
}
private Predicate equal(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.equal(caseWise(cb, expressionToCompare), caseWise(sqlValue));
private Predicate equal(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.equal(cb.upper(expressionToCompare), sqlValue.toUpperCase());
} else {
return cb.equal(expressionToCompare, sqlValue);
}
}
private Predicate notEqual(final Expression<String> expressionToCompare, String transformedValueStr) {
return cb.notEqual(caseWise(cb, expressionToCompare), caseWise(transformedValueStr));
private Predicate notEqual(final Path<String> expressionToCompare, String transformedValueStr) {
if (caseWise(expressionToCompare)) {
return cb.notEqual(cb.upper(expressionToCompare), transformedValueStr.toUpperCase());
} else {
return cb.notEqual(expressionToCompare, transformedValueStr);
}
}
private Predicate like(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.like(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
private Predicate like(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.like(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.like(expressionToCompare, sqlValue, ESCAPE_CHAR);
}
}
private Predicate notLike(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.notLike(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
private Predicate notLike(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.notLike(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.notLike(expressionToCompare, sqlValue, ESCAPE_CHAR);
}
}
private Predicate in(final Expression<String> expressionToCompare, final List<Object> transformedValues) {
final List<String> inParams = transformedValues.stream().filter(String.class::isInstance)
.map(String.class::cast).map(this::caseWise).collect(Collectors.toList());
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : caseWise(cb, expressionToCompare).in(inParams);
private Predicate in(final Path<String> expressionToCompare, final List<Object> transformedValues) {
if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) {
final List<String> inParams = transformedValues.stream()
.filter(String.class::isInstance)
.map(String.class::cast).map(String::toUpperCase).toList();
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : cb.upper(expressionToCompare).in(inParams);
} else {
return expressionToCompare.in(transformedValues);
}
}
private Expression<String> caseWise(final CriteriaBuilder cb, final Expression<String> expression) {
return ensureIgnoreCase ? cb.upper(expression) : expression;
}
private String caseWise(final String str) {
return ensureIgnoreCase ? str.toUpperCase() : str;
private boolean caseWise(final Path<?> fieldPath) {
return ensureIgnoreCase && fieldPath.getJavaType() == String.class;
}
}