/*
* Copyright 2015 JAXIO http://www.jaxio.com
*
* 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
*
* http://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 com.jaxio.jpa.querybyexample;
import org.hibernate.search.annotations.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.persistence.*;
import javax.persistence.criteria.*;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
/**
* JPA 2 {@link GenericRepository} implementation
*/
public abstract class GenericRepository<E extends Identifiable<PK>, PK extends Serializable> {
@Inject
protected ByExampleUtil byExampleUtil;
@Inject
protected ByPatternUtil byPatternUtil;
@Inject
protected ByRangeUtil byRangeUtil;
@Inject
protected ByNamedQueryUtil byNamedQueryUtil;
@Inject
protected ByPropertySelectorUtil byPropertySelectorUtil;
@Inject
protected OrderByUtil orderByUtil;
@Inject
protected MetamodelUtil metamodelUtil;
@Inject
private JpaUtil jpaUtil;
@Inject
protected ByFullTextUtil byFullTextUtil;
protected List<SingularAttribute<?, ?>> indexedAttributes;
@PersistenceContext
protected EntityManager entityManager;
protected Class<E> type;
protected Logger log;
protected String cacheRegion;
/*
* This constructor needs the real type of the generic type E so it can be given to the {@link javax.persistence.EntityManager}.
*/
public GenericRepository(Class<E> type) {
this.type = type;
this.log = LoggerFactory.getLogger(getClass());
this.cacheRegion = type.getCanonicalName();
}
@PostConstruct
protected void init() {
this.indexedAttributes = buildIndexedAttributes(type);
}
public Class<E> getType() {
return type;
}
/**
* Create a new instance of the repository type.
*
* @return a new instance with no property set.
*/
public abstract E getNew();
/**
* Creates a new instance and initializes it with some default values.
*
* @return a new instance initialized with default values.
*/
public E getNewWithDefaults() {
return getNew();
}
/**
* Gets from the repository the E entity instance.
* <p>
* DAO for the local database will typically use the primary key or unique fields of the given entity, while DAO for external repository may use a unique
* field present in the entity as they probably have no knowledge of the primary key. Hence, passing the entity as an argument instead of the primary key
* allows you to switch the DAO more easily.
*
* @param entity an E instance having a primary key set
* @return the corresponding E persistent instance or null if none could be found.
*/
@Transactional(readOnly = true)
public E get(E entity) {
return entity == null ? null : getById(entity.getId());
}
@Transactional(readOnly = true)
public E getById(PK pk) {
if (pk == null) {
return null;
}
E entityFound = entityManager.find(type, pk);
if (entityFound == null) {
log.warn("get returned null with id={}", pk);
}
return entityFound;
}
/**
* Refresh the given entity with up to date data. Does nothing if the given entity is a new entity (not yet managed).
*
* @param entity the entity to refresh.
*/
@Transactional(readOnly = true)
public void refresh(E entity) {
if (entityManager.contains(entity)) {
entityManager.refresh(entity);
}
}
/*
* Find and load all instances.
*/
@Transactional(readOnly = true)
public List<E> find() {
return find(getNew(), new SearchParameters());
}
/**
* Find and load a list of E instance.
*
* @param entity a sample entity whose non-null properties may be used as search hints
* @return the entities matching the search.
*/
@Transactional(readOnly = true)
public List<E> find(E entity) {
return find(entity, new SearchParameters());
}
/**
* Find and load a list of E instance.
*
* @param searchParameters carries additional search information
* @return the entities matching the search.
*/
@Transactional(readOnly = true)
public List<E> find(SearchParameters searchParameters) {
return find(getNew(), searchParameters);
}
/**
* Find and load a list of E instance.
*
* @param entity a sample entity whose non-null properties may be used as search hints
* @param sp carries additional search information
* @return the entities matching the search.
*/
@Transactional(readOnly = true)
public List<E> find(E entity, SearchParameters sp) {
if (sp.hasNamedQuery()) {
return byNamedQueryUtil.findByNamedQuery(sp);
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<E> criteriaQuery = builder.createQuery(type);
if (sp.getDistinct()) {
criteriaQuery.distinct(true);
}
Root<E> root = criteriaQuery.from(type);
// predicate
Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp);
if (predicate != null) {
criteriaQuery = criteriaQuery.where(predicate);
}
// fetches
fetches(sp, root);
// order by
criteriaQuery.orderBy(orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp));
TypedQuery<E> typedQuery = entityManager.createQuery(criteriaQuery);
applyCacheHints(typedQuery, sp);
jpaUtil.applyPagination(typedQuery, sp);
List<E> entities = typedQuery.getResultList();
log.debug("Returned {} elements", entities.size());
return entities;
}
/*
* Find a list of E property.
*
* @param propertyType type of the property
* @param entity a sample entity whose non-null properties may be used as search hints
* @param sp carries additional search information
* @param path the path to the property
* @return the entities property matching the search.
*/
@Transactional(readOnly = true)
public <T> List<T> findProperty(Class<T> propertyType, E entity, SearchParameters sp, String path) {
return findProperty(propertyType, entity, sp, metamodelUtil.toAttributes(path, type));
}
/*
* Find a list of E property.
*
* @param propertyType type of the property
* @param entity a sample entity whose non-null properties may be used as search hints
* @param sp carries additional search information
* @param attributes the list of attributes to the property
* @return the entities property matching the search.
*/
@Transactional(readOnly = true)
public <T> List<T> findProperty(Class<T> propertyType, E entity, SearchParameters sp, Attribute<?, ?>... attributes) {
return findProperty(propertyType, entity, sp, newArrayList(attributes));
}
/*
* Find a list of E property.
*
* @param propertyType type of the property
* @param entity a sample entity whose non-null properties may be used as search hints
* @param sp carries additional search information
* @param attributes the list of attributes to the property
* @return the entities property matching the search.
*/
@Transactional(readOnly = true)
public <T> List<T> findProperty(Class<T> propertyType, E entity, SearchParameters sp, List<Attribute<?, ?>> attributes) {
if (sp.hasNamedQuery()) {
return byNamedQueryUtil.findByNamedQuery(sp);
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> criteriaQuery = builder.createQuery(propertyType);
if (sp.getDistinct()) {
criteriaQuery.distinct(true);
}
Root<E> root = criteriaQuery.from(type);
Path<T> path = jpaUtil.getPath(root, attributes);
criteriaQuery.select(path);
// predicate
Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp);
if (predicate != null) {
criteriaQuery = criteriaQuery.where(predicate);
}
// fetches
fetches(sp, root);
// order by
// we do not want to follow order by specified in search parameters
criteriaQuery.orderBy(builder.asc(path));
TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
applyCacheHints(typedQuery, sp);
jpaUtil.applyPagination(typedQuery, sp);
List<T> entities = typedQuery.getResultList();
log.debug("Returned {} elements", entities.size());
return entities;
}
/**
* Count the number of E instances.
*
* @param sp carries additional search information
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findCount(SearchParameters sp) {
return findCount(getNew(), sp);
}
/**
* Count the number of E instances.
*
* @param entity a sample entity whose non-null properties may be used as search hint
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findCount(E entity) {
return findCount(entity, new SearchParameters());
}
/**
* Count the number of E instances.
*
* @param entity a sample entity whose non-null properties may be used as search hint
* @param sp carries additional search information
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findCount(E entity, SearchParameters sp) {
checkNotNull(entity, "The entity cannot be null");
checkNotNull(sp, "The searchParameters cannot be null");
if (sp.hasNamedQuery()) {
return byNamedQueryUtil.numberByNamedQuery(sp).intValue();
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = builder.createQuery(Long.class);
Root<E> root = criteriaQuery.from(type);
if (sp.getDistinct()) {
criteriaQuery = criteriaQuery.select(builder.countDistinct(root));
} else {
criteriaQuery = criteriaQuery.select(builder.count(root));
}
// predicate
Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp);
if (predicate != null) {
criteriaQuery = criteriaQuery.where(predicate);
}
// construct order by to fetch or joins if needed
orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp);
TypedQuery<Long> typedQuery = entityManager.createQuery(criteriaQuery);
applyCacheHints(typedQuery, sp);
return typedQuery.getSingleResult().intValue();
}
/**
* Count the number of E instances.
*
* @param entity a sample entity whose non-null properties may be used as search hint
* @param sp carries additional search information
* @param path the path to the property
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findPropertyCount(E entity, SearchParameters sp, String path) {
return findPropertyCount(entity, sp, metamodelUtil.toAttributes(path, type));
}
/**
* Count the number of E instances.
*
* @param entity a sample entity whose non-null properties may be used as search hint
* @param sp carries additional search information
* @param attributes the list of attributes to the property
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findPropertyCount(E entity, SearchParameters sp, Attribute<?, ?>... attributes) {
return findPropertyCount(entity, sp, newArrayList(attributes));
}
/**
* Count the number of E instances.
*
* @param entity a sample entity whose non-null properties may be used as search hint
* @param sp carries additional search information
* @param attributes the list of attributes to the property
* @return the number of entities matching the search.
*/
@Transactional(readOnly = true)
public int findPropertyCount(E entity, SearchParameters sp, List<Attribute<?, ?>> attributes) {
if (sp.hasNamedQuery()) {
return byNamedQueryUtil.numberByNamedQuery(sp).intValue();
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = builder.createQuery(Long.class);
Root<E> root = criteriaQuery.from(type);
Path<?> path = jpaUtil.getPath(root, attributes);
if (sp.getDistinct()) {
criteriaQuery = criteriaQuery.select(builder.countDistinct(path));
} else {
criteriaQuery = criteriaQuery.select(builder.count(path));
}
// predicate
Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp);
if (predicate != null) {
criteriaQuery = criteriaQuery.where(predicate);
}
// construct order by to fetch or joins if needed
orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp);
TypedQuery<Long> typedQuery = entityManager.createQuery(criteriaQuery);
applyCacheHints(typedQuery, sp);
return typedQuery.getSingleResult().intValue();
}
@Transactional(readOnly = true)
public E findUnique(SearchParameters sp) {
return findUnique(getNew(), sp);
}
@Transactional(readOnly = true)
public E findUnique(E e) {
return findUnique(e, new SearchParameters());
}
@Transactional(readOnly = true)
public E findUnique(E entity, SearchParameters sp) {
E result = findUniqueOrNone(entity, sp);
if (result != null) {
return result;
}
throw new NoResultException("Developper: You expected 1 result but found none !");
}
@Transactional(readOnly = true)
public E findUniqueOrNone(SearchParameters sp) {
return findUniqueOrNone(getNew(), sp);
}
@Transactional(readOnly = true)
public E findUniqueOrNone(E entity) {
return findUniqueOrNone(entity, new SearchParameters());
}
/*
* We request at most 2, if there's more than one then we throw a {@link javax.persistence.NonUniqueResultException}
*
* @throws javax.persistence.NonUniqueResultException
*/
@Transactional(readOnly = true)
public E findUniqueOrNone(E entity, SearchParameters sp) {
// this code is an optimization to prevent using a count
sp.setFirst(0);
sp.setMaxResults(2);
List<E> results = find(entity, sp);
if (results == null || results.isEmpty()) {
return null;
} else if (results.size() > 1) {
throw new NonUniqueResultException("Developper: You expected 1 result but we found more ! sample: " + entity);
} else {
return results.iterator().next();
}
}
@Transactional(readOnly = true)
public E findUniqueOrNew(SearchParameters sp) {
return findUniqueOrNew(getNew(), sp);
}
@Transactional(readOnly = true)
public E findUniqueOrNew(E e) {
return findUniqueOrNew(e, new SearchParameters());
}
@Transactional(readOnly = true)
public E findUniqueOrNew(E entity, SearchParameters sp) {
E result = findUniqueOrNone(entity, sp);
if (result != null) {
return result;
} else {
return getNewWithDefaults();
}
}
protected <R> Predicate getPredicate(CriteriaQuery<?> criteriaQuery, Root<E> root, CriteriaBuilder builder, E entity, SearchParameters sp) {
return jpaUtil.andPredicate(builder, //
bySearchPredicate(root, builder, entity, sp), //
byMandatoryPredicate(criteriaQuery, root, builder, entity, sp));
}
protected <R> Predicate bySearchPredicate(Root<E> root, CriteriaBuilder builder, E entity, SearchParameters sp) {
return jpaUtil.concatPredicate(sp, builder, //
byFullText(root, builder, sp, entity, indexedAttributes), //
byRanges(root, builder, sp, type), //
byPropertySelectors(root, builder, sp), //
byExample(root, builder, sp, entity), //
byPattern(root, builder, sp, type));
}
protected <T extends Identifiable<?>> Predicate byFullText(Root<T> root, CriteriaBuilder builder, SearchParameters sp, T entity,
List<SingularAttribute<?, ?>> indexedAttributes) {
return byFullTextUtil.byFullText(root, builder, sp, entity, indexedAttributes);
}
protected Predicate byExample(Root<E> root, CriteriaBuilder builder, SearchParameters sp, E entity) {
return byExampleUtil.byExampleOnEntity(root, entity, builder, sp);
}
protected Predicate byPropertySelectors(Root<E> root, CriteriaBuilder builder, SearchParameters sp) {
return byPropertySelectorUtil.byPropertySelectors(root, builder, sp);
}
protected Predicate byRanges(Root<E> root, CriteriaBuilder builder, SearchParameters sp, Class<E> type) {
return byRangeUtil.byRanges(root, builder, sp, type);
}
protected Predicate byPattern(Root<E> root, CriteriaBuilder builder, SearchParameters sp, Class<E> type) {
return byPatternUtil.byPattern(root, builder, sp, type);
}
/*
* You may override this method to add a Predicate to the default find method.
*/
protected <R> Predicate byMandatoryPredicate(CriteriaQuery<?> criteriaQuery, Root<E> root, CriteriaBuilder builder, E entity, SearchParameters sp) {
return null;
}
/**
* Save or update the given entity E to the repository. Assume that the entity is already present in the persistence context. No merge is done.
*
* @param entity the entity to be saved or updated.
*/
@Transactional
public void save(E entity) {
checkNotNull(entity, "The entity to save cannot be null");
// creation with auto generated id
if (!entity.isIdSet()) {
entityManager.persist(entity);
return;
}
// creation with manually assigned key
if (jpaUtil.isEntityIdManuallyAssigned(type) && !entityManager.contains(entity)) {
entityManager.persist(entity);
return;
}
// other cases are update
// the simple fact to invoke this method, from a service method annotated with @Transactional,
// does the job (assuming the give entity is present in the persistence context)
}
/*
* Persist the given entity.
*/
@Transactional
public void persist(E entity) {
entityManager.persist(entity);
}
/*
* Merge the state of the given entity into the current persistence context.
*/
@Transactional
public E merge(E entity) {
return entityManager.merge(entity);
}
/*
* Delete the given entity E from the repository.
*
* @param entity the entity to be deleted.
*/
@Transactional
public void delete(E entity) {
if (entityManager.contains(entity)) {
entityManager.remove(entity);
} else {
// could be a delete on a transient instance
E entityRef = entityManager.getReference(type, entity.getId());
if (entityRef != null) {
entityManager.remove(entityRef);
} else {
log.warn("Attempt to delete an instance that is not present in the database: {}", entity);
}
}
}
protected List<SingularAttribute<?, ?>> buildIndexedAttributes(Class<E> type) {
List<SingularAttribute<?, ?>> ret = newArrayList();
for (Method m : type.getMethods()) {
if (m.getAnnotation(Field.class) != null) {
ret.add(metamodelUtil.toAttribute(jpaUtil.methodToProperty(m), type));
}
}
return ret;
}
public boolean isIndexed(String property) {
return !property.contains(".") && indexedAttributes.contains(metamodelUtil.toAttribute(property, type));
}
// -----------------
// Util
// -----------------
/*
* Helper to determine if the passed given property is null. Used mainly on binary lazy loaded property.
*
* @param id the entity id
* @param property the property to check
*/
@Transactional(readOnly = true)
public boolean isPropertyNull(PK id, SingularAttribute<E, ?> property) {
checkNotNull(id, "The id cannot be null");
checkNotNull(property, "The property cannot be null");
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = builder.createQuery(Long.class);
Root<E> root = criteriaQuery.from(type);
criteriaQuery = criteriaQuery.select(builder.count(root));
// predicate
Predicate idPredicate = builder.equal(root.get("id"), id);
Predicate isNullPredicate = builder.isNull(root.get(property));
criteriaQuery = criteriaQuery.where(jpaUtil.andPredicate(builder, idPredicate, isNullPredicate));
TypedQuery<Long> typedQuery = entityManager.createQuery(criteriaQuery);
return typedQuery.getSingleResult().intValue() == 1;
}
/*
* Return the optimistic version value, if any.
*/
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public Comparable<Object> getVersion(E entity) {
EntityType<E> entityType = entityManager.getMetamodel().entity(type);
if (!entityType.hasVersionAttribute()) {
return null;
}
return (Comparable<Object>) jpaUtil.getValue(entity, getVersionAttribute(entityType));
}
/*
* _HACK_ too bad that JPA does not provide this entityType.getVersion();
* <p>
* http://stackoverflow.com/questions/13265094/generic-way-to-get-jpa-entity-version
*/
protected SingularAttribute<? super E, ?> getVersionAttribute(EntityType<E> entityType) {
for (SingularAttribute<? super E, ?> sa : entityType.getSingularAttributes()) {
if (sa.isVersion()) {
return sa;
}
}
return null;
}
// -----------------
// Commons
// -----------------
/*
* Set hints for 2d level cache.
*/
protected void applyCacheHints(TypedQuery<?> typedQuery, SearchParameters sp) {
if (sp.isCacheable()) {
typedQuery.setHint("org.hibernate.cacheable", true);
if (sp.hasCacheRegion()) {
typedQuery.setHint("org.hibernate.cacheRegion", sp.getCacheRegion());
} else {
typedQuery.setHint("org.hibernate.cacheRegion", cacheRegion);
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
protected void fetches(SearchParameters sp, Root<E> root) {
for (List<Attribute<?, ?>> args : sp.getFetches()) {
FetchParent<?, ?> from = root;
for (Attribute<?, ?> arg : args) {
boolean found = false;
for (Fetch<?, ?> fetch : from.getFetches()) {
if (arg.equals(fetch.getAttribute())) {
from = fetch;
found = true;
break;
}
}
if (!found) {
if (arg instanceof PluralAttribute) {
from = from.fetch((PluralAttribute) arg, JoinType.LEFT);
} else {
from = from.fetch((SingularAttribute) arg, JoinType.LEFT);
}
}
}
}
}
}