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

io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations Maven / Gradle / Ivy

There is a newer version: 4.9.3
Show newest version
/*
 * Copyright 2017-2020 original 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.micronaut.data.jdbc.operations;

import io.micronaut.aop.InvocationContext;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.data.annotation.QueryResult;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.jdbc.config.DataJdbcConfiguration;
import io.micronaut.data.jdbc.convert.JdbcConversionContext;
import io.micronaut.data.jdbc.mapper.ColumnIndexResultSetReader;
import io.micronaut.data.jdbc.mapper.ColumnNameResultSetReader;
import io.micronaut.data.jdbc.mapper.JdbcQueryStatement;
import io.micronaut.data.jdbc.mapper.SqlResultConsumer;
import io.micronaut.data.jdbc.runtime.ConnectionCallback;
import io.micronaut.data.jdbc.runtime.PreparedStatementCallback;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.JsonDataType;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.query.JoinPath;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.model.runtime.AttributeConverterRegistry;
import io.micronaut.data.model.runtime.DeleteBatchOperation;
import io.micronaut.data.model.runtime.DeleteOperation;
import io.micronaut.data.model.runtime.EntityOperation;
import io.micronaut.data.model.runtime.InsertBatchOperation;
import io.micronaut.data.model.runtime.InsertOperation;
import io.micronaut.data.model.runtime.PagedQuery;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.QueryResultInfo;
import io.micronaut.data.model.runtime.RuntimeAssociation;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
import io.micronaut.data.model.runtime.RuntimePersistentProperty;
import io.micronaut.data.model.runtime.UpdateBatchOperation;
import io.micronaut.data.model.runtime.UpdateOperation;
import io.micronaut.data.model.runtime.convert.AttributeConverter;
import io.micronaut.data.operations.async.AsyncCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveRepositoryOperations;
import io.micronaut.data.runtime.convert.DataConversionService;
import io.micronaut.data.runtime.convert.RuntimePersistentPropertyConversionContext;
import io.micronaut.data.runtime.date.DateTimeProvider;
import io.micronaut.data.runtime.mapper.DTOMapper;
import io.micronaut.data.runtime.mapper.ResultConsumer;
import io.micronaut.data.runtime.mapper.ResultReader;
import io.micronaut.data.runtime.mapper.TypeMapper;
import io.micronaut.data.runtime.mapper.sql.SqlDTOMapper;
import io.micronaut.data.runtime.mapper.sql.SqlResultEntityTypeMapper;
import io.micronaut.data.runtime.mapper.sql.SqlTypeMapper;
import io.micronaut.data.runtime.multitenancy.SchemaTenantResolver;
import io.micronaut.data.runtime.operations.ExecutorAsyncOperations;
import io.micronaut.data.runtime.operations.ExecutorReactiveOperations;
import io.micronaut.data.runtime.operations.internal.AbstractSyncEntitiesOperations;
import io.micronaut.data.runtime.operations.internal.AbstractSyncEntityOperations;
import io.micronaut.data.runtime.operations.internal.OperationContext;
import io.micronaut.data.runtime.operations.internal.SyncCascadeOperations;
import io.micronaut.data.runtime.operations.internal.query.BindableParametersStoredQuery;
import io.micronaut.data.runtime.operations.internal.sql.AbstractSqlRepositoryOperations;
import io.micronaut.data.runtime.operations.internal.sql.SqlJsonColumnMapperProvider;
import io.micronaut.data.runtime.operations.internal.sql.SqlPreparedQuery;
import io.micronaut.data.runtime.operations.internal.sql.SqlStoredQuery;
import io.micronaut.data.runtime.support.AbstractConversionContext;
import io.micronaut.json.JsonMapper;
import io.micronaut.transaction.TransactionOperations;
import io.micronaut.transaction.jdbc.DataSourceUtils;
import io.micronaut.transaction.jdbc.DelegatingDataSource;
import jakarta.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.annotation.PreDestroy;
import javax.sql.DataSource;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Implementation of {@link JdbcRepositoryOperations}.
 *
 * @author graemerocher
 * @author Denis Stepanov
 * @since 1.0.0
 */
@EachBean(DataSource.class)
@Internal
public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepositoryOperations implements
        JdbcRepositoryOperations,
        AsyncCapableRepository,
        ReactiveCapableRepository,
        AutoCloseable,
        SyncCascadeOperations.SyncCascadeOperationsHelper {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultJdbcRepositoryOperations.class);
    private final TransactionOperations transactionOperations;
    private final DataSource dataSource;
    private final DataSource unwrapedDataSource;
    private ExecutorAsyncOperations asyncOperations;
    private ExecutorService executorService;
    private final SyncCascadeOperations cascadeOperations;
    private final DataJdbcConfiguration jdbcConfiguration;
    @Nullable
    private final SchemaTenantResolver schemaTenantResolver;
    private final JdbcSchemaHandler schemaHandler;

    /**
     * Default constructor.
     *
     * @param dataSourceName                  The data source name
     * @param jdbcConfiguration               The jdbcConfiguration
     * @param dataSource                      The datasource
     * @param transactionOperations           The JDBC operations for the data source
     * @param executorService                 The executor service
     * @param beanContext                     The bean context
     * @param dateTimeProvider                The dateTimeProvider
     * @param entityRegistry                  The entity registry
     * @param conversionService               The conversion service
     * @param attributeConverterRegistry      The attribute converter registry
     * @param schemaTenantResolver            The schema tenant resolver
     * @param schemaHandler                   The schema handler
     * @param jsonMapper                      The JSON mapper
     * @param sqlJsonColumnMapperProvider     The SQL JSON column mapper provider
     */
    @Internal
    @SuppressWarnings("ParameterNumber")
    protected DefaultJdbcRepositoryOperations(@Parameter String dataSourceName,
                                              @Parameter DataJdbcConfiguration jdbcConfiguration,
                                              DataSource dataSource,
                                              @Parameter TransactionOperations transactionOperations,
                                              @Named("io") @Nullable ExecutorService executorService,
                                              BeanContext beanContext,
                                              @NonNull DateTimeProvider dateTimeProvider,
                                              RuntimeEntityRegistry entityRegistry,
                                              DataConversionService conversionService,
                                              AttributeConverterRegistry attributeConverterRegistry,
                                              @Nullable
                                              SchemaTenantResolver schemaTenantResolver,
                                              JdbcSchemaHandler schemaHandler,
                                              @Nullable JsonMapper jsonMapper,
                                              SqlJsonColumnMapperProvider sqlJsonColumnMapperProvider) {
        super(
                dataSourceName,
                new ColumnNameResultSetReader(conversionService),
                new ColumnIndexResultSetReader(conversionService),
                new JdbcQueryStatement(conversionService),
                dateTimeProvider,
                entityRegistry,
                beanContext,
                conversionService,
                attributeConverterRegistry,
                jsonMapper,
                sqlJsonColumnMapperProvider);
        this.schemaTenantResolver = schemaTenantResolver;
        this.schemaHandler = schemaHandler;
        ArgumentUtils.requireNonNull("dataSource", dataSource);
        ArgumentUtils.requireNonNull("transactionOperations", transactionOperations);
        this.dataSource = dataSource;
        this.unwrapedDataSource = DelegatingDataSource.unwrapDataSource(dataSource);
        this.transactionOperations = transactionOperations;
        this.executorService = executorService;
        this.cascadeOperations = new SyncCascadeOperations<>(conversionService, this);
        this.jdbcConfiguration = jdbcConfiguration;
    }

    @NonNull
    private ExecutorService newLocalThreadPool() {
        this.executorService = Executors.newCachedThreadPool();
        return executorService;
    }

    @Override
    public  T persistOne(JdbcOperationContext ctx, T value, RuntimePersistentEntity persistentEntity) {
        SqlStoredQuery storedQuery = resolveEntityInsert(ctx.annotationMetadata, ctx.repositoryType, (Class) value.getClass(), persistentEntity);
        JdbcEntityOperations persistOneOp = new JdbcEntityOperations<>(ctx, storedQuery, persistentEntity, value, true);
        persistOneOp.persist();
        return persistOneOp.getEntity();
    }

    @Override
    public  List persistBatch(JdbcOperationContext ctx, Iterable values,
                                    RuntimePersistentEntity childPersistentEntity,
                                    Predicate predicate) {
        SqlStoredQuery storedQuery = resolveEntityInsert(
                ctx.annotationMetadata,
                ctx.repositoryType,
                childPersistentEntity.getIntrospection().getBeanType(),
                childPersistentEntity
        );
        JdbcEntitiesOperations persistBatchOp = new JdbcEntitiesOperations<>(ctx, childPersistentEntity, values, storedQuery, true);
        persistBatchOp.veto(predicate);
        persistBatchOp.persist();
        return persistBatchOp.getEntities();
    }

    @Override
    public  T updateOne(JdbcOperationContext ctx, T value, RuntimePersistentEntity persistentEntity) {
        SqlStoredQuery storedQuery = resolveEntityUpdate(ctx.annotationMetadata, ctx.repositoryType, (Class) value.getClass(), persistentEntity);
        JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, persistentEntity, value, storedQuery);
        op.update();
        return op.getEntity();
    }

    @Override
    public void persistManyAssociation(JdbcOperationContext ctx,
                                       RuntimeAssociation runtimeAssociation,
                                       Object value, RuntimePersistentEntity persistentEntity,
                                       Object child, RuntimePersistentEntity childPersistentEntity) {
        SqlStoredQuery storedQuery = resolveSqlInsertAssociation(ctx.repositoryType, runtimeAssociation, persistentEntity, value);
        try {
            new JdbcEntityOperations<>(ctx, childPersistentEntity, child, storedQuery).execute();
        } catch (Exception e) {
            throw new DataAccessException("SQL error executing INSERT: " + e.getMessage(), e);
        }
    }

    @Override
    public void persistManyAssociationBatch(JdbcOperationContext ctx,
                                            RuntimeAssociation runtimeAssociation,
                                            Object value, RuntimePersistentEntity persistentEntity,
                                            Iterable child, RuntimePersistentEntity childPersistentEntity) {
        SqlStoredQuery storedQuery = resolveSqlInsertAssociation(ctx.repositoryType, runtimeAssociation, persistentEntity, value);
        try {
            JdbcEntitiesOperations assocOp = new JdbcEntitiesOperations<>(ctx, childPersistentEntity, child, storedQuery);
            assocOp.veto(ctx.persisted::contains);
            assocOp.execute();
        } catch (Exception e) {
            throw new DataAccessException("SQL error executing INSERT: " + e.getMessage(), e);
        }
    }

    @NonNull
    @Override
    public ExecutorAsyncOperations async() {
        ExecutorAsyncOperations asyncOperations = this.asyncOperations;
        if (asyncOperations == null) {
            synchronized (this) { // double check
                asyncOperations = this.asyncOperations;
                if (asyncOperations == null) {
                    asyncOperations = new ExecutorAsyncOperations(
                            this,
                            executorService != null ? executorService : newLocalThreadPool()
                    );
                    this.asyncOperations = asyncOperations;
                }
            }
        }
        return asyncOperations;
    }

    @NonNull
    @Override
    public ReactiveRepositoryOperations reactive() {
        return new ExecutorReactiveOperations(async(), conversionService);
    }

    @Nullable
    @Override
    public  R findOne(@NonNull PreparedQuery pq) {
        return executeRead(connection -> {
            SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
            RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
            try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, true)) {
                preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery));
                try (ResultSet rs = ps.executeQuery()) {
                    Class resultType = preparedQuery.getResultType();
                    if (preparedQuery.getResultDataType() == DataType.ENTITY) {
                        RuntimePersistentEntity resultPersistentEntity = getEntity(resultType);
                        BiFunction, Object, Object> loadListener = (loadedEntity, o) -> {
                            if (loadedEntity.hasPostLoadEventListeners()) {
                                return triggerPostLoad(o, loadedEntity, preparedQuery.getAnnotationMetadata());
                            } else {
                                return o;
                            }
                        };
                        QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
                        if (queryResultInfo == null || queryResultInfo.getType() == QueryResult.Type.TABULAR) {
                            final Set joinFetchPaths = preparedQuery.getJoinFetchPaths();
                            SqlResultEntityTypeMapper mapper = new SqlResultEntityTypeMapper<>(
                                resultPersistentEntity,
                                columnNameResultSetReader,
                                joinFetchPaths,
                                sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, ResultSet.class),
                                loadListener,
                                conversionService);
                            SqlResultEntityTypeMapper.PushingMapper oneMapper = mapper.readOneWithJoins();
                            if (rs.next()) {
                                oneMapper.processRow(rs);
                            }
                            while (!joinFetchPaths.isEmpty() && rs.next()) {
                                oneMapper.processRow(rs);
                            }
                            R result = oneMapper.getResult();
                            if (preparedQuery.hasResultConsumer()) {
                                preparedQuery.getParameterInRole(SqlResultConsumer.ROLE, SqlResultConsumer.class)
                                    .ifPresent(consumer -> consumer.accept(result, newMappingContext(rs)));
                            }
                            return result;
                        } else {
                            return rs.next() ? mapQueryColumnResult(preparedQuery, rs, queryResultInfo.getColumnName(), queryResultInfo.getJsonDataType(),
                                persistentEntity, resultType, ResultSet.class, loadListener) : null;
                        }
                    } else if (rs.next()) {
                        if (preparedQuery.isDtoProjection()) {
                            QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
                            if (queryResultInfo == null || queryResultInfo.getType() == QueryResult.Type.TABULAR) {
                                boolean isRawQuery = preparedQuery.isRawQuery();
                                TypeMapper introspectedDataMapper = new SqlDTOMapper<>(
                                    persistentEntity,
                                    isRawQuery ? getEntity(preparedQuery.getResultType()) : persistentEntity,
                                    columnNameResultSetReader,
                                    sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, ResultSet.class),
                                    conversionService
                                );
                                return introspectedDataMapper.map(rs, resultType);
                            } else {
                                String column = queryResultInfo.getColumnName();
                                JsonDataType jsonDataType = queryResultInfo.getJsonDataType();
                                return mapQueryColumnResult(preparedQuery, rs, column, jsonDataType, persistentEntity, resultType, ResultSet.class, null);
                            }
                        } else {
                            QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
                            if (queryResultInfo == null || queryResultInfo.getType() == QueryResult.Type.TABULAR) {
                                Object v = columnIndexResultSetReader.readDynamic(rs, 1, preparedQuery.getResultDataType());
                                if (v == null) {
                                    return null;
                                } else if (resultType.isInstance(v)) {
                                    return (R) v;
                                } else {
                                    return columnIndexResultSetReader.convertRequired(v, resultType);
                                }
                            } else {
                                String column = queryResultInfo.getColumnName();
                                JsonDataType jsonDataType = queryResultInfo.getJsonDataType();
                                return mapQueryColumnResult(preparedQuery, rs, column, jsonDataType, persistentEntity, resultType, ResultSet.class, null);
                            }
                        }
                    }
                }
            } catch (SQLException e) {
                throw new DataAccessException("Error executing SQL Query: " + e.getMessage(), e);
            }
            return null;
        });
    }

    @Override
    public  boolean exists(@NonNull PreparedQuery pq) {
        return executeRead(connection -> {
            try {
                SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
                try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, true)) {
                    preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery));
                    try (ResultSet rs = ps.executeQuery()) {
                        return rs.next();
                    }
                }
            } catch (SQLException e) {
                throw new DataAccessException("Error executing SQL query: " + e.getMessage(), e);
            }
        });
    }

    @NonNull
    @Override
    public  Stream findStream(@NonNull PreparedQuery preparedQuery) {
        ConnectionContext connectionContext = getConnectionCtx();
        return findStream(preparedQuery, connectionContext.connection, connectionContext.needsToBeClosed);
    }

    private  Stream findStream(@NonNull PreparedQuery pq, Connection connection, boolean closeConnection) {
        SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
        Class resultType = preparedQuery.getResultType();
        AtomicBoolean finished = new AtomicBoolean();

        PreparedStatement ps;
        try {
            ps = prepareStatement(connection::prepareStatement, preparedQuery, false, false);
            preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery));
        } catch (Exception e) {
            throw new DataAccessException("SQL Error preparing Query: " + e.getMessage(), e);
        }

        ResultSet openedRs = null;
        ResultSet rs;
        try {
            openedRs = ps.executeQuery();
            rs = openedRs;

            boolean dtoProjection = preparedQuery.isDtoProjection();
            boolean isEntity = preparedQuery.getResultDataType() == DataType.ENTITY;
            Spliterator spliterator;

            if (isEntity || dtoProjection) {
                RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
                SqlResultConsumer sqlMappingConsumer = preparedQuery.hasResultConsumer() ? preparedQuery.getParameterInRole(SqlResultConsumer.ROLE, SqlResultConsumer.class).orElse(null) : null;
                SqlTypeMapper mapper;
                if (dtoProjection) {
                    QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
                    if (queryResultInfo == null || queryResultInfo.getType() == QueryResult.Type.TABULAR) {
                        boolean isRawQuery = preparedQuery.isRawQuery();
                        mapper = new SqlDTOMapper<>(
                            persistentEntity,
                            isRawQuery ? getEntity(preparedQuery.getResultType()) : persistentEntity,
                            columnNameResultSetReader,
                            sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, ResultSet.class),
                            conversionService
                        );
                    } else {
                        String column = queryResultInfo.getColumnName();
                        JsonDataType jsonDataType = queryResultInfo.getJsonDataType();
                        mapper = createQueryResultMapper(preparedQuery, column, jsonDataType, ResultSet.class, persistentEntity, null);
                    }
                } else {
                    BiFunction, Object, Object> loadListener = (loadedEntity, o) -> {
                        if (loadedEntity.hasPostLoadEventListeners()) {
                            return triggerPostLoad(o, loadedEntity, preparedQuery.getAnnotationMetadata());
                        } else {
                            return o;
                        }
                    };
                    QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
                    if (queryResultInfo == null || queryResultInfo.getType() == QueryResult.Type.TABULAR) {
                        Set joinFetchPaths = preparedQuery.getJoinFetchPaths();
                        SqlResultEntityTypeMapper entityTypeMapper = new SqlResultEntityTypeMapper<>(
                            getEntity(resultType),
                            columnNameResultSetReader,
                            joinFetchPaths,
                            sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, ResultSet.class),
                            loadListener,
                            conversionService);
                        boolean onlySingleEndedJoins = isOnlySingleEndedJoins(persistentEntity, joinFetchPaths);
                        // Cannot stream ResultSet for "many" joined query
                        if (!onlySingleEndedJoins) {
                            try {
                                SqlResultEntityTypeMapper.PushingMapper> manyMapper = entityTypeMapper.readAllWithJoins();
                                while (rs.next()) {
                                    manyMapper.processRow(rs);
                                }
                                return manyMapper.getResult().stream();
                            } finally {
                                closeResultSet(connection, ps, rs, finished, closeConnection);
                            }
                        } else {
                            mapper = entityTypeMapper;
                        }
                    } else {
                        String column = queryResultInfo.getColumnName();
                        JsonDataType jsonDataType = queryResultInfo.getJsonDataType();
                        mapper = createQueryResultMapper(preparedQuery, column, jsonDataType, ResultSet.class, persistentEntity, loadListener);
                    }
                }
                spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE,
                        Spliterator.ORDERED | Spliterator.IMMUTABLE) {
                    @Override
                    public boolean tryAdvance(Consumer action) {
                        if (finished.get()) {
                            return false;
                        }
                        boolean hasNext = mapper.hasNext(rs);
                        if (hasNext) {
                            R o = mapper.map(rs, resultType);
                            if (sqlMappingConsumer != null) {
                                sqlMappingConsumer.accept(rs, o);
                            }
                            action.accept(o);
                        } else {
                            closeResultSet(connection, ps, rs, finished, closeConnection);
                        }
                        return hasNext;
                    }
                };
            } else {
                spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE,
                        Spliterator.ORDERED | Spliterator.IMMUTABLE) {
                    @Override
                    public boolean tryAdvance(Consumer action) {
                        if (finished.get()) {
                            return false;
                        }
                        try {
                            boolean hasNext = rs.next();
                            if (hasNext) {
                                Object v = columnIndexResultSetReader
                                        .readDynamic(rs, 1, preparedQuery.getResultDataType());
                                if (resultType.isInstance(v)) {
                                    //noinspection unchecked
                                    action.accept((R) v);
                                } else if (v != null) {
                                    Object r = columnIndexResultSetReader.convertRequired(v, resultType);
                                    if (r != null) {
                                        action.accept((R) r);
                                    }
                                }
                            } else {
                                closeResultSet(connection, ps, rs, finished, closeConnection);
                            }
                            return hasNext;
                        } catch (SQLException e) {
                            throw new DataAccessException("Error retrieving next JDBC result: " + e.getMessage(), e);
                        }
                    }
                };
            }
            return StreamSupport.stream(spliterator, false)
                .onClose(() -> closeResultSet(connection, ps, rs, finished, closeConnection));
        } catch (Exception e) {
            closeResultSet(connection, ps, openedRs, finished, closeConnection);
            throw new DataAccessException("SQL Error executing Query: " + e.getMessage(), e);
        }
    }

    private void closeResultSet(Connection connection, PreparedStatement ps, ResultSet rs, AtomicBoolean finished, boolean closeConnection) {
        if (finished.compareAndSet(false, true)) {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (ps != null) {
                    ps.close();
                }
                if (closeConnection) {
                    connection.close();
                }
            } catch (SQLException e) {
                throw new DataAccessException("Error closing JDBC result stream: " + e.getMessage(), e);
            }
        }
    }

    @NonNull
    @Override
    public  Iterable findAll(@NonNull PreparedQuery preparedQuery) {
        return executeRead(connection -> {
            try (Stream stream = findStream(preparedQuery, connection, false)) {
                return stream.collect(Collectors.toList());
            }
        });
    }

    @NonNull
    @Override
    public Optional executeUpdate(@NonNull PreparedQuery pq) {
        return executeWrite(connection -> {
            SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
            try {
                try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, true, false)) {
                    preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery));
                    int result = ps.executeUpdate();
                    if (QUERY_LOG.isTraceEnabled()) {
                        QUERY_LOG.trace("Update operation updated {} records", result);
                    }
                    if (preparedQuery.isOptimisticLock()) {
                        checkOptimisticLocking(1, result);
                    }
                    return Optional.of(result);
                }
            } catch (SQLException e) {
                Throwable throwable = handleSqlException(e, preparedQuery.getDialect());
                if (throwable instanceof DataAccessException dataAccessException) {
                    throw dataAccessException;
                }
                throw new DataAccessException("Error executing SQL UPDATE: " + e.getMessage(), e);
            }
        });
    }

    private Integer sum(Stream stream) {
        return stream.mapToInt(i -> i).sum();
    }

    @Override
    public  Optional deleteAll(@NonNull DeleteBatchOperation operation) {
        return Optional.ofNullable(executeWrite(connection -> {
            SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity();
            if (isSupportsBatchDelete(persistentEntity, storedQuery.getDialect())) {
                JdbcEntitiesOperations op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery);
                op.delete();
                return op.rowsUpdated;
            }
            return sum(
                    operation.split().stream()
                            .map(deleteOp -> {
                                JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, persistentEntity, deleteOp.getEntity(), storedQuery);
                                op.delete();
                                return op.rowsUpdated;
                            })
            );
        }));
    }

    @Override
    public  int delete(@NonNull DeleteOperation operation) {
        return executeWrite(connection -> {
            SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery);
            op.delete();
            return op;
        }).rowsUpdated;
    }

    @NonNull
    @Override
    public  T update(@NonNull UpdateOperation operation) {
        return executeWrite(connection -> {
            SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery);
            op.update();
            return op.getEntity();
        });
    }

    @NonNull
    @Override
    public  Iterable updateAll(@NonNull UpdateBatchOperation operation) {
        return executeWrite(connection -> {
            final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity();
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            if (!isSupportsBatchUpdate(persistentEntity, storedQuery.getDialect())) {
                return operation.split()
                        .stream()
                        .map(updateOp -> {
                            JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, persistentEntity, updateOp.getEntity(), storedQuery);
                            op.update();
                            return op.getEntity();
                        })
                        .collect(Collectors.toList());
            }
            JdbcEntitiesOperations op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery);
            op.update();
            return op.getEntities();
        });
    }

    @NonNull
    @Override
    public  T persist(@NonNull InsertOperation operation) {
        return executeWrite(connection -> {
            final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, storedQuery.getPersistentEntity(), operation.getEntity(), true);
            op.persist();
            return op;
        }).getEntity();
    }

    @Nullable
    @Override
    public  T findOne(@NonNull Class type, @NonNull Serializable id) {
        throw new UnsupportedOperationException("The findOne method by ID is not supported. Execute the SQL query directly");
    }

    @NonNull
    @Override
    public  Iterable findAll(@NonNull PagedQuery query) {
        throw new UnsupportedOperationException("The findAll method without an explicit query is not supported. Use findAll(PreparedQuery) instead");
    }

    @Override
    public  long count(PagedQuery pagedQuery) {
        throw new UnsupportedOperationException("The count method without an explicit query is not supported. Use findAll(PreparedQuery) instead");
    }

    @NonNull
    @Override
    public  Stream findStream(@NonNull PagedQuery query) {
        throw new UnsupportedOperationException("The findStream method without an explicit query is not supported. Use findStream(PreparedQuery) instead");
    }

    @Override
    public  Page findPage(@NonNull PagedQuery query) {
        throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findPage(PreparedQuery) instead");
    }

    @NonNull
    public  Iterable persistAll(@NonNull InsertBatchOperation operation) {
        return executeWrite(connection -> {
            final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery());
            final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity();
            JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
            if (!isSupportsBatchInsert(persistentEntity, storedQuery.getDialect())) {
                return operation.split().stream()
                        .map(persistOp -> {
                            JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, persistentEntity, persistOp.getEntity(), true);
                            op.persist();
                            return op.getEntity();
                        })
                        .collect(Collectors.toList());
            } else {
                JdbcEntitiesOperations op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery, true);
                op.persist();
                return op.getEntities();
            }

        });
    }

    private  I executeRead(Function fn) {
        if (jdbcConfiguration.isTransactionPerOperation()) {
            return transactionOperations.executeRead(status -> {
                Connection connection = status.getConnection();
                applySchema(connection);
                return fn.apply(connection);
            });
        }
        if (!jdbcConfiguration.isAllowConnectionPerOperation() || transactionOperations.hasConnection()) {
            Connection connection = transactionOperations.getConnection();
            applySchema(connection);
            return fn.apply(connection);
        }
        try (Connection connection = unwrapedDataSource.getConnection()) {
            applySchema(connection);
            return fn.apply(connection);
        } catch (SQLException e) {
            throw new DataAccessException("Cannot get connection: " + e.getMessage(), e);
        }
    }

    private  I executeWrite(Function fn) {
        if (jdbcConfiguration.isTransactionPerOperation()) {
            return transactionOperations.executeWrite(status -> {
                Connection connection = status.getConnection();
                applySchema(connection);
                return fn.apply(connection);
            });
        }
        if (!jdbcConfiguration.isAllowConnectionPerOperation() || transactionOperations.hasConnection()) {
            Connection connection = transactionOperations.getConnection();
            applySchema(connection);
            return fn.apply(connection);
        }
        try (Connection connection = unwrapedDataSource.getConnection()) {
            applySchema(connection);
            return fn.apply(connection);
        } catch (SQLException e) {
            throw new DataAccessException("Cannot get connection: " + e.getMessage(), e);
        }
    }

    @Override
    @PreDestroy
    public void close() {
        if (executorService != null) {
            executorService.shutdown();
        }
    }

    private void applySchema(Connection connection) {
        if (schemaTenantResolver != null) {
            String schema = schemaTenantResolver.resolveTenantSchemaName();
            schemaHandler.useSchema(connection, jdbcConfiguration.getDialect(), schema);
        }
    }

    @NonNull
    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @NonNull
    @Override
    public Connection getConnection() {
        Connection connection;
        if (jdbcConfiguration.isTransactionPerOperation() || !jdbcConfiguration.isAllowConnectionPerOperation() || transactionOperations.hasConnection()) {
            connection = transactionOperations.getConnection();
        } else {
            connection = DataSourceUtils.getConnection(dataSource, true);
        }
        applySchema(connection);
        return connection;
    }

    @NonNull
    private ConnectionContext getConnectionCtx() {
        boolean needsToCloseConnection;
        Connection connection;
        if (jdbcConfiguration.isTransactionPerOperation() || !jdbcConfiguration.isAllowConnectionPerOperation() || transactionOperations.hasConnection()) {
            connection = transactionOperations.getConnection();
            needsToCloseConnection = false;
        } else {
            connection = DataSourceUtils.getConnection(dataSource, true);
            needsToCloseConnection = true;
        }
        applySchema(connection);
        return new ConnectionContext(connection, needsToCloseConnection);
    }

    @NonNull
    @Override
    public  R execute(@NonNull ConnectionCallback callback) {
        return executeWrite(connection -> {
            try {
                return callback.call(connection);
            } catch (SQLException e) {
                throw new DataAccessException("Error executing SQL Callback: " + e.getMessage(), e);
            }
        });
    }

    @NonNull
    @Override
    public  R prepareStatement(@NonNull String sql, @NonNull PreparedStatementCallback callback) {
        ArgumentUtils.requireNonNull("sql", sql);
        ArgumentUtils.requireNonNull("callback", callback);
        if (QUERY_LOG.isDebugEnabled()) {
            QUERY_LOG.debug("Executing Query: {}", sql);
        }
        ConnectionContext connectionCtx = getConnectionCtx();
        try {
            R result = null;
            try {
                PreparedStatement ps = connectionCtx.connection.prepareStatement(sql);
                try {
                    result = callback.call(ps);
                    return result;
                } finally {
                    if (!(result instanceof AutoCloseable)) {
                        ps.close();
                    }
                }
            } finally {
                if (!(result instanceof AutoCloseable) && connectionCtx.needsToBeClosed) {
                    connectionCtx.connection.close();
                }
            }
        } catch (SQLException e) {
            throw new DataAccessException("Error preparing SQL statement: " + e.getMessage(), e);
        }
    }

    @NonNull
    @Override
    public  Stream entityStream(@NonNull ResultSet resultSet, @NonNull Class rootEntity) {
        return entityStream(resultSet, null, rootEntity);
    }

    @NonNull
    @Override
    public  E readEntity(@NonNull String prefix, @NonNull ResultSet resultSet, @NonNull Class type) throws DataAccessException {
        return new SqlResultEntityTypeMapper<>(
                prefix,
                getEntity(type),
                columnNameResultSetReader,
                jsonMapper != null ? () -> jsonMapper : null,
                conversionService).map(resultSet, type);
    }

    @NonNull
    @Override
    public  D readDTO(@NonNull String prefix, @NonNull ResultSet resultSet, @NonNull Class rootEntity, @NonNull Class dtoType) throws DataAccessException {
        return new DTOMapper(
                getEntity(rootEntity),
                columnNameResultSetReader,
                jsonMapper != null ? () -> jsonMapper : null,
                conversionService).map(resultSet, dtoType);
    }

    @NonNull
    @Override
    public  Stream entityStream(@NonNull ResultSet resultSet, @Nullable String prefix, @NonNull Class rootEntity) {
        ArgumentUtils.requireNonNull("resultSet", resultSet);
        ArgumentUtils.requireNonNull("rootEntity", rootEntity);
        TypeMapper mapper = new SqlResultEntityTypeMapper<>(prefix, getEntity(rootEntity), columnNameResultSetReader,
            jsonMapper != null ? () -> jsonMapper : null, conversionService);
        Iterable iterable = () -> new Iterator() {
            boolean fetched = false;
            boolean end = false;

            @Override
            public boolean hasNext() {
                if (fetched) {
                    return true;
                }
                if (end) {
                    return false;
                }
                try {
                    if (resultSet.next()) {
                        fetched = true;
                    } else {
                        end = true;
                    }
                } catch (SQLException e) {
                    throw new DataAccessException("Error retrieving next JDBC result: " + e.getMessage(), e);
                }
                return !end;
            }

            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                fetched = false;
                return mapper.map(resultSet, rootEntity);
            }
        };
        return StreamSupport.stream(iterable.spliterator(), false);
    }

    @NonNull
    private ResultConsumer.Context newMappingContext(ResultSet rs) {
        return new ResultConsumer.Context() {
            @Override
            public ResultSet getResultSet() {
                return rs;
            }

            @Override
            public ResultReader getResultReader() {
                return columnNameResultSetReader;
            }

            @NonNull
            @Override
            public  E readEntity(String prefix, Class type) throws DataAccessException {
                RuntimePersistentEntity entity = getEntity(type);
                SqlResultEntityTypeMapper mapper = new SqlResultEntityTypeMapper<>(
                        prefix,
                        entity,
                        columnNameResultSetReader,
                        jsonMapper != null ? () -> jsonMapper : null,
                        conversionService);
                return mapper.map(rs, type);
            }

            @NonNull
            @Override
            public  D readDTO(@NonNull String prefix, @NonNull Class rootEntity, @NonNull Class dtoType) throws DataAccessException {
                RuntimePersistentEntity entity = getEntity(rootEntity);
                TypeMapper introspectedDataMapper = new DTOMapper<>(
                        entity,
                        columnNameResultSetReader,
                        jsonMapper != null ? () -> jsonMapper : null,
                        conversionService);
                return introspectedDataMapper.map(rs, dtoType);
            }
        };
    }

    private  JdbcOperationContext createContext(EntityOperation operation, Connection connection, SqlStoredQuery storedQuery) {
        return new JdbcOperationContext(operation.getAnnotationMetadata(),  operation.getInvocationContext(), operation.getRepositoryType(), storedQuery.getDialect(), connection);
    }

    /**
     * Gets the generated id on record insert.
     *
     * @param generatedKeysResultSet the generated keys result set
     * @param identity the identity persistent field
     * @param dialect the SQL dialect
     * @return the generated id
     */
    private Object getGeneratedIdentity(@NonNull ResultSet generatedKeysResultSet, RuntimePersistentProperty identity, Dialect dialect) {
        if (dialect == Dialect.POSTGRES) {
            // Postgres returns all fields, not just id so we need to access generated id by the name
            return columnNameResultSetReader.readDynamic(generatedKeysResultSet, identity.getPersistedName(), identity.getDataType());
        }
        return columnIndexResultSetReader.readDynamic(generatedKeysResultSet, 1, identity.getDataType());
    }

    @Override
    public boolean isSupportsBatchInsert(JdbcOperationContext jdbcOperationContext, RuntimePersistentEntity persistentEntity) {
        return isSupportsBatchInsert(persistentEntity, jdbcOperationContext.dialect);
    }

    private final class JdbcParameterBinder implements BindableParametersStoredQuery.Binder {

        private final SqlStoredQuery sqlStoredQuery;
        private final Connection connection;
        private final PreparedStatement ps;
        private int index = 1;

        public JdbcParameterBinder(Connection connection, PreparedStatement ps, SqlStoredQuery sqlStoredQuery) {
            this.connection = connection;
            this.ps = ps;
            this.sqlStoredQuery = sqlStoredQuery;
        }

        @Override
        public Object autoPopulateRuntimeProperty(RuntimePersistentProperty persistentProperty, Object previousValue) {
            return runtimeEntityRegistry.autoPopulateRuntimeProperty(persistentProperty, previousValue);
        }

        @Override
        public Object convert(Object value, RuntimePersistentProperty property) {
            AttributeConverter converter = property.getConverter();
            if (converter != null) {
                return converter.convertToPersistedValue(value, createTypeConversionContext(property, property.getArgument()));
            }
            return value;
        }

        @Override
        public Object convert(Class converterClass, Object value, Argument argument) {
            if (converterClass == null) {
                return value;
            }
            AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass);
            ConversionContext conversionContext = createTypeConversionContext(null, argument);
            return converter.convertToPersistedValue(value, conversionContext);
        }

        private ConversionContext createTypeConversionContext(RuntimePersistentProperty property,
                                                              Argument argument) {
            Objects.requireNonNull(connection);
            if (property != null) {
                return new RuntimePersistentPropertyJdbcCC(connection, property);
            }
            if (argument != null) {
                return new ArgumentJdbcCC(connection, argument);
            }
            return new JdbcConversionContextImpl(connection);
        }

        @Override
        public void bindOne(QueryParameterBinding binding, Object value) {
            JsonDataType jsonDataType = null;
            if (binding.getDataType() == DataType.JSON) {
                jsonDataType = binding.getJsonDataType();
            }
            setStatementParameter(ps, index, binding.getDataType(), jsonDataType, value, sqlStoredQuery);
            index++;
        }

        @Override
        public void bindMany(QueryParameterBinding binding, Collection values) {
            for (Object value : values) {
                bindOne(binding, value);
            }
        }

        @Override
        public int currentIndex() {
            return index;
        }

    }

    private final class JdbcEntityOperations extends AbstractSyncEntityOperations {

        private final SqlStoredQuery storedQuery;
        private Integer rowsUpdated;
        private Map previousValues;

        private JdbcEntityOperations(JdbcOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity, SqlStoredQuery storedQuery) {
            this(ctx, storedQuery, persistentEntity, entity, false);
        }

        private JdbcEntityOperations(JdbcOperationContext ctx, SqlStoredQuery storedQuery, RuntimePersistentEntity persistentEntity, T entity, boolean insert) {
            super(ctx,
                    DefaultJdbcRepositoryOperations.this.cascadeOperations,
                    entityEventRegistry, persistentEntity,
                    DefaultJdbcRepositoryOperations.this.conversionService, entity, insert);
            this.storedQuery = storedQuery;
        }

        @Override
        protected void collectAutoPopulatedPreviousValues() {
            previousValues = storedQuery.collectAutoPopulatedPreviousValues(entity);
        }

        private PreparedStatement prepare(Connection connection, SqlStoredQuery storedQuery) throws SQLException {
            if (storedQuery instanceof SqlPreparedQuery) {
                ((SqlPreparedQuery) storedQuery).prepare(entity);
            }
            if (insert) {
                Dialect dialect = storedQuery.getDialect();
                if (hasGeneratedId && (dialect == Dialect.ORACLE || dialect == Dialect.SQL_SERVER)) {
                    return connection.prepareStatement(this.storedQuery.getQuery(), new String[]{persistentEntity.getIdentity().getPersistedName()});
                } else {
                    return connection.prepareStatement(this.storedQuery.getQuery(), hasGeneratedId ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
                }
            } else {
                return connection.prepareStatement(this.storedQuery.getQuery());
            }
        }

        @Override
        protected void execute() throws SQLException {
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing SQL query: {}", storedQuery.getQuery());
            }
            try (PreparedStatement ps = prepare(ctx.connection, storedQuery)) {
                storedQuery.bindParameters(new JdbcParameterBinder(ctx.connection, ps, storedQuery), ctx.invocationContext, entity, previousValues);
                rowsUpdated = ps.executeUpdate();
                if (hasGeneratedId) {
                    try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                        if (generatedKeys.next()) {
                            RuntimePersistentProperty identity = persistentEntity.getIdentity();
                            Object id = getGeneratedIdentity(generatedKeys, identity, storedQuery.getDialect());
                            BeanProperty property = identity.getProperty();
                            entity = updateEntityId(property, entity, id);
                        } else {
                            throw new DataAccessException("Failed to generate ID for entity: " + entity);
                        }
                    }
                }
                if (storedQuery.isOptimisticLock()) {
                    checkOptimisticLocking(1, rowsUpdated);
                }
            }
        }
    }

    private final class JdbcEntitiesOperations extends AbstractSyncEntitiesOperations {

        private final SqlStoredQuery storedQuery;
        private int rowsUpdated;

        private JdbcEntitiesOperations(JdbcOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities, SqlStoredQuery storedQuery) {
            this(ctx, persistentEntity, entities, storedQuery, false);
        }

        private JdbcEntitiesOperations(JdbcOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities, SqlStoredQuery storedQuery, boolean insert) {
            super(ctx,
                    DefaultJdbcRepositoryOperations.this.cascadeOperations,
                    DefaultJdbcRepositoryOperations.this.conversionService,
                    entityEventRegistry, persistentEntity, entities, insert);
            this.storedQuery = storedQuery;
        }

        @Override
        protected void collectAutoPopulatedPreviousValues() {
            for (Data d : entities) {
                if (d.vetoed) {
                    continue;
                }
                d.previousValues = storedQuery.collectAutoPopulatedPreviousValues(d.entity);
            }
        }

        private PreparedStatement prepare(Connection connection) throws SQLException {
            if (insert) {
                Dialect dialect = storedQuery.getDialect();
                if (hasGeneratedId && (dialect == Dialect.ORACLE || dialect == Dialect.SQL_SERVER)) {
                    return connection.prepareStatement(storedQuery.getQuery(), new String[]{persistentEntity.getIdentity().getPersistedName()});
                } else {
                    return connection.prepareStatement(storedQuery.getQuery(), hasGeneratedId ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
                }
            } else {
                return connection.prepareStatement(storedQuery.getQuery());
            }
        }

        private void setParameters(PreparedStatement stmt, SqlStoredQuery storedQuery) throws SQLException {
            for (Data d : entities) {
                if (d.vetoed) {
                    continue;
                }
                storedQuery.bindParameters(new JdbcParameterBinder(ctx.connection, stmt, storedQuery), ctx.invocationContext, d.entity, d.previousValues);
                stmt.addBatch();
            }
        }

        @Override
        protected void execute() throws SQLException {
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing SQL query: {}", storedQuery.getQuery());
            }
            try (PreparedStatement ps = prepare(ctx.connection)) {
                setParameters(ps, storedQuery);
                rowsUpdated = Arrays.stream(ps.executeBatch()).sum();
                if (hasGeneratedId) {
                    RuntimePersistentProperty identity = persistentEntity.getIdentity();
                    List ids = new ArrayList<>();
                    try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                        Dialect dialect = storedQuery.getDialect();
                        while (generatedKeys.next()) {
                            ids.add(getGeneratedIdentity(generatedKeys, identity, dialect));
                        }
                    }
                    Iterator iterator = ids.iterator();
                    for (Data d : entities) {
                        if (d.vetoed) {
                            continue;
                        }
                        if (!iterator.hasNext()) {
                            throw new DataAccessException("Failed to generate ID for entity: " + d.entity);
                        } else {
                            Object id = iterator.next();
                            d.entity = updateEntityId(identity.getProperty(), d.entity, id);
                        }
                    }
                }
                if (storedQuery.isOptimisticLock()) {
                    int expected = (int) entities.stream().filter(d -> !d.vetoed).count();
                    checkOptimisticLocking(expected, rowsUpdated);
                }
            }
        }

    }

    @SuppressWarnings("VisibilityModifier")
    protected static class JdbcOperationContext extends OperationContext {

        public final Connection connection;
        public final Dialect dialect;
        private final InvocationContext invocationContext;

        /**
         * The old deprecated constructor.
         *
         * @param annotationMetadata the annotation metadata
         * @param repositoryType the repository type
         * @param dialect the dialect
         * @param connection the connection
         * @deprecated Use constructor with {@link InvocationContext}.
         */
        @Deprecated
        public JdbcOperationContext(AnnotationMetadata annotationMetadata, Class repositoryType, Dialect dialect, Connection connection) {
            this(annotationMetadata, null , repositoryType, dialect, connection);
        }

        /**
         * The default constructor.
         *
         * @param annotationMetadata the annotation metadata
         * @param invocationContext the invocation context
         * @param repositoryType the repository type
         * @param dialect the dialect
         * @param connection the connection
         */
        public JdbcOperationContext(AnnotationMetadata annotationMetadata, InvocationContext invocationContext, Class repositoryType, Dialect dialect, Connection connection) {
            super(annotationMetadata, repositoryType);
            this.dialect = dialect;
            this.connection = connection;
            this.invocationContext = invocationContext;
        }
    }

    private static final class RuntimePersistentPropertyJdbcCC extends JdbcConversionContextImpl implements RuntimePersistentPropertyConversionContext {

        private final RuntimePersistentProperty property;

        public RuntimePersistentPropertyJdbcCC(Connection connection, RuntimePersistentProperty property) {
            super(ConversionContext.of(property.getArgument()), connection);
            this.property = property;
        }

        @Override
        public RuntimePersistentProperty getRuntimePersistentProperty() {
            return property;
        }
    }

    private static final class ArgumentJdbcCC extends JdbcConversionContextImpl implements ArgumentConversionContext {

        private final Argument argument;

        public ArgumentJdbcCC(Connection connection, Argument argument) {
            super(ConversionContext.of(argument), connection);
            this.argument = argument;
        }

        @Override
        public Argument getArgument() {
            return argument;
        }
    }

    private static class JdbcConversionContextImpl extends AbstractConversionContext
            implements JdbcConversionContext {

        private final Connection connection;

        public JdbcConversionContextImpl(Connection connection) {
            this(ConversionContext.DEFAULT, connection);
        }

        public JdbcConversionContextImpl(ConversionContext conversionContext, Connection connection) {
            super(conversionContext);
            this.connection = connection;
        }

        @Override
        public Connection getConnection() {
            return connection;
        }

    }

    private static final class ConnectionContext {

        private final Connection connection;
        private final boolean needsToBeClosed;

        private ConnectionContext(Connection connection, boolean needsToBeClosed) {
            this.connection = connection;
            this.needsToBeClosed = needsToBeClosed;
        }

        public Connection getConnection() {
            return connection;
        }
    }

}