org.omnifaces.persistence.JPA Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of omnipersistence Show documentation
Show all versions of omnipersistence Show documentation
Utilities for JPA, JDBC and DataSources
The newest version!
/*
* Copyright 2021 OmniFaces
*
* 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.omnifaces.persistence;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.omnifaces.persistence.Database.POSTGRESQL;
import static org.omnifaces.persistence.Provider.HIBERNATE;
import static org.omnifaces.utils.stream.Collectors.toMap;
import static org.omnifaces.utils.stream.Streams.stream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.enterprise.inject.Typed;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.NoResultException;
import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.CollectionAttribute;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.ListAttribute;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SetAttribute;
import jakarta.persistence.metamodel.SingularAttribute;
import org.omnifaces.persistence.criteria.Numeric;
/**
* JPA utilities.
*/
@Typed
public final class JPA {
// Public constants -------------------------------------------------------------------------------------------------------------------
public static final String QUERY_HINT_LOAD_GRAPH = "jakarta.persistence.loadgraph";
public static final String QUERY_HINT_FETCH_GRAPH = "jakarta.persistence.fetchgraph";
public static final String QUERY_HINT_CACHE_STORE_MODE = "jakarta.persistence.cache.storeMode"; // USE | BYPASS | REFRESH
public static final String QUERY_HINT_CACHE_RETRIEVE_MODE = "jakarta.persistence.cache.retrieveMode"; // USE | BYPASS
public static final String PROPERTY_VALIDATION_MODE = "jakarta.persistence.validation.mode"; // AUTO | CALLBACK | NONE
// Constructors -----------------------------------------------------------------------------------------------------------------------
private JPA() {
throw new AssertionError();
}
// Configuration utils ----------------------------------------------------------------------------------------------------------------
/**
* Returns the currently configured bean validation mode for given entity manager.
* This consults the {@value #PROPERTY_VALIDATION_MODE} property in persistence.xml
.
* @param entityManager The involved entity manager.
* @return The currently configured bean validation mode.
*/
public static ValidationMode getValidationMode(EntityManager entityManager) {
Object validationMode = entityManager.getEntityManagerFactory().getProperties().get(PROPERTY_VALIDATION_MODE);
return validationMode != null ? ValidationMode.valueOf(validationMode.toString().toUpperCase()) : ValidationMode.AUTO;
}
// Query utils ------------------------------------------------------------------------------------------------------------------------
/**
* Returns single result of given typed query as {@link Optional}.
* @param The generic result type.
* @param typedQuery The involved typed query.
* @return Single result of given typed query as {@link Optional}.
* @throws NonUniqueResultException When there is no unique result.
*/
public static Optional getOptionalSingleResult(TypedQuery typedQuery) {
return ofNullable(getSingleResultOrNull(typedQuery));
}
/**
* Returns single result of given query as {@link Optional}.
* @param The expected result type.
* @param query The involved query.
* @return Single result of given query as {@link Optional}.
* @throws NonUniqueResultException When there is no unique result.
* @throws ClassCastException When T
is of wrong type.
*/
public static Optional getOptionalSingleResult(Query query) {
return ofNullable(getSingleResultOrNull(query));
}
/**
* Returns single result of given typed query, or null
if there is none.
* @param The generic result type.
* @param typedQuery The involved typed query.
* @return Single result of given typed query, or null
if there is none.
* @throws NonUniqueResultException When there is no unique result.
*/
public static T getSingleResultOrNull(TypedQuery typedQuery) {
try {
return typedQuery.getSingleResult();
}
catch (NoResultException e) {
return null;
}
}
/**
* Returns single result of given query, or null
if there is none.
* @param The expected result type.
* @param query The involved query.
* @return Single result of given query, or null
if there is none.
* @throws NonUniqueResultException When there is no unique result.
* @throws ClassCastException When T
is of wrong type.
*/
@SuppressWarnings("unchecked")
public static T getSingleResultOrNull(Query query) {
try {
return (T) query.getSingleResult();
}
catch (NoResultException e) {
return null;
}
}
/**
* Returns first result of given typed query as {@link Optional}.
* The difference with {@link #getOptionalSingleResult(TypedQuery)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
* @param The generic result type.
* @param typedQuery The involved typed query.
* @return First result of given typed query as {@link Optional}.
*/
public static Optional getOptionalFirstResult(TypedQuery typedQuery) {
typedQuery.setMaxResults(1);
return typedQuery.getResultList().stream().findFirst();
}
/**
* Returns first result of given query as {@link Optional}.
* The difference with {@link #getOptionalSingleResult(Query)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
* @param The expected result type.
* @param query The involved query.
* @return First result of given query as {@link Optional}.
* @throws ClassCastException When T
is of wrong type.
*/
@SuppressWarnings("unchecked")
public static Optional getOptionalFirstResult(Query query) {
query.setMaxResults(1);
return query.getResultList().stream().findFirst();
}
/**
* Returns first result of given typed query, or null
if there is none.
* The difference with {@link #getSingleResultOrNull(TypedQuery)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
* @param The generic result type.
* @param typedQuery The involved typed query.
* @return First result of given typed query, or null
if there is none.
*/
public static T getFirstResultOrNull(TypedQuery typedQuery) {
return getOptionalFirstResult(typedQuery).orElse(null);
}
/**
* Returns first result of given query, or null
if there is none.
* The difference with {@link #getSingleResultOrNull(Query)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
* @param The expected result type.
* @param query The involved query.
* @return First result of given query, or null
if there is none.
* @throws ClassCastException When T
is of wrong type.
*/
@SuppressWarnings("unchecked")
public static T getFirstResultOrNull(Query query) {
return (T) getOptionalFirstResult(query).orElse(null);
}
/**
* Returns the result list of given typed query as a map mapped by the given key mapper.
* @param The generic map key type.
* @param The generic result type, also map value type.
* @param typedQuery The involved typed query.
* @param keyMapper The key mapper.
* @return The result list of given typed query as a map mapped by the given key mapper.
*/
public static Map getResultMap(TypedQuery typedQuery, Function super T, ? extends K> keyMapper) {
return typedQuery.getResultList().stream().collect(toMap(keyMapper));
}
/**
* Returns the result list of given typed query as a map mapped by the given key and value mappers.
* @param The generic map key type.
* @param The generic result type.
* @param The generic map value type.
* @param typedQuery The involved typed query.
* @param keyMapper The key mapper.
* @param valueMapper The value mapper.
* @return The result list of given typed query as a map mapped by the given key and value mappers.
*/
public static Map getResultMap(TypedQuery typedQuery, Function super T, ? extends K> keyMapper, Function super T, ? extends V> valueMapper) {
return typedQuery.getResultList().stream().collect(Collectors.toMap(keyMapper, valueMapper));
}
// Entity utils -----------------------------------------------------------------------------------------------------------------------
/**
* Returns count of all foreign key references to entity of given entity type with given ID of given identifier type.
* This is particularly useful in case you intend to check if the given entity is still referenced elsewhere in database.
* @param The generic result type.
* @param The generic identifier type.
* @param entityManager The involved entity manager.
* @param entityType Entity type.
* @param identifierType Identifier type.
* @param id Entity ID.
* @return Count of all foreign key references to entity of given entity type with given ID of given identifier type.
*/
public static long countForeignKeyReferences(EntityManager entityManager, Class entityType, Class identifierType, I id) {
Metamodel metamodel = entityManager.getMetamodel();
SingularAttribute super T, I> idAttribute = metamodel.entity(entityType).getId(identifierType);
return metamodel.getEntities().stream()
.flatMap(entity -> getAttributesOfType(entity, entityType))
.distinct()
.mapToLong(attribute -> countReferencesTo(entityManager, attribute, idAttribute, id))
.sum();
}
private static Stream> getAttributesOfType(EntityType entity, Class entityType) {
return entity.getAttributes().stream()
.filter(attribute -> entityType.equals(getJavaType(attribute)))
.map(attribute -> attribute);
}
private static Class> getJavaType(Attribute super E, ?> attribute) {
return (attribute instanceof PluralAttribute)
? ((PluralAttribute, ?, ?>) attribute).getElementType().getJavaType()
: attribute.getJavaType();
}
@SuppressWarnings("unchecked")
private static Long countReferencesTo(EntityManager entityManager, Attribute attribute, SingularAttribute super T, I> idAttribute, I id) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery query = criteriaBuilder.createQuery(Long.class);
Root root = query.from(attribute.getDeclaringType().getJavaType());
Join join;
if (attribute instanceof SingularAttribute) {
join = root.join((SingularAttribute) attribute);
}
else if (attribute instanceof ListAttribute) {
join = root.join((ListAttribute) attribute);
}
else if (attribute instanceof SetAttribute) {
join = root.join((SetAttribute) attribute);
}
else if (attribute instanceof MapAttribute) {
join = root.join((MapAttribute) attribute);
}
else if (attribute instanceof CollectionAttribute) {
join = root.join((CollectionAttribute) attribute);
}
else {
return 0L; // Unknown attribute type, just return 0.
}
query.select(criteriaBuilder.count(root)).where(criteriaBuilder.equal(join.get(idAttribute), id));
return entityManager.createQuery(query).getSingleResult();
}
// Criteria utils ---------------------------------------------------------------------------------------------------------------------
/**
* Returns a SQL CONCAT(...) of given expressions or strings.
* @param builder The involved criteria builder.
* @param expressionsOrStrings Expressions or Strings.
* @return A SQL CONCAT(...) of given expressions or strings.
* @throws IllegalArgumentException When there are less than 2 expressions or strings. There's no point of concat then.
*/
public static Expression concat(CriteriaBuilder builder, Object... expressionsOrStrings) {
if (expressionsOrStrings.length < 2) {
throw new IllegalArgumentException("There must be at least 2 expressions or strings");
}
List> expressions = stream(expressionsOrStrings).map(expressionOrString -> {
if (expressionOrString instanceof Expression) {
return castAsString(builder, (Expression>) expressionOrString);
}
else {
return builder.literal(expressionOrString);
}
}).collect(toList());
return builder.function("CONCAT", String.class, expressions.toArray(new Expression[expressions.size()]));
}
/**
* Returns a new expression wherein given expression is cast as String.
* This covers known problems with certain providers and/or databases.
* @param builder The involved criteria builder.
* @param expression Expression to be cast as String.
* @return A new expression wherein given expression is cast as String.
*/
@SuppressWarnings("unchecked")
public static Expression castAsString(CriteriaBuilder builder, Expression> expression) {
if (Provider.is(HIBERNATE)) {
return expression.as(String.class);
}
// EclipseLink and OpenJPA have a broken Expression#as() implementation, need to delegate to DB specific function.
// PostgreSQL is quite strict in string casting, it has to be performed explicitly.
if (Database.is(POSTGRESQL)) {
String pattern = null;
if (Numeric.is(expression.getJavaType())) {
pattern = "FM999999999999999999"; // NOTE: Amount of digits matches amount of Long.MAX_VALUE digits minus one.
}
else if (LocalDate.class.isAssignableFrom(expression.getJavaType())) {
pattern = "YYYY-MM-DD";
}
else if (LocalTime.class.isAssignableFrom(expression.getJavaType())) {
pattern = "HH24:MI:SS";
}
else if (LocalDateTime.class.isAssignableFrom(expression.getJavaType())) {
pattern = "YYYY-MM-DD'T'HH24:MI:SS'Z'"; // NOTE: PostgreSQL uses ISO_INSTANT instead of ISO_LOCAL_DATE_TIME.
}
else if (OffsetTime.class.isAssignableFrom(expression.getJavaType())) {
pattern = "HH24:MI:SS-OF"; // TODO: PostgreSQL doc says it's not supported?
}
else if (OffsetDateTime.class.isAssignableFrom(expression.getJavaType())) {
pattern = "YYYY-MM-DD'T'HH24:MI:SS-OF";
}
if (pattern != null) {
return builder.function("TO_CHAR", String.class, expression, builder.literal(pattern));
}
}
// H2 and MySQL are more lenient in this, they can do implicit casting with sane defaults, so no custom function call necessary.
return (Expression) expression;
}
/**
* Returns whether given path is {@link Enumerated} by {@link EnumType#ORDINAL}.
* @param path Path of interest.
* @return Whether given path is {@link Enumerated} by {@link EnumType#ORDINAL}.
*/
public static boolean isEnumeratedByOrdinal(Path> path) {
Bindable> model = path.getModel();
if (model instanceof Attribute) {
Member member = ((Attribute, ?>) model).getJavaMember();
if (member instanceof AnnotatedElement) {
Enumerated enumerated = ((AnnotatedElement) member).getAnnotation(Enumerated.class);
if (enumerated != null) {
return enumerated.value() == EnumType.ORDINAL;
}
}
}
return false;
}
}