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

com.manydesigns.portofino.persistence.QueryUtils Maven / Gradle / Ivy

There is a newer version: 5.3.4
Show newest version
/*
 * Copyright (C) 2005-2020 ManyDesigns srl.  All rights reserved.
 * http://www.manydesigns.com/
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package com.manydesigns.portofino.persistence;

import com.manydesigns.elements.fields.search.Criterion;
import com.manydesigns.elements.fields.search.TextMatchMode;
import com.manydesigns.elements.reflection.ClassAccessor;
import com.manydesigns.elements.reflection.PropertyAccessor;
import com.manydesigns.elements.text.OgnlHqlFormat;
import com.manydesigns.elements.text.OgnlSqlFormat;
import com.manydesigns.elements.text.QueryStringWithParameters;
import com.manydesigns.portofino.model.Model;
import com.manydesigns.portofino.model.database.*;
import com.manydesigns.portofino.reflection.TableAccessor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcNamedParameter;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.statement.select.*;
import org.apache.commons.lang.StringUtils;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.jdbc.Work;
import org.hibernate.query.Query;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import java.io.Serializable;
import java.io.StringReader;
import java.sql.*;
import java.text.MessageFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class for running queries (SQL and HQL) against the database. Provides methods for many common cases,
 * but you can always use the Hibernate {@link Session} directly.
 *
 * @author Paolo Predonzani     - [email protected]
 * @author Angelo Lupo          - [email protected]
 * @author Giampiero Granatella - [email protected]
 * @author Alessio Stalla       - [email protected]
 */
public class QueryUtils {
    public static final String copyright =
            "Copyright (C) 2005-2020 ManyDesigns srl";

    protected static final String WHERE_STRING = " WHERE ";
    protected static final Pattern FROM_PATTERN =
            Pattern.compile("(SELECT\\s+.*\\s+)?FROM\\s+([a-z_$\\u0080-\\ufffe]{1}[a-z_$0-9\\u0080-\\ufffe]*).*",
                            Pattern.CASE_INSENSITIVE | Pattern.DOTALL); //. (dot) matches newlines

    protected static final Logger logger = LoggerFactory.getLogger(QueryUtils.class);

    /**
     * Runs a SQL query against a session. The query is processed with an {@link OgnlSqlFormat}, so it can
     * access values from the OGNL context.
     * @param session the session
     * @param sql the query string
     * @return the results of the query as an Object[] (an array cell per column)
     */
    public static List runSql(Session session, String sql) {
        OgnlHqlFormat hqlFormat = OgnlHqlFormat.create(sql);
        String formatString = hqlFormat.getFormatString();
        Object[] parameters = hqlFormat.evaluateOgnlExpressions(null);
        return runSql(session, formatString, parameters);
    }

    /**
     * Runs a SQL query against a session. The query can contain placeholders for the parameters, as supported
     * by {@link PreparedStatement}.
     * @param session the session
     * @param queryString the query
     * @param parameters parameters to substitute in the query
     * @return the results of the query as an Object[] (an array cell per column)
     */
    public static List runSql(Session session, final String queryString, final Object[] parameters) {
        final List result = new ArrayList();

        try {
            session.doWork(new Work() {
                public void execute(Connection connection) throws SQLException {
                    try (PreparedStatement stmt = connection.prepareStatement(queryString)) {
                        for (int i = 0; i < parameters.length; i++) {
                            stmt.setObject(i + 1, parameters[i]);
                        }
                        try(ResultSet rs = stmt.executeQuery()) {
                            ResultSetMetaData md = rs.getMetaData();
                            int cc = md.getColumnCount();
                            while (rs.next()) {
                                Object[] current = new Object[cc];
                                for (int i = 0; i < cc; i++) {
                                    current[i] = rs.getObject(i + 1);
                                }
                                result.add(current);
                            }
                        }
                    }
                }
            });
        } catch (HibernateException e) {
            session.getTransaction().rollback();
            session.beginTransaction();
            throw e;
        }

        return result;
    }

    /**
     * Runs a query, expressed as {@link TableCriteria}, against the database.
     * @param session the session
     * @param criteria the search criteria
     * @param firstResult index of the first result to return
     * @param maxResults maximum number of results to return
     * @return at most maxResults results from the query
     */
    public static List getObjects(
            Session session, TableCriteria criteria,
            @Nullable Integer firstResult, @Nullable Integer maxResults) {
        QueryStringWithParameters queryStringWithParameters =
                getQueryStringWithParametersForCriteria(criteria);

        return runHqlQuery(
                session,
                queryStringWithParameters.getQueryString(),
                queryStringWithParameters.getParameters(),
                firstResult,
                maxResults
        );
    }

    /**
     * Runs a query against the database. The query is processed with an {@link OgnlSqlFormat}, so it can
     * access values from the OGNL context, as well as from an (optional) rootFactory object.
     * @param session the session
     * @param queryString the query
     * @param rootObject the rootFactory object passed to the ognl evaluator (can be null).
     * @param firstResult index of the first result to return
     * @param maxResults maximum number of results to return
     * @return at most maxResults results from the query
     */
    public static List getObjects(
            Session session, String queryString,
            Object rootObject,
            @Nullable Integer firstResult, @Nullable Integer maxResults) {
        OgnlHqlFormat hqlFormat = OgnlHqlFormat.create(queryString);
        String formatString = hqlFormat.getFormatString();
        Object[] parameters = hqlFormat.evaluateOgnlExpressions(rootObject);

        return runHqlQuery(session, formatString, parameters, firstResult, maxResults);
    }

    /**
     * Runs a query against the database. The query is processed with an {@link OgnlSqlFormat}, so it can
     * access values from the OGNL context.
     * @param session the session
     * @param queryString the query
     * @param firstResult index of the first result to return
     * @param maxResults maximum number of results to return
     * @return at most maxResults results from the query
     */
    public static List getObjects(
            Session session, String queryString,
            @Nullable Integer firstResult, @Nullable Integer maxResults) {
        return getObjects(session, queryString, (TableCriteria) null, null, firstResult, maxResults);
    }

    /**
     * Tranforms a {@link TableCriteria} to a query string with an associated array of parameters.
     * @param criteria the criteria.
     * @return the same criteria encoded as a HQL query with parameters.
     */
    public static QueryStringWithParameters getQueryStringWithParametersForCriteria(
            TableCriteria criteria) {
        return getQueryStringWithParametersForCriteria(criteria, null, 1);
    }

    /**
     * Tranforms a {@link TableCriteria} to a query string with an associated array of parameters.
     * @param criteria the criteria.
     * @param alias the alias to use for the main entity.
     * @return the same criteria encoded as a HQL query with parameters.
     */
    public static QueryStringWithParameters getQueryStringWithParametersForCriteria(
            @Nullable TableCriteria criteria, @Nullable String alias, int initialParameterIndex) {
        if (criteria == null) {
            return new QueryStringWithParameters("", new Object[0]);
        }
        Table table = criteria.getTable();

        ArrayList parametersList = new ArrayList();
        StringBuilder whereBuilder = new StringBuilder();
        for (Criterion criterion : criteria) {
            PropertyAccessor accessor = criterion.getPropertyAccessor();
            String hqlFormat;
            if (criterion instanceof TableCriteria.EqCriterion) {
                TableCriteria.EqCriterion eqCriterion =
                        (TableCriteria.EqCriterion) criterion;
                Object value = eqCriterion.getValue();
                hqlFormat = "{0} = :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.InCriterion) {
                TableCriteria.InCriterion inCriterion =
                        (TableCriteria.InCriterion) criterion;
                Object[] values = inCriterion.getValues();
                StringBuilder params = new StringBuilder();
                if (values != null){
                    boolean first = true;
                    for (Object value : values) {
                        if (!first){
                            params.append(", :p").append(parametersList.size() + initialParameterIndex);
                        } else {
                            params.append(":p").append(parametersList.size() + initialParameterIndex);
                            first = false;
                        }
                        parametersList.add(value);
                    }
                    hqlFormat = "{0} in (" + params + ")";
                } else {
                    hqlFormat = null;
                }
            } else if (criterion instanceof TableCriteria.NeCriterion) {
                TableCriteria.NeCriterion neCriterion =
                        (TableCriteria.NeCriterion) criterion;
                Object value = neCriterion.getValue();
                hqlFormat = "{0} <> :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.BetweenCriterion) {
                TableCriteria.BetweenCriterion betweenCriterion =
                        (TableCriteria.BetweenCriterion) criterion;
                Object min = betweenCriterion.getMin();
                Object max = betweenCriterion.getMax();
                hqlFormat =
                        "{0} >= :p" + (parametersList.size() + initialParameterIndex) +
                        " AND {0} <= :p" + (parametersList.size() + initialParameterIndex + 1);
                parametersList.add(min);
                parametersList.add(max);
            } else if (criterion instanceof TableCriteria.GtCriterion) {
                TableCriteria.GtCriterion gtCriterion =
                        (TableCriteria.GtCriterion) criterion;
                Object value = gtCriterion.getValue();
                hqlFormat = "{0} > :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.GeCriterion) {
                TableCriteria.GeCriterion gtCriterion =
                        (TableCriteria.GeCriterion) criterion;
                Object value = gtCriterion.getValue();
                hqlFormat = "{0} >= :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.LtCriterion) {
                TableCriteria.LtCriterion ltCriterion =
                        (TableCriteria.LtCriterion) criterion;
                Object value = ltCriterion.getValue();
                hqlFormat = "{0} < :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.LeCriterion) {
                TableCriteria.LeCriterion leCriterion =
                        (TableCriteria.LeCriterion) criterion;
                Object value = leCriterion.getValue();
                hqlFormat = "{0} <= :p" + (parametersList.size() + initialParameterIndex);
                parametersList.add(value);
            } else if (criterion instanceof TableCriteria.LikeCriterion) {
                TableCriteria.LikeCriterion likeCriterion =
                        (TableCriteria.LikeCriterion) criterion;
                String value = (String) likeCriterion.getValue();
                if(likeCriterion.getTextMatchMode() == TextMatchMode.EQUALS) {
                    hqlFormat = "{0} = :p" + (parametersList.size() + initialParameterIndex);
                    parametersList.add(value);
                } else {
                    String pattern = processTextMatchMode(likeCriterion.getTextMatchMode(), value);
                    hqlFormat = "{0} like :p" + (parametersList.size() + initialParameterIndex);
                    parametersList.add(pattern);
                }
            } else if (criterion instanceof TableCriteria.IlikeCriterion) {
                TableCriteria.IlikeCriterion ilikeCriterion =
                        (TableCriteria.IlikeCriterion) criterion;
                String value = (String) ilikeCriterion.getValue();
                if(ilikeCriterion.getTextMatchMode() == TextMatchMode.EQUALS) {
                    hqlFormat = "lower({0}) = lower(:p" + (parametersList.size() + initialParameterIndex + ")");
                    parametersList.add(value);
                } else {
                    String pattern = processTextMatchMode(ilikeCriterion.getTextMatchMode(), value);
                    hqlFormat = "lower({0}) like lower(:p" + (parametersList.size() + initialParameterIndex) + ")";
                    parametersList.add(pattern);
                }
            } else if (criterion instanceof TableCriteria.IsNullCriterion) {
                hqlFormat = "{0} is null";
            } else if (criterion instanceof TableCriteria.IsNotNullCriterion) {
                hqlFormat = "{0} is not null";
            } else {
                logger.error("Unrecognized criterion: {}", criterion);
                throw new InternalError("Unrecognied criterion");
            }

            if (hqlFormat == null) {
                continue;
            }

            String accessorName = accessor.getName();
            if(alias != null) {
                accessorName = alias + "." + accessorName;
            }
            String hql = MessageFormat.format(hqlFormat, accessorName);

            if (whereBuilder.length() > 0) {
                whereBuilder.append(" AND ");
            }
            whereBuilder.append(hql);
        }
        String whereClause = whereBuilder.toString();
        String queryString;
        String actualEntityName = table.getActualEntityName();
        if(alias != null) {
            actualEntityName += " " + alias;
        }
        if (whereClause.length() > 0) {
            queryString = MessageFormat.format(
                    "FROM {0}" + WHERE_STRING + "{1}",
                    actualEntityName,
                    whereClause);
        } else {
            queryString = MessageFormat.format(
                    "FROM {0}",
                    actualEntityName);
        }

        Object[] parameters = new Object[parametersList.size()];
        parametersList.toArray(parameters);

        return new QueryStringWithParameters(queryString, parameters);
    }

    protected static String processTextMatchMode(TextMatchMode textMatchMode,
                                          String value) {
        String pattern;
        switch (textMatchMode) {
            case EQUALS:
                pattern = value;
                break;
            case CONTAINS:
                pattern = "%" + value + "%";
                break;
            case STARTS_WITH:
                pattern = value + "%";
                break;
            case ENDS_WITH:
                pattern = "%" + value;
                break;
            default:
                String msg = MessageFormat.format(
                        "Unrecognized text match mode: {0}",
                        textMatchMode);
                logger.error(msg);
                throw new InternalError(msg);
        }
        return pattern;
    }

    /**
     * Extracts the name of the main entity from a HQL query string, i.e. the first entity in the
     * from clause.
     * @param database the database containing the table.
     * @param queryString the query to analyze.
     * @return the main entity selected by the query
     */
    public static Table getTableFromQueryString(Database database, String queryString) {
        Matcher matcher = FROM_PATTERN.matcher(queryString);
        String entityName;
        if (matcher.matches()) {
            entityName =  matcher.group(2);
        } else {
            return null;
        }

        Table table = DatabaseLogic.findTableByEntityName(database, entityName);
        return table;
    }

     /**
      * Runs a query against the database. The query is expressed as a {@link TableCriteria} object plus a
      * query string to be merged with it (the typical case of a search in a crud defined by a query).
      * The query string is processed with an {@link OgnlSqlFormat}, so it can access values from the OGNL context,
      * as well as from an (optional) rootFactory object.
      * @param session the session
      * @param queryString the query
      * @param criteria the search criteria to merge with the query.
      * @param rootObject the rootFactory object passed to the ognl evaluator (can be null).
      * @param firstResult index of the first result to return
      * @param maxResults maximum number of results to return
      * @return at most maxResults results from the query
     */
    public static List getObjects(
            Session session,
            String queryString,
            TableCriteria criteria,
            @Nullable Object rootObject,
            @Nullable Integer firstResult,
            @Nullable Integer maxResults) {
        QueryStringWithParameters result = mergeQuery(queryString, criteria, rootObject);

        return runHqlQuery(session, result.getQueryString(), result.getParameters(), firstResult, maxResults);
    }

    /**
     * Merges a HQL query string with a {@link TableCriteria} object representing a search. The query string
     * is processed with an {@link OgnlSqlFormat}, so it can access values from the OGNL context, as well as
     * from an (optional) rootFactory object.
     * @param queryString the base query
     * @param criteria the criteria to merge with the query
     * @param rootObject the OGNL rootFactory object (can be null)
     * @return the merged query
     */
    public static QueryStringWithParameters mergeQuery
            (String queryString, @Nullable TableCriteria criteria, Object rootObject) {
        OgnlHqlFormat hqlFormat = OgnlHqlFormat.create(queryString);
        String formatString = hqlFormat.getFormatString();
        Object[] parameters = hqlFormat.evaluateOgnlExpressions(rootObject);

        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        PlainSelect parsedQueryString;
        PlainSelect parsedCriteriaQuery;
        try {
            parsedQueryString = parseQuery(parserManager, formatString);
        } catch (JSQLParserException e) {
            throw new RuntimeException("Couldn't merge query", e);
        }

        Alias mainEntityAlias = null;
        if(criteria != null) {
            mainEntityAlias = getEntityAlias(criteria.getTable().getActualEntityName(), parsedQueryString);
        }

        QueryStringWithParameters criteriaQuery =
                getQueryStringWithParametersForCriteria(
                        criteria, mainEntityAlias != null ? mainEntityAlias.getName() : null, parameters.length + 1);
        String criteriaQueryString = criteriaQuery.getQueryString();
        Object[] criteriaParameters = criteriaQuery.getParameters();

        try {
            if(StringUtils.isEmpty(criteriaQueryString)) {
                parsedCriteriaQuery = new PlainSelect();
            } else {
                parsedCriteriaQuery = parseQuery(parserManager, criteriaQueryString);
            }
        } catch (JSQLParserException e) {
            throw new RuntimeException("Couldn't merge query", e);
        }

        Expression whereExpression;
        if(parsedQueryString.getWhere() != null) {
            if(parsedCriteriaQuery.getWhere() != null) {
                whereExpression = parsedQueryString.getWhere();
                if(!(whereExpression instanceof Parenthesis)) {
                    whereExpression = new Parenthesis(whereExpression);
                }
                whereExpression = new AndExpression(whereExpression, parsedCriteriaQuery.getWhere());
            } else {
                whereExpression = parsedQueryString.getWhere();
            }
        } else {
            whereExpression = parsedCriteriaQuery.getWhere();
        }
        parsedQueryString.setWhere(whereExpression);
        if(criteria != null && criteria.getOrderBy() != null) {
            List orderByElements = new ArrayList();
            OrderByElement orderByElement = new OrderByElement();
            orderByElement.setAsc(criteria.getOrderBy().isAsc());
            String propertyName = criteria.getOrderBy().getPropertyAccessor().getName();
            if(mainEntityAlias != null) {
                propertyName = mainEntityAlias.getName() + "." + propertyName;
            }
            orderByElement.setExpression(
                    new net.sf.jsqlparser.schema.Column(
                            new net.sf.jsqlparser.schema.Table(), propertyName));
            orderByElements.add(orderByElement);
            if(parsedQueryString.getOrderByElements() != null) {
                for(Object el : parsedQueryString.getOrderByElements()) {
                    OrderByElement toAdd = (OrderByElement) el;
                    if(toAdd.getExpression() instanceof net.sf.jsqlparser.schema.Column) {
                        net.sf.jsqlparser.schema.Column column = (net.sf.jsqlparser.schema.Column) toAdd.getExpression();
                        if(StringUtils.isEmpty(column.getTable().getName()) && propertyName.equals(column.getColumnName())) {
                            continue; //do not add
                        }
                    }
                    orderByElements.add(toAdd);
                }
            }
            parsedQueryString.setOrderByElements(orderByElements);
        }
        String fullQueryString = parsedQueryString.toString();
        if(fullQueryString.toLowerCase().startsWith(FAKE_SELECT_PREFIX)) {
            fullQueryString = fullQueryString.substring(FAKE_SELECT_PREFIX.length());
        }

        // merge the parameters
        ArrayList mergedParametersList = new ArrayList();
        mergedParametersList.addAll(Arrays.asList(parameters));
        mergedParametersList.addAll(Arrays.asList(criteriaParameters));
        Object[] mergedParameters = new Object[mergedParametersList.size()];
        mergedParametersList.toArray(mergedParameters);

        return new QueryStringWithParameters(fullQueryString, mergedParameters);
    }

    public static final String FAKE_SELECT_PREFIX = "select __portofino_fake_select__ ";

    public static PlainSelect parseQuery(CCJSqlParserManager parserManager, String query) throws JSQLParserException {
        PlainSelect parsedQueryString;
        if(!query.toLowerCase().trim().startsWith("select")) {
            query = FAKE_SELECT_PREFIX + query;
        }
        parsedQueryString =
                (PlainSelect) ((Select) parserManager.parse(new StringReader(query)))
                        .getSelectBody();
        return parsedQueryString;
    }

    /**
     * Runs a HQL query against the database.
     * @see QueryUtils#runHqlQuery(Session, String, Object[], Integer, Integer)
     * @param session the session
     * @param queryString the query
     * @param parameters the query parameters
     * @return the results of the query
     */
    public static List runHqlQuery(
            Session session,
            String queryString,
            @Nullable Object[] parameters) {
        return runHqlQuery(session, queryString, parameters, null, null);
    }

    /**
     * Runs a HQL query against the database.
     * @see QueryUtils#runHqlQuery(Session, String, Object[], Integer, Integer)
     * @param session the session
     * @param queryString the query
     * @param parameters the query parameters
     * @param firstResult index of the first result to return
     * @param maxResults maximum number of results to return
     * @return the results of the query
     */
    public static List runHqlQuery(
            Session session,
            String queryString,
            @Nullable Object[] parameters,
            @Nullable Integer firstResult,
            @Nullable Integer maxResults) {

        Query query = session.createQuery(queryString);
        if (parameters != null) {
            for (int i = 0; i < parameters.length; i++) {
                if (parameters[i] instanceof Collection) {
                    query.setParameterList("p" + (i + 1), (Collection) parameters[i]);
                } else
                    query.setParameter("p" + (i + 1), parameters[i]);
            }
        }

        if (firstResult != null) {
            query.setFirstResult(firstResult);
        }

        if(maxResults != null) {
            query.setMaxResults(maxResults);
        }

        //noinspection unchecked
        try {
            List result = query.list();
            return result;
        } catch (HibernateException e) {
            logger.error("Error running query", e);
            session.getTransaction().rollback();
            session.beginTransaction();
            throw e;
        }
    }

    /**
     * Loads an object by primary key.
     * @param persistence the persistence object
     * @param database the database (connection provider) to use
     * @param entityName the name of the entity to load - usually the normalized (lowercased, etc.) table name.
     * @param pk the primary key object. Might be an atomic value (String, Integer, etc.) for single-column primary
     * keys, or a composite object for multi-column primary keys.
     * @return the loaded object, or null if an object with that key does not exist.
     */
    public static Object getObjectByPk(
            Persistence persistence, String database, String entityName, Serializable pk) {
        Session session = persistence.getSession(database);
        TableAccessor table = persistence.getTableAccessor(database, entityName);
        return getObjectByPk(session, table, pk);
    }

    /**
     * Loads an object by primary key.
     * @param session the Hibernate session
     * @param table the table where to load the object from
     * @param pk the primary key object. Might be an atomic value (String, Integer, etc.) for single-column primary
     * keys, or a composite object for multi-column primary keys.
     * @return the loaded object, or null if an object with that key does not exist.
     */
    public static Object getObjectByPk(Session session, TableAccessor table, Serializable pk) {
        String actualEntityName = table.getTable().getActualEntityName();
        Object result;
        PropertyAccessor[] keyProperties = table.getKeyProperties();
        int size = keyProperties.length;
        if (size > 1) {
            result = session.get(actualEntityName, pk);
            return result;
        }
        PropertyAccessor propertyAccessor = keyProperties[0];
        Serializable key = (Serializable) propertyAccessor.get(pk);
        result = session.get(actualEntityName, key);
        return result;
    }

    /**
     * Loads an object by primary key.
     * @param persistence the persistence object
     * @param baseTable the table to query
     * @param pkObject the primary key object.
     * @return the loaded object, or null if an object with that key does not exist.
     */
    public static Object getObjectByPk(Persistence persistence, Table baseTable, Serializable pkObject) {
        return getObjectByPk
                (persistence, baseTable.getDatabaseName(), baseTable.getActualEntityName(), pkObject);
    }

    /**
     * Loads an object by primary key. It also verifies that the object falls within the results of a given query.
     * @param persistence the persistence object
     * @param baseTable the table to load from
     * @param pkObject the primary key object
     * @param query the query (where condition) that the object must fulfill
     * @param rootObject the OGNL rootFactory object against which to evaluate the query string.
     * @return the loaded object, or null if an object with that key does not exist or falls outside the query.
     */
    public static Object getObjectByPk(
            Persistence persistence, Table baseTable, Serializable pkObject, String query, Object rootObject) {
        return getObjectByPk
                (persistence, baseTable.getDatabaseName(), baseTable.getActualEntityName(), pkObject, query, rootObject);
    }

    /**
     * Loads an object by primary key. It also verifies that the object falls within the results of a given query.
     * @param persistence the persistence object
     * @param database the database (connection provider)
     * @param entityName the name of the entity to load
     * @param pk the primary key object
     * @param hqlQueryString the query (where condition) that the object must fulfill
     * @param rootObject the OGNL rootFactory object against which to evaluate the query string.
     * @return the loaded object, or null if an object with that key does not exist or falls outside the query.
     */
    public static Object getObjectByPk(
            Persistence persistence, String database, String entityName,
            Serializable pk, String hqlQueryString, Object rootObject) {
        TableAccessor table = persistence.getTableAccessor(database, entityName);
        List result;
        PropertyAccessor[] keyProperties = table.getKeyProperties();
        OgnlHqlFormat hqlFormat = OgnlHqlFormat.create(hqlQueryString);
        String formatString = hqlFormat.getFormatString();
        Object[] ognlParameters = hqlFormat.evaluateOgnlExpressions(rootObject);
        int p = ognlParameters.length;
        Object[] parameters = new Object[p + keyProperties.length];
        System.arraycopy(ognlParameters, 0, parameters, 0, p);
        try {
            PlainSelect parsedQuery = parseQuery(new CCJSqlParserManager(), formatString);
            if(parsedQuery.getWhere() == null) {
                return getObjectByPk(persistence, database, entityName, pk);
            }

            Alias mainEntityAlias = getEntityAlias(entityName, parsedQuery);
            net.sf.jsqlparser.schema.Table mainEntityTable;
            if(mainEntityAlias != null) {
                mainEntityTable = new net.sf.jsqlparser.schema.Table(null, mainEntityAlias.getName());
            } else {
                mainEntityTable = new net.sf.jsqlparser.schema.Table();
            }


            for(int i = 0; i < keyProperties.length; i++) {
                PropertyAccessor propertyAccessor = keyProperties[i];
                EqualsTo condition = new EqualsTo();
                parsedQuery.setWhere(
                        new AndExpression(condition, new Parenthesis(parsedQuery.getWhere())));
                net.sf.jsqlparser.schema.Column column =
                        new net.sf.jsqlparser.schema.Column(mainEntityTable, propertyAccessor.getName());
                condition.setLeftExpression(column);
                JdbcNamedParameter jdbcParameter = new JdbcNamedParameter();
                jdbcParameter.setName("p" + (p + i + 1));
                condition.setRightExpression(jdbcParameter);
                parameters[p + i] = propertyAccessor.get(pk);
            }

            String fullQueryString = parsedQuery.toString();
            if(fullQueryString.toLowerCase().startsWith(FAKE_SELECT_PREFIX)) {
                fullQueryString = fullQueryString.substring(FAKE_SELECT_PREFIX.length());
            }
            Session session = persistence.getSession(database);
            result = runHqlQuery(session, fullQueryString, parameters);
            if(result != null && !result.isEmpty()) {
                return result.get(0);
            } else {
                return null;
            }
        } catch (JSQLParserException e) {
            throw new Error(e);
        }
    }

    protected static Alias getEntityAlias(String entityName, PlainSelect query) {
        FromItem fromItem = query.getFromItem();
        if (hasEntityAlias(entityName, fromItem)) {
            return fromItem.getAlias();
        }
        if(query.getJoins() != null) {
            for(Object o : query.getJoins()) {
                Join join = (Join) o;
                if (hasEntityAlias(entityName, join.getRightItem())) {
                    return join.getRightItem().getAlias();
                }
            }
        }
        logger.debug("Alias from entity " + entityName + " not found in query " + query);
        return null;
    }

    private static boolean hasEntityAlias(String entityName, FromItem fromItem) {
        return fromItem instanceof net.sf.jsqlparser.schema.Table &&
               ((net.sf.jsqlparser.schema.Table) fromItem).getName().equals(entityName) &&
               fromItem.getAlias() != null &&
               !StringUtils.isBlank(fromItem.getAlias().getName());
    }

    /**
     * Cleanly commits the current (for this thread) transaction of the given database.
     * @param persistence the persistence object
     * @param databaseName the name of the database (connection provider)
     */
    public static void commit(Persistence persistence, String databaseName) {
        Session session = persistence.getSession(databaseName);
        try {
            session.getTransaction().commit();
        } catch (HibernateException e) {
            persistence.closeSession(databaseName);
            throw e;
        }
    }

    /**
     * Navigates a ...-to-many relationship returning the list of objects associated with a given entity.
     * @param persistence the persistence object
     * @param databaseName the name of the database (connection provider)
     * @param entityName the type (entity name) of the master object
     * @param obj the master object
     * @param oneToManyRelationshipName the name of the relationship to navigate
     * @return the list of associated objects   
     */
    @SuppressWarnings({"unchecked"})
    public static List getRelatedObjects(
            Persistence persistence, String databaseName, String entityName,
            Object obj, String oneToManyRelationshipName) {
        Model model = persistence.getModel();
        ForeignKey relationship =
                DatabaseLogic.findOneToManyRelationship(model, databaseName,
                        entityName, oneToManyRelationshipName);
        if(relationship == null) {
            throw new IllegalArgumentException("Relationship not defined: " + oneToManyRelationshipName);
        }
        Table fromTable = relationship.getFromTable();
        Session session = persistence.getSession(fromTable.getDatabaseName());

        ClassAccessor toAccessor = persistence.getTableAccessor(databaseName, entityName);

        try {
            CriteriaDefinition criteria = createCriteria(session, fromTable);
            List where = new ArrayList<>();
            for (Reference reference : relationship.getReferences()) {
                Column fromColumn = reference.getActualFromColumn();
                Column toColumn = reference.getActualToColumn();
                PropertyAccessor toPropertyAccessor
                        = toAccessor.getProperty(toColumn.getActualPropertyName());
                Object toValue = toPropertyAccessor.get(obj);
                where.add(criteria.builder.equal(criteria.root.get(fromColumn.getActualPropertyName()), toValue));
            }
            return session.createQuery(criteria.query.where(where.toArray(new Predicate[0]))).list();
        } catch (Throwable e) {
            String msg = String.format(
                    "Cannot access relationship %s on entity %s.%s",
                    oneToManyRelationshipName, databaseName, entityName);
            logger.warn(msg, e);
        }
        return null;
    }

    public static CriteriaDefinition createCriteria(Session session, Table table) {
        return createCriteria(session, table.getActualEntityName());
    }

    public static CriteriaDefinition createCriteria(Session session, String entityName) {
        CriteriaBuilder cb = session.getCriteriaBuilder();
        CriteriaQuery criteria = cb.createQuery();
        Root from = criteria.from(getEntityType(session, entityName));
        return new CriteriaDefinition(criteria.select(from), cb, from);
    }

    public static EntityType getEntityType(Session session, Table table) {
        String entityName = table.getActualEntityName();
        return getEntityType(session, entityName);
    }

    @NotNull
    public static EntityType getEntityType(Session session, String entityName) {
        for(ManagedType managedType : session.getMetamodel().getManagedTypes()) {
            if(managedType instanceof EntityType) {
                EntityType entityType = (EntityType) managedType;
                if (entityType.getName().equals(entityName)) {
                    return entityType;
                }
            }
        }
        throw new RuntimeException("Entity not found: " + entityName);
    }

}