All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.gc.aafc.dina.jpa.BaseDAO Maven / Gradle / Ivy

There is a newer version: 0.132
Show newest version
package ca.gc.aafc.dina.jpa;

import io.crnk.core.engine.information.bean.BeanInformation;
import lombok.NonNull;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hibernate.Session;
import org.hibernate.SimpleNaturalIdLoadAccess;
import org.hibernate.annotations.NaturalId;
import org.springframework.stereotype.Component;

import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceUnitUtil;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Base Data Access Object layer. This class should be the only one holding a
 * reference to the {@link EntityManager}.
 *
 */
@Component
public class BaseDAO {

  public static final String LOAD_GRAPH_HINT_KEY = "javax.persistence.loadgraph";

  public static final int DEFAULT_LIMIT = 100;
  
  @PersistenceContext
  private EntityManager entityManager;

  /**
   * This method can be used to inject the EntityManager into an external object.
   *
   * @param creator
   */
  public  T createWithEntityManager(Function creator) {
    Objects.requireNonNull(creator);
    return creator.apply(entityManager);
  }

  /**
   * Used to call the provided PredicateSupplier with the EntityManager.
   * @param where
   * @param criteriaBuilder
   * @param root
   * @param 
   * @return
   */
  public  Predicate[] buildPredicateFromSupplier(PredicateSupplier where, CriteriaBuilder criteriaBuilder, Root root) {
    return where.supply(criteriaBuilder, root, entityManager);
  }

  /**
   * Utility function that can check if a lazy loaded attribute is actually
   * loaded.
   *
   * @param entity
   * @param fieldName
   * @return
   */
  public Boolean isLoaded(Object entity, String fieldName) {
    Objects.requireNonNull(entity);
    Objects.requireNonNull(fieldName);

    PersistenceUnitUtil unitUtil = entityManager.getEntityManagerFactory().getPersistenceUnitUtil();
    return unitUtil.isLoaded(entity, fieldName);
  }

  /**
   * Creates an EntityGraph for the specified entity class with the given attribute nodes.
   *
   * This method constructs an EntityGraph for the provided entity class and populates it
   * with the specified attribute nodes. The attribute nodes can represent either single-level 
   * attributes or nested attributes in a dot-separated format. If an attribute node contains a dot, 
   * it will be treated as a nested attribute, and a subgraph will be created.
   *
   * @param entityClass     The class of the entity for which the EntityGraph is constructed.
   * @param attributeNodes  The attribute nodes to be included in the EntityGraph.
   *                        The attribute nodes can be either single-level attributes or nested attributes
   *                        represented in a dot-separated format (e.g., "attributeName" or "nestedEntity.attributeName").
   * @return An EntityGraph for the specified entity class with the given attribute nodes.
   */
  public  EntityGraph createEntityGraph(Class entityClass, String... attributeNodes) {
    EntityGraph graph = entityManager.createEntityGraph(entityClass);
    for (String attribute : attributeNodes) {
      if (attribute.contains(".")) {
        String[] parts = StringUtils.split(attribute, ".", 2);

        if (parts.length == 2) {
          graph.addSubgraph(parts[0]).addAttributeNodes(parts[1]);
        }
      } else {
        graph.addAttributeNodes(attribute);
      }
    }
    return graph;
  }

  /**
   * Find a POJO/scalar(class that is not necessary an entity) Projection from a query.
   * @param typeClass class of the result
   * @param sql sql query. Usually a jpql query.
   * @param parameters optional parameters for the query
   * @return the POJO/Scalar or null if not found
   */
  public  T findOneByQuery(Class typeClass, String sql,
                              List> parameters) {
    TypedQuery tq = entityManager.createQuery(sql, typeClass);
    if (parameters != null) {
      for (Pair param : parameters) {
        tq.setParameter(param.getKey(), param.getValue());
      }
    }
    try {
      return tq.getSingleResult();
    } catch (NoResultException nrEx) {
      return null;
    }
  }

  /**
   * Find a list of POJO/scalar(class that is not necessary an entity) Projection from a query.
   * @param typeClass class of the result
   * @param sql sql query. Usually a jpql query.
   * @param parameters optional parameters for the query
   * @return the list of POJO/Scalar or null if not found
   */
  public  List findAllByQuery(Class typeClass, String sql,
                              List> parameters) {
    return findAllByQuery(typeClass, sql, parameters, -1, -1);
  }

  /**
   * Find a list of POJO/scalar(class that is not necessary an entity) Projection from a query.
   * @param typeClass class of the result
   * @param sql sql query. Usually a jpql query.
   * @param parameters optional parameters for the query
   * @param limit optional parameters to limit the page size.
   * @param offset optional parameters to set the page offset. If used make sure the query includes an ORDER by.
   * @return the list of POJO/Scalar or null if nothing found
   */
  public  List findAllByQuery(Class typeClass, String sql,
                                    List> parameters, int limit, int offset) {
    TypedQuery tq = entityManager.createQuery(sql, typeClass);
    if (parameters != null) {
      for (Pair param : parameters) {
        tq.setParameter(param.getKey(), param.getValue());
      }
    }

    // greater than 10 000 is stream should be used
    if (limit > 0 && limit < 10_000) {
      tq.setMaxResults(limit);
    }

    if (offset > 0) {
      tq.setFirstResult(offset);
    }

    try {
      return tq.getResultList();
    } catch (NoResultException nrEx) {
      return null;
    }
  }

  /**
   * Find an entity by its primary key.
   *
   * @param id
   * @param entityClass
   * @return
   */
  public  T findOneByDatabaseId(Object id, Class entityClass) {
    return entityManager.find(entityClass, id);
  }

  /**
   * Find an entity by its {@link NaturalId}. The method assumes that the
   * naturalId is unique.
   *
   * @param id
   * @param entityClass
   * @return
   */
  public  T findOneByNaturalId(Object id, Class entityClass) {
    Session session = entityManager.unwrap(Session.class);
    return session.bySimpleNaturalId(entityClass).load(id);
  }

  /**
   * Find an entity by its {@link NaturalId}. The method assumes that the
   * naturalId is unique.
   *
   * @param id
   * @param entityClass
   * @param hints
   * @return
   */
  public  T findOneByNaturalId(Object id, Class entityClass, Map hints) {

    // Hibernate 5 doesn't support hint on natural id, so we are creating a real query
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery criteria = criteriaBuilder.createQuery(entityClass);
    Root root = criteria.from(entityClass);

    criteria.where(criteriaBuilder.equal(root.get(getNaturalIdFieldName(entityClass)), id));
    criteria.select(root);

    TypedQuery query = entityManager.createQuery(criteria);
    if (hints != null) {
      hints.forEach(query::setHint);
    }
    try {
      return query.getSingleResult();
    } catch (NoResultException nrEx) {
      return null;
    }
  }

  /**
   * Find an entity by a specific property. The method assumes that the property
   * is unique.
   *
   * @param clazz
   * @param property
   * @param value
   * @return the entity or null if not found
   */
  public  T findOneByProperty(Class clazz, String property, Object value) {
    // Create a criteria to retrieve the specific property.
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery criteria = criteriaBuilder.createQuery(clazz);
    Root root = criteria.from(clazz);

    criteria.where(criteriaBuilder.equal(root.get(property), value));
    criteria.select(root);

    TypedQuery query = entityManager.createQuery(criteria);
    try {
      return query.getSingleResult();
    } catch (NoResultException nrEx) {
      return null;
    }
  }

  /**
   * Find an entity by specific properties. The method assumes that the properties
   * are part of a unique constraint (that the query will return nothing or 1 result but never more).
   * @param clazz
   * @param propertiesAndValue
   * @param 
   * @return
   */
  public  T findOneByProperties(Class clazz, List> propertiesAndValue) {
    // Create a criteria to retrieve the specific property.
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery criteria = criteriaBuilder.createQuery(clazz);
    Root root = criteria.from(clazz);

    Predicate whereClause =
        PredicateHelper.appendPropertiesEqual(null, criteriaBuilder, root, propertiesAndValue);

    criteria.where(whereClause);
    criteria.select(root);
    TypedQuery query = entityManager.createQuery(criteria);
    try {
      return query.getSingleResult();
    } catch (NoResultException nrEx) {
      return null;
    }

  }

  /**
   * Find one or more entity by a specific property. The number of records returned is limited
   * to {@link #DEFAULT_LIMIT}.
   *
   * @param clazz
   * @param property
   * @param value
   * @return list of entities or empty list if nothing is found
   */
  public  List findByProperty(Class clazz, String property, Object value) {
    // Create a criteria to retrieve the specific property.
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery criteria = criteriaBuilder.createQuery(clazz);
    Root root = criteria.from(clazz);

    criteria.where(criteriaBuilder.equal(root.get(property), value));
    criteria.select(root);

    TypedQuery query = entityManager.createQuery(criteria);
    return query.setMaxResults(DEFAULT_LIMIT).getResultList();
  }

  /**
   * Check for the existence of a record based on a property and a value
   *
   * @param clazz
   * @param property
   * @param value
   * @param 
   * @return
   */
  public  boolean existsByProperty(Class clazz, String property, Object value) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery(Integer.class);
    Root from = cq.from(clazz);

    cq.select(cb.literal(1))
      .where(
        cb.equal(
          from.get(property),
            value))
        .from(clazz);

    TypedQuery tq = entityManager.createQuery(cq);
    return !tq.getResultList().isEmpty();
  }

  /**
   * Check for the existence of a record by natural id.
   *
   * @param naturalId
   * @param entityClass
   * @return
   */
  public  boolean existsByNaturalId(
      @NonNull Object naturalId,
      @NonNull Class entityClass
  ) {
    return existsByProperty(entityClass, getNaturalIdFieldName(entityClass), naturalId);
  }

  /**
   * Returns a reference to an entity that should exist without actually loading it. Useful to set
   * relationships without loading the entity.
   *
   * @param entityClass
   * @param naturalId
   * @return
   */
  public  T getReferenceByNaturalId(Class entityClass, Object naturalId) {
    SimpleNaturalIdLoadAccess loadAccess = entityManager.unwrap(Session.class)
        .bySimpleNaturalId(entityClass);
    return loadAccess.getReference(naturalId);
  }

  /**
   * Set a relationship by calling the provided {@link Consumer} with a reference Entity loaded by
   * NaturalId. A reference to the entity allows to set a foreign key without loading the other entity.
   *
   * Usage:
   *
   * Using the object 'dep', set the relationship to DepartmentType using only its NaturalId (depTypeUUID).
   * baseDAO.setRelationshipByNaturalIdReference(DepartmentType.class, depTypeUUID,
        (x) -> dep.setDepartmentType(x));
   *
   * @param entityClass entity to link to that will be loaded with a reference entity
   * @param naturalId value
   * @param objConsumer
   */
  public  void setRelationshipByNaturalIdReference(Class entityClass, Object naturalId, Consumer objConsumer) {
    objConsumer.accept(getReferenceByNaturalId(entityClass, naturalId));
  }

  /**
   * Save the provided entity.
   *
   * @param entity
   */
  public void create(Object entity) {
    create(entity, false);
  }

  /**
   * Save the provided entity.
   *
   * @param entity
   * @param flush should the changes made in the current transaction be flushed immediately to the database
   */
  public void create(Object entity, boolean flush) {
    entityManager.persist(entity);
    if (flush) {
      entityManager.flush();
    }
  }

  /**
   * Merge the state of a given entity into the current persistence context.
   *
   * @param     Type of the entity
   * @param entity entity to update
   * @return returns the managed instance the state was merged to.
   */
  public  E update(E entity) {
    E result = entityManager.merge(entity);
    // Flush here to throw any validation errors:
    entityManager.flush();
    return result;
  }

  /**
   * Delete the provided entity.
   *
   * @param entity
   */
  public void delete(Object entity) {
    entityManager.remove(entity);
  }

  /**
   * Given a class, this method will extract the name of the field annotated with {@link NaturalId}.
   *
   * @param entityClass
   * @return
   */
  public String getNaturalIdFieldName(Class entityClass) {
    BeanInformation beanInfo = BeanInformation.get(entityClass);
    // Check for NaturalId:
    for (String attrName : beanInfo.getAttributeNames()) {
      if (beanInfo.getAttribute(attrName).getAnnotation(NaturalId.class).isPresent()) {
        return attrName;
      }
    }
    return null;
  }

  /**
   * Refresh an entity from the database.
   * This will revert any non-flushed changes made in the current transaction to the entity, and refresh its state to what is currently defined on the database.
   * @param entity
   */
  public void refresh(Object entity) {
    entityManager.refresh(entity);
  }

  /**
   * Force a flush to the database.
   * This is usually not necessary unless it is important to flush changes in the current transaction
   * at a very specific moment. Otherwise, the transaction will automatically flush at the end.
   */
  public void flush() {
    entityManager.flush();
  }

  /**
   * Remove the given entity from the persistence context, causing a managed entity to become detached.
   * This will revert any non-flushed changes made in the current transaction to the entity.
   *
   * Also used to remove the object from the first-level cache.
   * @param entity
   */
  public void detach(Object entity) {
    entityManager.detach(entity);
  }

  /**
   * Given a class, this method will return the name of the field annotated with {@link Id}.
   *
   * @param entityClass
   * @return
   */
  public String getDatabaseIdFieldName(Class entityClass) {
    return entityManager.getMetamodel()
        .entity(entityClass)
        .getId(Serializable.class)
        .getName();
  }

  /**
   * returns a {@link CriteriaBuilder} for the creation of {@link CriteriaQuery},
   * {@link Predicate}, {@link Expression}, and compound selections.
   *
   * @return {@link CriteriaBuilder}
   */
  public CriteriaBuilder getCriteriaBuilder() {
    return entityManager.getCriteriaBuilder();
  }

  /**
   * Returns a List of entities based off a given criteria.
   *
   * @param 
   *                    - Type of result list
   * @param criteria
   *                    - criteria to generate the typed query
   * @param start
   *                    - position of first result to retrieve
   * @param maxResult
   *                    - maximum number of results to return
   * @return List of entities
   */
  public  List resultListFromCriteria(CriteriaQuery criteria, int start, int maxResult) {
    return resultListFromCriteria(criteria, start, maxResult, null);
  }

  /**
   * Returns a List of entities based off a given criteria and a Hibernate hint.
   *
   * @param 
   *                    - Type of result list
   * @param criteria
   *                    - criteria to generate the typed query
   * @param start
   *                    - position of first result to retrieve
   * @param maxResult
   *                    - maximum number of results to return
   * @param hints
   *                    - Hibernate hint to set on the query
   * @return List of entities
   */
  public  List resultListFromCriteria(CriteriaQuery criteria, int start, int maxResult, Map hints) {
    TypedQuery query = entityManager.createQuery(criteria);
    if (hints != null) {
      hints.forEach(query::setHint);
    }
    return query
            .setFirstResult(start)
            .setMaxResults(maxResult)
            .getResultList();
  }

  /**
   * Returns the resource count from a given predicate supplier.
   *
   * @param                entity type
   * @param entityClass       - entity class to query cannot be null
   * @param predicateSupplier - function to return the predicates cannot be null
   * @return resource count
   */
  public  Long getResourceCount(
    @NonNull Class entityClass,
    @NonNull PredicateSupplier predicateSupplier
  ) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery countQuery = cb.createQuery(Long.class);
    Root root = countQuery.from(entityClass);
    countQuery.select(cb.count(root));
    countQuery.where(predicateSupplier.supply(cb, root, entityManager));
    return entityManager.createQuery(countQuery).getSingleResult();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy