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

io.github.pustike.persist.sql.Finder Maven / Gradle / Ivy

/*
 * Copyright (C) 2016-2019 the original author or authors.
 *
 * 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 io.github.pustike.persist.sql;

import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import io.github.pustike.persist.metadata.ColumnType;
import io.github.pustike.persist.metadata.EntityData;
import io.github.pustike.persist.metadata.FieldData;
import io.github.pustike.persist.metadata.Schema;
import io.github.pustike.persist.utils.PersistUtils;

/**
 * The data finder api.
 */
public final class Finder {
    private static final Logger logger = System.getLogger(Finder.class.getName());
    private final SqlQuery sqlQuery;
    private final String alias;
    private Map aliasEntityDataMap;
    private Map joinAliasMap;
    private StringBuilder joinClause, whereClause;
    private String groupBy, orderBy;
    private List parameterList;

    Finder(SqlQuery sqlQuery, Class entityClass, String alias) {
        this.sqlQuery = sqlQuery;
        this.alias = alias;
        this.aliasEntityDataMap = new HashMap<>();
        this.aliasEntityDataMap.put(alias, sqlQuery.getSchema().getEntityData(entityClass));
        this.parameterList = new ArrayList<>();
        this.joinClause = new StringBuilder();
        this.joinAliasMap = new HashMap<>();
    }

    /**
     * Overrides the initial entity class and creates new alias finder for the given data class
     * @param dataClass the expected data class
     * @param alias the alias
     * @param  the type of the value
     * @return a new alias finder
     */
    public  AliasFinder select(Class dataClass, String alias) {
        return new AliasFinder<>(this, alias);
    }

    /**
     * Add the join clause using alias.field format and also specify the new alias for this type. Field should be of
     * type: {@link ColumnType#ForeignKey} and if it is optional {@code left outer join} is used,
     * else it uses {@code inner join}.
     * @param aliasFieldName the alias.field name
     * @param asAlias the new alias for this join
     * @return this finder instance
     */
    public Finder join(String aliasFieldName, String asAlias) {
        return join(aliasFieldName, asAlias, false);
    }

    /**
     * Add the join clause using alias.field format and also specify the new alias for this type. Using the parameter
     * inner join can be enforced even though the field is optional.
     * @param aliasFieldName the alias.field name
     * @param asAlias the new alias for this join
     * @param changeToInnerJoin {@code true} to use inner join even though the field is optional
     * @return this finder instance
     */
    public Finder join(String aliasFieldName, String asAlias, boolean changeToInnerJoin) {
        if (aliasEntityDataMap.containsKey(asAlias)) {
            throw new IllegalArgumentException("this alias is already used: " + asAlias);
        }
        String[] strings = aliasFieldName.split("\\.");
        if (strings.length != 2) {
            throw new IllegalArgumentException("invalid usage of field: " + aliasFieldName);
        }
        String fromAlias = strings[0].trim();
        FieldData fieldData = toFieldData(strings[1].trim(), fromAlias);
        if (fieldData.getColumnType() != ColumnType.ForeignKey) {
            throw new IllegalStateException("join field is not a foreign key: " + aliasFieldName);
        }
        Schema schema = sqlQuery.getSchema();
        EntityData fkEntityData = schema.getEntityData(fieldData.getFieldType());
        aliasEntityDataMap.put(asAlias, fkEntityData);
        joinClause.append(getJoinClause(fromAlias, fkEntityData, fieldData, asAlias, changeToInnerJoin));
        joinAliasMap.put(aliasFieldName, asAlias);
        return this;
    }

    private FieldData toFieldData(String fieldName, String fromAlias) {
        EntityData entityData = aliasEntityDataMap.get(fromAlias);
        if (entityData == null) {
            throw new IllegalArgumentException("the alias is not joined in this query: " + fromAlias);
        }
        return entityData.getFieldData(fieldName);
    }

    private String getJoinClause(String fromAlias, EntityData fkData, FieldData fieldData, String asAlias,
        boolean changeToInnerJoin) {
        String joinType = changeToInnerJoin || !fieldData.isOptional() ? " inner join " : " left outer join ";
        StringBuilder queryBuilder = new StringBuilder(joinType).append(sqlQuery.getSchema().toSchemaTableName(fkData))
            .append(" as ").append(asAlias).append(" on ")
            .append(asAlias).append('.').append(fkData.getIdField().getColumnName()).append(" = ");
        return queryBuilder.append(fromAlias).append('.').append(fieldData.getColumnName()).toString();
    }

    /**
     * Add the query string to whereClause and add given parameters to the list of params.
     * @param queryString the query string, can be empty
     * @param parameters array of parameters, can be empty
     * @return this finder instance
     */
    public Finder where(String queryString, Object... parameters) {
        if (parameters != null && parameters.length > 0) {
            parameterList.addAll(Arrays.asList(parameters));
        }
        if (queryString != null && !queryString.isEmpty()) {
            getWhereClause().append(toSqlString(queryString));
        }
        return this;
    }

    private StringBuilder getWhereClause() {
        if (whereClause == null) {
            whereClause = new StringBuilder(" where ");
        } else {
            whereClause.append(" and ");
        }
        return whereClause;
    }

    private String toSqlString(String queryString) {
        StringBuilder queryBuilder = new StringBuilder(queryString.length());
        StringBuilder aliasBuilder = new StringBuilder(), fieldBuilder = new StringBuilder();
        String alias = null;
        for (int i = 0, length = queryString.length(); i < length; i++) {
            char c = queryString.charAt(i);
            if (alias == null) {
                if (Character.isLetterOrDigit(c)) {
                    aliasBuilder.append(c);
                } else if (c == '.') {
                    alias = aliasBuilder.toString();
                    aliasBuilder.setLength(0);
                } else {
                    if (aliasBuilder.length() > 0) {
                        queryBuilder.append(aliasBuilder);
                        aliasBuilder.setLength(0);
                    }
                    queryBuilder.append(c);
                }
            } else {
                if (Character.isLetterOrDigit(c)) {
                    fieldBuilder.append(c);
                } else {
                    if (fieldBuilder.length() > 0) {
                        FieldData fieldData = toFieldData(fieldBuilder.toString(), alias);
                        queryBuilder.append(alias).append('.').append(fieldData.getColumnName());
                        fieldBuilder.setLength(0);
                        alias = null;
                    }
                    queryBuilder.append(c);
                }
            }
        }
        if (alias != null && fieldBuilder.length() > 0) {
            FieldData fieldData = toFieldData(fieldBuilder.toString(), alias);
            queryBuilder.append(alias).append('.').append(fieldData.getColumnName());
        } else if (aliasBuilder.length() > 0) {
            queryBuilder.append(aliasBuilder);
        }
        return queryBuilder.toString();
    }

    /**
     * Add the query string + {@code (?, ...)} to whereClause and add given parameters to the list of params.
     * @param queryString the query string
     * @param parameters array of parameters
     * @return this finder instance
     */
    public Finder whereIn(String queryString, Object[] parameters) {
        Objects.requireNonNull(queryString);
        if (parameters == null || parameters.length == 0) {
            throw new IllegalArgumentException("IN parameters can not be empty!");
        }
        return whereIn(queryString, Arrays.asList(parameters));
    }

    /**
     * Add the query string + {@code (?, ...)} to whereClause and add given values to the list of parameters.
     * @param queryString the query string
     * @param valueList collection of parameter values
     * @return this finder instance
     */
    public Finder whereIn(String queryString, Collection valueList) {
        if (valueList.isEmpty()) {
            throw new IllegalArgumentException("IN parameters can not be empty!");
        }
        StringBuilder inClause = new StringBuilder(toSqlString(queryString)).append('(');
        valueList.forEach(o -> inClause.append("?,"));
        inClause.setLength(inClause.length() - 1);
        getWhereClause().append(inClause.append(')'));
        parameterList.addAll(valueList);
        return this;
    }

    /**
     * Add the query string + {@code (selectClause)} to whereClause using another finder instance to build inner queries
     * and it adds all parameters from the inner finder.
     * @param queryString the query string
     * @param finder the inner finder instance
     * @param selectClause the single field select clause to use for IN query
     * @return this finder instance
     */
    public Finder whereIn(String queryString, Finder finder, String selectClause) {
        StringBuilder inClause = new StringBuilder(toSqlString(queryString)).append('(');
        inClause.append(finder.buildInnerQueryString(selectClause));
        getWhereClause().append(inClause.append(')'));
        parameterList.addAll(finder.parameterList);
        return this;
    }

    private String buildInnerQueryString(String selectClause) {
        return toString("select ", toSqlString(selectClause), getFromClause(), joinClause, whereClause, groupBy);
    }

    private String toString(CharSequence... builders) {
        StringBuilder queryBuilder = new StringBuilder();
        // Arrays.stream(builders).filter(Objects::nonNull).forEach(queryBuilder::append);
        for (CharSequence builder : builders) {
            if (builder != null && builder.length() > 0) {
                queryBuilder.append(builder);
            }
        }
        return queryBuilder.toString();
    }

    private StringBuilder getFromClause() {
        EntityData entityData = aliasEntityDataMap.get(alias);
        return new StringBuilder(" from ").append(sqlQuery.getSchema().toSchemaTableName(entityData))
            .append(" as ").append(alias);
    }

    /**
     * Add LIKE query string to where clause.
     * @param likeString the like string
     * @param searchByWord {@code true} if the like string should be searched by matching all words
     * @param searchColumns an array of columns to search
     * @return this finder instance
     */
    public Finder like(String likeString, boolean searchByWord, String... searchColumns) {
        getWhereClause().append(likeClause(likeString, searchByWord, searchColumns));
        return this;
    }

    private String likeClause(String likeString, boolean searchByWord, String... searchColumns) {
        StringBuilder likeBuilder = new StringBuilder().append('(');
        if (!searchByWord) {
            for (int columnIndex = 0; columnIndex < searchColumns.length; columnIndex++) {
                String searchColumn = toSqlColumn(searchColumns[columnIndex]);
                likeBuilder.append(columnIndex > 0 ? " or " : "");
                likeBuilder.append("lower(").append(searchColumn).append(") like ?");
                parameterList.add('%' + likeString + '%');
            }
            return likeBuilder.append(')').toString();
        } // else split likeString into words and search for any of them!
        String[] likeStrings = likeString.split(" ");
        for (int columnIndex = 0; columnIndex < searchColumns.length; columnIndex++) {
            String searchColumn = toSqlColumn(searchColumns[columnIndex]);
            likeBuilder.append(columnIndex > 0 ? " or " : "");
            for (int likeIndex = 0; likeIndex < likeStrings.length; likeIndex++) {
                likeBuilder.append(likeIndex > 0 ? " or " : "");
                likeBuilder.append("lower(").append(searchColumn).append(") like ?");
                parameterList.add('%' + likeString + '%');
            }
        }
        // .append(") LIKE CONCAT(\"%\", ?").append(paramIndex + likeIndex).append(", \"%\")");
        return likeBuilder.append(')').toString();
    }

    private String toSqlColumn(String aliasColumn) {
        String[] strings = aliasColumn.split("\\.");
        if (strings.length != 2) {
            throw new IllegalArgumentException("invalid usage of field: " + aliasColumn);
        }
        String fromAlias = strings[0].trim();
        FieldData fieldData = toFieldData(strings[1].trim(), fromAlias);
        return fromAlias + '.' + fieldData.getColumnName();
    }

    /**
     * Set the group by clause. Can be set only once.
     * @param groupByClause the group by clause
     * @return this finder instance
     */
    public Finder groupBy(String groupByClause) {
        if (this.groupBy != null) {
            throw new IllegalArgumentException("Group By clause is already specified in this finder!");
        }
        this.groupBy = " group by " + toSqlString(groupByClause);
        return this;
    }

    /**
     * Set the order by clause. Can be set only once.
     * @param orderByClause the order by clause
     * @return this finder instance
     */
    public Finder orderBy(String orderByClause) {
        if (this.orderBy != null) {
            throw new IllegalArgumentException("Order By clause is already specified in this finder!");
        }
        this.orderBy = " order by " + toSqlString(orderByClause);
        return this;
    }

    /**
     * Fetch the first row data from the resultSet.
     * @return the first instance if present, else null
     */
    public E fetchFirst() {
        return fetchFirst(null);
    }

    /**
     * Fetch the first row data from the resultSet using the field group.
     * @param fieldGroup the field group to select
     * @return the first instance if present, else null
     */
    public E fetchFirst(String fieldGroup) {
        List resultDataList = fetch(alias, fieldGroup, -1, 1, false);
        return resultDataList.isEmpty() ? null : resultDataList.get(0);
    }

    private  List fetch(String alias, String fieldGroup, int offset, int limit, boolean forUpdate) {
        EntityData entityData = aliasEntityDataMap.get(alias);
        if (entityData == null) {
            throw new IllegalArgumentException("the alias is not joined in this query: " + alias);
        }
        Map> aliasFieldNamesMap = new HashMap<>();
        StringBuilder joinBuilder = new StringBuilder(joinClause);
        String select = buildSelect(entityData, alias, fieldGroup, joinBuilder, aliasFieldNamesMap);
        String offsetLimit = (offset > 0 ? " offset " + offset : "") + (limit > 0 ? " limit " + limit : "");
        String queryString = toString(select, getFromClause(), joinBuilder, whereClause, groupBy, orderBy, offsetLimit);
        if (forUpdate) {
            queryString = queryString + " for update of " + alias;
        }
        logger.log(Level.INFO, queryString);
        try (PreparedStatement stmt = sqlQuery.getConnection().prepareStatement(queryString)) {
            setParameters(stmt);
            try (ResultSet resultSet = stmt.executeQuery()) {
                List resultDataList = new ArrayList<>();
                while (resultSet.next()) {
                    resultDataList.add(toResultData(resultSet, entityData.getEntityClass(), alias, aliasFieldNamesMap));
                }
                return Collections.unmodifiableList(resultDataList);
            }
        } catch (Exception e) {
            throw new RuntimeException("Couldn't execute query", e);
        }
    }

    private String buildSelect(EntityData entityData, String alias, String fieldGroup,
        StringBuilder joinBuilder, Map> aliasFieldNamesMap) {
        Schema schema = sqlQuery.getSchema();
        Set fieldGroupFields = entityData.getFieldGroupFields(fieldGroup);
        int fkCounter = 0;
        List selectedFieldNames = new ArrayList<>();
        StringBuilder queryBuilder = new StringBuilder("select");
        for (String fieldName : fieldGroupFields) {
            int fgIndex = fieldName.indexOf('@');
            String joinFieldGroup = null;
            if (fgIndex != -1) {
                joinFieldGroup = fieldName.substring(fgIndex + 1);
                fieldName = fieldName.substring(0, fgIndex);
            }
            FieldData fieldData = entityData.getFieldData(fieldName);
            if (fieldData.getColumnType() == ColumnType.ForeignKey) {
                EntityData fkEntityData = schema.getEntityData(fieldData.getFieldType());
                String asAlias = joinAliasMap.get(alias + '.' + fieldData.getName());
                if (asAlias == null) {
                    asAlias = "t" + (fkCounter++);
                    joinBuilder.append(getJoinClause(alias, fkEntityData, fieldData, asAlias, false));
                }
                List selectedFkFieldNames = new ArrayList<>();
                for (String fkFieldName : fkEntityData.getJoinFetchFields(joinFieldGroup)) {
                    int fgIndex2 = fkFieldName.indexOf('@');
                    fkFieldName = fgIndex2 == -1 ? fkFieldName : fkFieldName.substring(0, fgIndex2);
                    FieldData fkFieldData = fkEntityData.getFieldData(fkFieldName);
                    selectedFkFieldNames.add(fkFieldData.getName());
                    queryBuilder.append(' ').append(asAlias).append('.')
                        .append(fkFieldData.getColumnName()).append(',');
                }
                selectedFieldNames.add(fieldData.getName() + '@' + asAlias);
                aliasFieldNamesMap.put(fieldData.getFieldType().getSimpleName() + '@' + asAlias, selectedFkFieldNames);
            } else {
                selectedFieldNames.add(fieldData.getName());
                queryBuilder.append(' ').append(alias).append('.').append(fieldData.getColumnName()).append(',');
            }
        }
        queryBuilder.setLength(queryBuilder.length() - 1);
        aliasFieldNamesMap.put(entityData.getEntityClass().getSimpleName() + '@' + alias, selectedFieldNames);
        return queryBuilder.toString();
    }

    private void setParameters(PreparedStatement stmt) throws SQLException {
        for (int i = 0; i < parameterList.size(); i++) {
            Object param = parameterList.get(i);
            if (param instanceof Enum) {
                param = param.toString();
            }
            stmt.setObject(i + 1, param);
        }
    }

    @SuppressWarnings("unchecked")
    private  T toResultData(ResultSet resultSet, Class entityClass, String alias,
        Map> aliasFieldNamesMap) throws Exception {
        int index = 1;
        T instance = (T) entityClass.getDeclaredConstructor().newInstance();
        Schema schema = sqlQuery.getSchema();
        EntityData entityData = schema.getEntityData(entityClass);
        List fieldAliasList = aliasFieldNamesMap.get(entityClass.getSimpleName() + '@' + alias);
        for (String fieldAlias : fieldAliasList) {
            String[] fieldAliasSplit = fieldAlias.split("@");
            FieldData fieldData = entityData.getFieldData(fieldAliasSplit[0]);
            if (fieldData.getColumnType() == ColumnType.ForeignKey) {
                Class fkFieldType = fieldData.getFieldType();
                String key = fkFieldType.getSimpleName() + '@' + fieldAliasSplit[1];
                List joinFetchFields = aliasFieldNamesMap.get(key);
                EntityData fkEntityData = schema.getEntityData(fkFieldType);
                Object fkInstance = null;// TODO cache these instances based on id to avoid multiple object creations
                for (String fieldName : joinFetchFields) {
                    FieldData fkFieldData = fkEntityData.getFieldData(fieldName);
                    if (fkFieldData.getColumnType() != ColumnType.ForeignKey) {
                        Class resultType = fkFieldData.getResultType();
                        Object value = resultType == null ? resultSet.getObject(index++)
                            : resultSet.getObject(index++, resultType);
                        if (value != null && fkInstance == null) {
                            fkInstance = fkEntityData.getEntityClass().getDeclaredConstructor().newInstance();
                        }
                        if (fkInstance != null) {
                            fkFieldData.setValue(fkInstance, value);
                        }
                    } else {// only load the id from the 2nd level foreign key!
                        Class fkFieldType2 = fkFieldData.getFieldType();
                        EntityData fkEntityData2 = schema.getEntityData(fkFieldType2);
                        FieldData fkIdField2 = fkEntityData2.getIdField();
                        Class resultType = fkIdField2.getResultType();
                        Object value = resultType == null ? resultSet.getObject(index++)
                            : resultSet.getObject(index++, resultType);
                        if (value != null) {
                            Object fkInstance2 = fkEntityData2.getEntityClass().getDeclaredConstructor().newInstance();
                            fkIdField2.setValue(fkInstance2, value);
                            fkFieldData.setValue(fkInstance, fkInstance2);
                        }
                    }
                }
                fieldData.setValue(instance, fkInstance);
            } else {
                Class resultType = fieldData.getResultType();
                fieldData.setValue(instance, resultType == null ? resultSet.getObject(index++)
                    : resultSet.getObject(index++, resultType));
            }
        }
        return instance;
    }

    /**
     * Fetch the first row data from the resultSet for update.
     * @return the first instance if present, else null
     */
    public E fetchFirstForUpdate() {
        return fetchFirstForUpdate(null);
    }

    /**
     * Fetch the first row data from the resultSet using the field group, for update.
     * @param fieldGroup the field group to select
     * @return the first instance if present, else null
     */
    public E fetchFirstForUpdate(String fieldGroup) {
        List resultDataList = fetch(alias, fieldGroup, -1, 1, true);
        return resultDataList.isEmpty() ? null : resultDataList.get(0);
    }

    /**
     * Fetch the list of data from the resultSet.
     * @param firstResult the offset to use
     * @param maxResults the limit to apply
     * @return the list of data
     */
    public List fetch(int firstResult, int maxResults) {
        return fetch(alias, null, firstResult, maxResults, false);
    }

    /**
     * Fetch the list of data from the resultSet using the field group.
     * @param fieldGroup the field group to select
     * @param firstResult the offset to use
     * @param maxResults the limit to apply
     * @return the list of data
     */
    public List fetch(String fieldGroup, int firstResult, int maxResults) {
        return fetch(alias, fieldGroup, firstResult, maxResults, false);
    }

    /**
     * Fetch the list of data from the resultSet, for update.
     * @param firstResult the offset to use
     * @param maxResults the limit to apply
     * @return the list of data
     */
    public List fetchForUpdate(int firstResult, int maxResults) {
        return fetch(alias, null, firstResult, maxResults, true);
    }

    /**
     * Fetch the list of data from the resultSet using the field group, for update.
     * @param fieldGroup the field group to select
     * @param firstResult the offset to use
     * @param maxResults the limit to apply
     * @return the list of data
     */
    public List fetchForUpdate(String fieldGroup, int firstResult, int maxResults) {
        return fetch(alias, fieldGroup, firstResult, maxResults, true);
    }

    /**
     * Fetch the aggregate query result data.
     * @param selectClause the select clause to apply
     * @param  the type of result data
     * @return the single result from the query
     */
    public  T fetchSingleResult(String selectClause) {
        List resultDataList = doFetchResults(null, selectClause, -1, 1, null);
        return resultDataList.isEmpty() ? null : resultDataList.get(0);
    }

    @SuppressWarnings("unchecked")
    private  List doFetchResults(Class resultType, String selectClause, int offset, int limit,
        Map> columnDataTypes) {
        Class dataType = resultType != null && resultType.isArray()
            ? (Class) resultType.getComponentType() : resultType;
        String offsetLimit = (offset > 0 ? " offset " + offset : "") + (limit > 0 ? " limit " + limit : "");
        String queryString = toString("select ", toSqlString(selectClause), getFromClause(),
            joinClause, whereClause, groupBy, orderBy, offsetLimit);
        logger.log(Level.INFO, queryString);
        try (PreparedStatement stmt = sqlQuery.getConnection().prepareStatement(queryString)) {
            setParameters(stmt);
            List resultDataList = new ArrayList<>();
            try (ResultSet resultSet = stmt.executeQuery()) {
                ResultSetMetaData resultMetaData = resultSet.getMetaData();
                int columnCount = resultMetaData.getColumnCount();
                Map columnFieldMap = null;
                while (resultSet.next()) {
                    if (columnCount == 1) {
                        resultDataList.add(dataType == null ? (T) resultSet.getObject(1)
                            : resultSet.getObject(1, dataType));
                    } else {
                        if (dataType == null || resultType.isArray()) {
                            List rowDataList = new ArrayList<>();
                            for (int idx = 1; idx <= columnCount; idx++) {
                                Class fieldType = dataType != null ? dataType :
                                    columnDataTypes != null ? columnDataTypes.get(idx) : null;
                                rowDataList.add(fieldType == null ? resultSet.getObject(idx)
                                    : resultSet.getObject(idx, fieldType));
                            }
                            resultDataList.add((T) rowDataList.toArray(new Object[0]));
                        } else { // it is a bean class, so map the resultData to bean instances
                            if (columnFieldMap == null) {
                                columnFieldMap = new LinkedHashMap<>();
                                for (int idx = 1; idx <= columnCount; idx++) {
                                    String columnName = resultMetaData.getColumnLabel(idx);
                                    Field field = resultType.getDeclaredField(
                                        PersistUtils.underscoreToCamelCase(columnName));
                                    if (!field.trySetAccessible()) {
                                        throw new InaccessibleObjectException(
                                            "couldn't enable access to field: " + field);
                                    }
                                    columnFieldMap.put(columnName, field);
                                }
                            }
                            T instance = resultType.getDeclaredConstructor().newInstance();
                            for (Map.Entry mapEntry : columnFieldMap.entrySet()) {
                                Field field = mapEntry.getValue();
                                Class type = field.getType();
                                type = type.isPrimitive() ? PersistUtils.getWrapperClass(type) : type;
                                field.set(instance, resultSet.getObject(mapEntry.getKey(), type));
                            }
                            resultDataList.add(instance);
                        }
                    }
                }
            }
            return Collections.unmodifiableList(resultDataList);
        } catch (Exception e) {
            throw new RuntimeException("Couldn't execute query", e);
        }
    }

    /**
     * Fetch the aggregate query result data.
     * @param resultType the class of the result type
     * @param selectClause the select clause to apply
     * @param  the type of result data
     * @return the single result from the query
     */
    public  T fetchSingleResult(Class resultType, String selectClause) {
        List resultDataList = doFetchResults(resultType, selectClause, -1, 1, null);
        return resultDataList.isEmpty() ? null : resultDataList.get(0);
    }

    /**
     * Fetch the list of data from the resultSet by applying the offset and limit.
     * @param selectClause the select clause to apply
     * @param offset the offset to use
     * @param limit the limit to apply
     * @param  the type of result data
     * @return the list of data
     */
    public  List fetchResults(String selectClause, int offset, int limit) {
        return doFetchResults(null, selectClause, offset, limit, null);
    }

    /**
     * Fetch the list of data from the resultSet by applying the offset and limit.
     * @param resultType the class of the result type
     * @param selectClause the select clause to apply
     * @param offset the offset to use
     * @param limit the limit to apply
     * @param  the type of result data
     * @return the list of data
     */
    public  List fetchResults(Class resultType, String selectClause, int offset, int limit) {
        return doFetchResults(resultType, selectClause, offset, limit, null);
    }

    /**
     * Fetch the list of data from the resultSet by applying the offset and limit.
     * @param selectClause the select clause to apply
     * @param offset the offset to use
     * @param limit the limit to apply
     * @param columnDataTypes data type to use per column
     * @param  the type of result data
     * @return the list of data
     */
    public  List fetchResults(String selectClause, int offset, int limit, Map> columnDataTypes) {
        return doFetchResults(null, selectClause, offset, limit, columnDataTypes);
    }

    /**
     * Execute the delete query.
     * @return the updated row count
     */
    public int delete() {
        String queryString = "delete" + toString(getFromClause(), joinClause, whereClause);
        logger.log(Level.INFO, queryString);
        try (PreparedStatement stmt = sqlQuery.getConnection().prepareStatement(queryString)) {
            setParameters(stmt);
            return stmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Couldn't execute query", e);
        }
    }

    /**
     * The string formatted as a sql query, by appending all values from this finder api.
     * @return a string formatted as a sql query
     */
    @Override
    public String toString() {
        return toString(getFromClause(), joinClause, whereClause, groupBy, orderBy);
    }

    /**
     * The Alias finder to select different data than the initial entity type used to create the finder.
     * @param  the type of value
     */
    public static final class AliasFinder {
        private final Finder finder;
        private final String alias;

        /**
         * Constructor with the finder and alias to use to create this instance.
         * @param finder the finder
         * @param alias the alias to fetch
         */
        public AliasFinder(Finder finder, String alias) {
            this.finder = finder;
            this.alias = alias;
        }

        /**
         * Fetch the first row data from the resultSet.
         * @return the first instance if present, else null
         */
        public V fetchFirst() {
            return fetchFirst(null);
        }

        /**
         * Fetch the first row data from the resultSet using the field group.
         * @param fieldGroup the field group to select
         * @return the first instance if present, else null
         */
        public V fetchFirst(String fieldGroup) {
            List resultDataList = finder.fetch(alias, fieldGroup, -1, 1, false);
            return resultDataList.isEmpty() ? null : resultDataList.get(0);
        }

        /**
         * Fetch the first row data from the resultSet for update.
         * @return the first instance if present, else null
         */
        public V fetchFirstForUpdate() {
            return fetchFirstForUpdate(null);
        }

        /**
         * Fetch the first row data from the resultSet using the field group, for update.
         * @param fieldGroup the field group to select
         * @return the first instance if present, else null
         */
        public V fetchFirstForUpdate(String fieldGroup) {
            List resultDataList = finder.fetch(alias, fieldGroup, -1, 1, true);
            return resultDataList.isEmpty() ? null : resultDataList.get(0);
        }

        /**
         * Fetch the list of data from the resultSet.
         * @param firstResult the offset to use
         * @param maxResults the limit to apply
         * @return the list of data
         */
        public List fetch(int firstResult, int maxResults) {
            return finder.fetch(alias, null, firstResult, maxResults, false);
        }

        /**
         * Fetch the list of data from the resultSet using the field group.
         * @param fieldGroup the field group to select
         * @param firstResult the offset to use
         * @param maxResults the limit to apply
         * @return the list of data
         */
        public List fetch(String fieldGroup, int firstResult, int maxResults) {
            return finder.fetch(alias, fieldGroup, firstResult, maxResults, false);
        }

        /**
         * Fetch the list of data from the resultSet, for update.
         * @param firstResult the offset to use
         * @param maxResults the limit to apply
         * @return the list of data
         */
        public List fetchForUpdate(int firstResult, int maxResults) {
            return finder.fetch(alias, null, firstResult, maxResults, true);
        }

        /**
         * Fetch the list of data from the resultSet using the field group, for update.
         * @param fieldGroup the field group to select
         * @param firstResult the offset to use
         * @param maxResults the limit to apply
         * @return the list of data
         */
        public List fetchForUpdate(String fieldGroup, int firstResult, int maxResults) {
            return finder.fetch(alias, fieldGroup, firstResult, maxResults, true);
        }
    }
}