com.mmnaseri.dragonfly.data.impl.DefaultDataAccess.java Source code

Java tutorial

Introduction

Here is the source code for com.mmnaseri.dragonfly.data.impl.DefaultDataAccess.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Milad Naseri.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.mmnaseri.dragonfly.data.impl;

import com.mmnaseri.dragonfly.data.*;
import com.mmnaseri.couteau.basics.api.Cache;
import com.mmnaseri.couteau.basics.api.Filter;
import com.mmnaseri.couteau.basics.api.Processor;
import com.mmnaseri.couteau.basics.api.Transformer;
import com.mmnaseri.couteau.basics.api.impl.EqualityFilter;
import com.mmnaseri.couteau.basics.api.impl.SimpleDataDispenser;
import com.mmnaseri.couteau.context.error.RegistryException;
import com.mmnaseri.couteau.reflection.beans.BeanInitializer;
import com.mmnaseri.couteau.reflection.beans.BeanWrapper;
import com.mmnaseri.couteau.reflection.beans.impl.ConstructorBeanInitializer;
import com.mmnaseri.couteau.reflection.beans.impl.MethodBeanWrapper;
import com.mmnaseri.couteau.reflection.error.BeanInstantiationException;
import com.mmnaseri.couteau.reflection.util.ReflectionUtils;
import com.mmnaseri.dragonfly.annotations.ParameterMode;
import com.mmnaseri.dragonfly.annotations.Partial;
import com.mmnaseri.dragonfly.entity.*;
import com.mmnaseri.dragonfly.entity.impl.*;
import com.mmnaseri.dragonfly.error.*;
import com.mmnaseri.dragonfly.events.DataAccessEventHandler;
import com.mmnaseri.dragonfly.events.EventHandlerContext;
import com.mmnaseri.dragonfly.events.impl.CompositeDataAccessEventHandler;
import com.mmnaseri.dragonfly.fluent.SelectQueryInitiator;
import com.mmnaseri.dragonfly.fluent.impl.DefaultSelectQueryInitiator;
import com.mmnaseri.dragonfly.metadata.*;
import com.mmnaseri.dragonfly.metadata.impl.ColumnMappingMetadataCollector;
import com.mmnaseri.dragonfly.metadata.impl.DefaultPagedResultOrderMetadata;
import com.mmnaseri.dragonfly.statement.*;
import com.mmnaseri.dragonfly.statement.Statement;
import com.mmnaseri.dragonfly.statement.impl.DefaultStatementPreparator;
import com.mmnaseri.dragonfly.statement.impl.DelegatingPreparedStatement;
import com.mmnaseri.dragonfly.statement.impl.FreemarkerSecondPassStatementBuilder;
import com.mmnaseri.dragonfly.statement.impl.ProcedureCallStatement;
import com.mmnaseri.dragonfly.tools.ColumnNameFilter;
import com.mmnaseri.dragonfly.tools.MapTools;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Serializable;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static com.mmnaseri.couteau.basics.collections.CollectionWrapper.with;

/**
 * <p>This class is the default implementation of the {@link DataAccess} interface.</p>
 *
 * <p>Some of the capabilities of the current implementation include:</p>
 *
 * <ul>
 *     <li>Highly traceable logging.</li>
 *     <li>Stacking batch operations of the same type together.</li>
 *     <li>Deferring deduction of the keys of inserted entities in batch operations.</li>
 *     <li>Connection piggy-backing for statements executed within the same thread.</li>
 *     <li>Support for complex event handler and extension points.</li>
 *     <li>Caching of delete statements for many-to-many relations within each thread.</li>
 *     <li>Freezing the element count view during batch operations until batch operations
 *     are successfully committed.</li>
 *     <li>Caching of initialized entities, until their persistent properties are modified
 *     externally.</li>
 *     <li>Support for operations on partial entities.</li>
 * </ul>
 *
 * <p><strong>NB</strong> This implementation of the data access interface does not provide
 * security measures. To use a secured version, you should instantiate {@link SecuredDataAccess},
 * instead.</p>
 *
 * @author Milad Naseri (mmnaseri@programmer.net)
 * @since 1.0 (2013/9/20, 23:29)
 */
public class DefaultDataAccess implements PartialDataAccess, EventHandlerContext, FluentDataAccess {

    private static final Log log = LogFactory.getLog(DataAccess.class);
    private static final Map<Statements.Manipulation, String> STATEMENTS = new ConcurrentHashMap<Statements.Manipulation, String>();
    private static final long SESSION_INITIALIZATION_TIMEOUT = 5000L;

    static {
        STATEMENTS.put(Statements.Manipulation.CALL, "call");
        STATEMENTS.put(Statements.Manipulation.DELETE_ALL, "deleteAll");
        STATEMENTS.put(Statements.Manipulation.DELETE_LIKE, "deleteLike");
        STATEMENTS.put(Statements.Manipulation.DELETE_ONE, "deleteByKey");
        STATEMENTS.put(Statements.Manipulation.DELETE_DEPENDENCIES, "deleteDependencies");
        STATEMENTS.put(Statements.Manipulation.DELETE_DEPENDENTS, "deleteDependents");
        STATEMENTS.put(Statements.Manipulation.FIND_ALL, "findAll");
        STATEMENTS.put(Statements.Manipulation.FIND_LIKE, "findLike");
        STATEMENTS.put(Statements.Manipulation.FIND_ONE, "findByKey");
        STATEMENTS.put(Statements.Manipulation.COUNT_ALL, "countAll");
        STATEMENTS.put(Statements.Manipulation.COUNT_LIKE, "countLike");
        STATEMENTS.put(Statements.Manipulation.COUNT_ONE, "countByKey");
        STATEMENTS.put(Statements.Manipulation.INSERT, "insert");
        STATEMENTS.put(Statements.Manipulation.TRUNCATE, "truncate");
        STATEMENTS.put(Statements.Manipulation.UPDATE, "updateBySample");
    }

    private final DataAccessSession session;
    private final EntityContext entityContext;
    private final EntityHandlerContext entityHandlerContext;
    private final BeanInitializer beanInitializer;
    private final ColumnMappingMetadataCollector metadataCollector;
    private final CompositeDataAccessEventHandler eventHandler;
    private final EntityInitializationContext initializationContext;
    private final RowHandler rowHandler;
    private final ThreadLocal<Map<Object, Object>> saveQueue;
    private final ThreadLocal<Set<Object>> deferredSaveQueue;
    private final ThreadLocal<Long> saveQueueLock;
    private final ThreadLocal<Set<Object>> deleteQueue;
    private final EntityMapCreator mapCreator;
    private final MapEntityCreator entityCreator;
    private final Map<Class<?>, Collection<ColumnMetadata>> partialEntityColumns = new ConcurrentHashMap<Class<?>, Collection<ColumnMetadata>>();
    private final ThreadLocal<Map<Class<?>, Map<Statements.Manipulation, Set<Statement>>>> deleteAllStatements;
    private final StatementPreparator statementPreparator;
    private final ThreadLocal<List<BatchOperationDescriptor>> batchOperation;
    private final ThreadLocal<List<Object>> deferredKeys;
    private final ThreadLocal<Boolean> batch;
    private final ThreadLocal<Set<LocalOperationResult>> localCounts;
    private final ThreadLocal<Stack<PreparedStatement>> localStatements;

    public DefaultDataAccess(DataAccessSession session, EntityContext entityContext,
            EntityHandlerContext entityHandlerContext, boolean autoInitialize) {
        this.session = session;
        this.entityContext = entityContext;
        this.entityHandlerContext = entityHandlerContext;
        this.beanInitializer = new ConstructorBeanInitializer();
        this.metadataCollector = new ColumnMappingMetadataCollector();
        this.eventHandler = new CompositeDataAccessEventHandler();
        this.initializationContext = new ThreadLocalEntityInitializationContext(this);
        this.rowHandler = new DefaultRowHandler();
        this.mapCreator = new DefaultEntityMapCreator();
        try {
            this.entityCreator = new DefaultMapEntityCreator();
        } catch (RegistryException e) {
            throw new DataAccessSessionInitializationError("Failed to initialize the map-to-entity converter", e);
        }
        this.saveQueue = new ThreadLocal<Map<Object, Object>>() {
            @Override
            protected Map<Object, Object> initialValue() {
                return new HashMap<Object, Object>();
            }
        };
        this.deleteQueue = new ThreadLocal<Set<Object>>() {
            @Override
            protected Set<Object> initialValue() {
                return new HashSet<Object>();
            }
        };
        this.deleteAllStatements = new ThreadLocal<Map<Class<?>, Map<Statements.Manipulation, Set<Statement>>>>() {
            @Override
            protected Map<Class<?>, Map<Statements.Manipulation, Set<Statement>>> initialValue() {
                return new HashMap<Class<?>, Map<Statements.Manipulation, Set<Statement>>>();
            }
        };
        this.batchOperation = new ThreadLocal<List<BatchOperationDescriptor>>();
        this.batch = new ThreadLocal<Boolean>() {
            @Override
            protected Boolean initialValue() {
                return false;
            }
        };
        this.entityContext.initialize(this);
        this.statementPreparator = new DefaultStatementPreparator(false);
        this.deferredKeys = new ThreadLocal<List<Object>>() {
            @Override
            protected List<Object> initialValue() {
                return new ArrayList<Object>();
            }
        };
        this.deferredSaveQueue = new ThreadLocal<Set<Object>>() {
            @Override
            protected Set<Object> initialValue() {
                return new HashSet<Object>();
            }
        };
        this.saveQueueLock = new ThreadLocal<Long>() {
            @Override
            protected Long initialValue() {
                return 0L;
            }
        };
        this.localCounts = new ThreadLocal<Set<LocalOperationResult>>() {
            @Override
            protected Set<LocalOperationResult> initialValue() {
                return new HashSet<LocalOperationResult>();
            }
        };
        this.localStatements = new ThreadLocal<Stack<PreparedStatement>>() {
            @Override
            protected Stack<PreparedStatement> initialValue() {
                return new Stack<PreparedStatement>();
            }
        };
        if (autoInitialize) {
            log.info("Automatically initializing the session");
            synchronized (this.session) {
                if (!this.session.isInitialized()) {
                    this.session.initialize();
                    this.session.markInitialized();
                }
            }
        }
    }

    /**
     * Connection handling
     */

    private Connection openConnection() {
        if (localStatements.get().isEmpty()) {
            return session.getConnection();
        } else {
            try {
                return localStatements.get().peek().getConnection();
            } catch (SQLException e) {
                throw new DatabaseConnectionError(e);
            }
        }
    }

    private void closeConnection(Connection connection) throws SQLException {
        if (localStatements.get().isEmpty()) {
            connection.close();
        }
    }

    private <S extends PreparedStatement> S openStatement(S preparedStatement) {
        localStatements.get().push(preparedStatement);
        return preparedStatement;
    }

    private void closeStatement(PreparedStatement preparedStatement) throws SQLException {
        localStatements.get().pop();
        preparedStatement.close();
    }

    /**
     * Internal update methods
     * These are helpers for the rest of the interface
     */

    private int getUpdateCount(PreparedStatement preparedStatement) {
        try {
            return preparedStatement.getUpdateCount();
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to retrieve the number of affected rows", e);
        }
    }

    private PreparedStatement internalExecuteUpdate(Class<?> entityType, Statements.Manipulation statementName) {
        return internalExecuteUpdate(entityType, statementName, Collections.<String, Object>emptyMap());
    }

    private PreparedStatement internalExecuteUpdate(Class<?> entityType, Statements.Manipulation statement,
            Map<String, Object> values) {
        return internalExecuteUpdate(getStatement(entityType, statement, null, StatementType.INSERT,
                StatementType.DELETE, StatementType.UPDATE, StatementType.TRUNCATE), values);
    }

    private PreparedStatement internalExecuteUpdate(Class<?> entityType, String statement,
            Map<String, Object> values) {
        return internalExecuteUpdate(getStatement(entityType, statement, null, StatementType.INSERT,
                StatementType.DELETE, StatementType.UPDATE, StatementType.TRUNCATE), values);
    }

    private PreparedStatement internalExecuteUpdate(Statement statement, Map<String, Object> values) {
        waitForSessionInitialization();
        if (isInBatchMode()) {
            if (batchOperation.get() == null) {
                batchOperation.set(new Stack<BatchOperationDescriptor>());
            }
            final List<BatchOperationDescriptor> operationDescriptors = batchOperation.get();
            boolean firstStep = operationDescriptors.isEmpty();
            if (!firstStep) {
                String sql = statement.getSql();
                if (statement.isDynamic()) {
                    sql = new FreemarkerSecondPassStatementBuilder(statement, session.getDatabaseDialect(), values)
                            .getStatement(statement.getTableMetadata()).getSql();
                }
                firstStep = !sql.equals(operationDescriptors.get(operationDescriptors.size() - 1).getSql());
            }
            final PreparedStatement preparedStatement;
            if (!firstStep) {
                preparedStatement = operationDescriptors.get(operationDescriptors.size() - 1)
                        .getPreparedStatement();
                statementPreparator.prepare(preparedStatement, statement.getTableMetadata(), values,
                        operationDescriptors.get(operationDescriptors.size() - 1).getSql());
            } else {
                final BatchOperationDescriptor operationDescriptor = getPreparedStatement(statement, values);
                operationDescriptors.add(operationDescriptor);
                preparedStatement = operationDescriptor.getPreparedStatement();
            }
            try {
                preparedStatement.addBatch();
            } catch (SQLException e) {
                throw new BatchOperationExecutionError("Failed to add batch operation", e);
            }
            return preparedStatement;
        } else {
            final PreparedStatement preparedStatement = getPreparedStatement(statement, values)
                    .getPreparedStatement();
            try {
                preparedStatement.executeUpdate();
            } catch (SQLException e) {
                throw new UnsuccessfulOperationError("Failed to execute update", e);
            }
            return preparedStatement;
        }
    }

    private void waitForSessionInitialization() {
        long time = System.currentTimeMillis();
        while (!session.isInitialized()) {
            if (System.currentTimeMillis() - time > SESSION_INITIALIZATION_TIMEOUT) {
                throw new DataAccessSessionInitializationError("Session initialization timed out after "
                        + (System.currentTimeMillis() - time) + " milliseconds");
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new DataAccessSessionInitializationError("Session initialization was interrupted", e);
            }
        }
    }

    private synchronized BatchOperationDescriptor getPreparedStatement(Statement statement,
            Map<String, Object> values) {
        final Connection connection = openConnection();
        if (isInBatchMode()) {
            try {
                connection.setAutoCommit(false);
            } catch (SQLException e) {
                throw new BatchOperationExecutionError(
                        "Failed to disable auto-commit mode for the current connection", e);
            }
        }
        final Statement finalStatement;
        if (statement.isDynamic()) {
            finalStatement = new FreemarkerSecondPassStatementBuilder(statement, session.getDatabaseDialect(),
                    values).getStatement(statement.getTableMetadata());
        } else {
            finalStatement = statement;
        }
        final PreparedStatement preparedStatement = openStatement(
                new DelegatingPreparedStatement(finalStatement.prepare(connection, null, values), connection));
        return new BatchOperationDescriptor(preparedStatement, finalStatement.getSql());
    }

    /**
     * Internal query methods
     */

    private <E> List<E> internalExecuteQuery(Class<E> entityType, Statements.Manipulation statement,
            ResultOrderMetadata ordering) {
        return internalExecuteQuery(entityType, statement, Collections.<String, Object>emptyMap(), ordering);
    }

    private <E> List<E> internalExecuteQuery(Class<E> entityType, Statements.Manipulation statement,
            Map<String, Object> values, ResultOrderMetadata ordering) {
        return internalExecuteQuery(entityType, STATEMENTS.get(statement), values, ordering);
    }

    private <E> List<E> internalExecuteQuery(Class<E> entityType, String statementName, Map<String, Object> values,
            ResultOrderMetadata ordering) {
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entityType);
        final List<Map<String, Object>> maps = internalExecuteUntypedQuery(entityType, statementName, values,
                ordering);
        final ArrayList<E> result = new ArrayList<E>();
        for (Map<String, Object> entityMap : maps) {
            final E instance = entityContext.getInstance(entityType);
            entityHandler.fromMap(instance, entityMap);
            if (entityHandler.hasKey()) {
                final Serializable key = entityHandler.getKey(instance);
                if (initializationContext.contains(entityType, key)) {
                    result.add(initializationContext.get(entityType, key));
                    continue;
                }
            }
            prepareEntity(instance, entityMap);
            result.add(instance);
        }
        return result;
    }

    private <E> List<Map<String, Object>> internalExecuteUntypedQuery(Class<E> entityType,
            Statements.Manipulation statement, Map<String, Object> values, ResultOrderMetadata ordering) {
        return internalExecuteUntypedQuery(entityType, STATEMENTS.get(statement), values, ordering);
    }

    private <E> List<Map<String, Object>> internalExecuteUntypedQuery(Class<E> entityType, String statementName,
            Map<String, Object> values, ResultOrderMetadata ordering) {
        if (isInBatchMode() && !statementName.startsWith("count")) {
            throw new BatchOperationInterruptedByReadError();
        }
        waitForSessionInitialization();
        final Statement statement = getStatement(entityType, statementName, ordering, StatementType.QUERY);
        final Connection connection = openConnection();
        final PreparedStatement preparedStatement = openStatement(statement.prepare(connection, null, values));
        final ArrayList<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        try {
            final ResultSet resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                result.add(rowHandler.handleRow(resultSet));
            }
            resultSet.close();
            closeStatement(preparedStatement);
            closeConnection(connection);
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to retrieve result set from the database", e);
        }
        return result;
    }

    private <E> long internalCount(Class<E> entityType, Statements.Manipulation statement,
            Map<String, Object> values) {
        final LocalOperationResult result = new LocalOperationResult(entityType, statement, values);
        if (isInBatchMode() && localCounts.get().contains(result)) {
            return (Long) with(localCounts.get()).find(new EqualityFilter<LocalOperationResult>(result))
                    .getResult();
        }
        final List<Map<String, Object>> list = internalExecuteUntypedQuery(entityType, statement, values, null);
        if (list.size() != 1) {
            throw new UnsuccessfulOperationError("Failed to execute statement");
        }
        final Long value = (Long) list.get(0).get(with(list.get(0).keySet()).find(new Filter<String>() {
            @Override
            public boolean accepts(String item) {
                return session.getDatabaseDialect().getCountColumn().equalsIgnoreCase(item);
            }
        }));
        if (isInBatchMode()) {
            result.setResult(value);
            localCounts.get().add(result);
        }
        return value;
    }

    /**
     * Internal statement access methods
     */

    private Statement getStatement(Class<?> entityType, Statements.Manipulation statement,
            ResultOrderMetadata ordering, StatementType... expected) {
        return getStatement(entityType, STATEMENTS.get(statement), ordering, expected);
    }

    private Statement getStatement(Class<?> entityType, final String statementName, ResultOrderMetadata ordering,
            StatementType... expected) {
        Statement result;
        try {
            if (ordering == null) {
                result = session.getStatementRegistry(entityType).get(statementName);
            } else {
                final StatementBuilder statementBuilder = session.getDatabaseDialect().getStatementBuilderContext()
                        .getManipulationStatementBuilder(
                                with(STATEMENTS.keySet()).find(new Filter<Statements.Manipulation>() {
                                    @Override
                                    public boolean accepts(Statements.Manipulation item) {
                                        return STATEMENTS.get(item).equals(statementName);
                                    }
                                }));
                ordering = new DefaultPagedResultOrderMetadata(ordering);
                result = statementBuilder
                        .getStatement(session.getTableMetadataRegistry().getTableMetadata(entityType), ordering);
            }
        } catch (RegistryException e) {
            throw new NoSuchQueryError(entityType, statementName);
        }
        if (expected.length > 0) {
            boolean found = false;
            for (StatementType statementType : expected) {
                if (statementType.equals(result.getType())) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                throw new InvalidStatementTypeError(result.getType());
            }
        }
        return result;
    }

    private <E> InitializedEntity<E> getInitializedEntity(E entity) {
        //noinspection unchecked
        return (InitializedEntity<E>) getEnhancedEntity(entity);
    }

    private <E> E getEnhancedEntity(E entity) {
        if (entityContext.has(entity)) {
            return entity;
        }
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final E result = entityContext.getInstance(entityHandler.getEntityType());
        entityHandler.copy(entity, result);
        if (result instanceof DataAccessAware) {
            ((DataAccessAware) result).setDataAccess(this);
        }
        if (result instanceof DataAccessSessionAware) {
            ((DataAccessSessionAware) result).setDataAccessSession(session);
        }
        if (session instanceof DefaultDataAccessSession && result instanceof DataStructureHandlerAware) {
            ((DataStructureHandlerAware) result)
                    .setDataStructureHandler(((DefaultDataAccessSession) session).getDataStructureHandler());
        }
        return result;
    }

    private <E> void prepareEntity(final E entity, Map<String, Object> values) {
        final E enhancedEntity = getEnhancedEntity(entity);
        final InitializedEntity<E> initializedEntity = getInitializedEntity(enhancedEntity);
        initializedEntity.setOriginalCopy(enhancedEntity);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final EntityInitializationContext initializationContext;
        if (initializedEntity.getInitializationContext() != null) {
            initializationContext = initializedEntity.getInitializationContext();
        } else {
            initializationContext = new DefaultEntityInitializationContext(this, this.initializationContext);
            final Serializable key = entityHandler.hasKey() ? entityHandler.getKey(enhancedEntity) : null;
            if (key != null) {
                initializationContext.register(entityHandler.getEntityType(), key, enhancedEntity);
            }
            initializedEntity.setInitializationContext(initializationContext);
        }
        initializationContext.lock();
        initializedEntity.setMap(values);
        entityHandler.incrementVersion(enhancedEntity);
        entityHandler.loadEagerRelations(enhancedEntity, values, initializationContext);
        final TableMetadata<E> tableMetadata = session.getTableMetadataRegistry()
                .getTableMetadata(entityHandler.getEntityType());
        final BeanWrapper<E> wrapper = new MethodBeanWrapper<E>(enhancedEntity);
        with(tableMetadata.getForeignReferences()).forThose(new Filter<RelationMetadata<E, ?>>() {
            @Override
            public boolean accepts(RelationMetadata<E, ?> item) {
                return !item.isLazy() && item.getType().equals(RelationType.MANY_TO_MANY);
            }
        }, new Processor<RelationMetadata<E, ?>>() {
            @Override
            public void process(RelationMetadata<E, ?> reference) {
                final Connection connection = openConnection();
                final TableMetadata<?> middleTable = reference.getForeignTable();
                final ManyToManyActionHelper helper = new ManyToManyActionHelper(statementPreparator, connection,
                        session.getDatabaseDialect().getStatementBuilderContext(), middleTable, tableMetadata,
                        reference, entityContext);
                final ManyToManyMiddleEntity middleEntity = new ManyToManyMiddleEntity();
                final BeanWrapper<ManyToManyMiddleEntity> middleEntityWrapper = new MethodBeanWrapper<ManyToManyMiddleEntity>(
                        middleEntity);
                try {
                    middleEntityWrapper.setPropertyValue(with(middleTable.getColumns())
                            .find(new ColumnNameFilter(tableMetadata.getName())).getPropertyName(), enhancedEntity);
                    final Collection<Object> collection = ReflectionUtils
                            .getCollection(wrapper.getPropertyType(reference.getPropertyName()));
                    collection.addAll(helper.find(middleEntity, new EntityPreparationCallback() {
                        @Override
                        public void prepare(Object entity, Map<String, Object> values) {
                            prepareEntity(entity, values);
                        }
                    }, new Transformer<Object, Object>() {
                        @Override
                        public Object map(Object input) {
                            final EntityHandler<Object> handler = entityHandlerContext.getHandler(input);
                            if (handler.hasKey() && handler.getKey(input) != null && initializationContext
                                    .contains(handler.getEntityType(), handler.getKey(input))) {
                                final Object cached;
                                if (entityHandler.hasKey()) {
                                    cached = initializationContext.get(handler.getEntityType(),
                                            handler.getKey(input), entityHandler.getEntityType(),
                                            entityHandler.getKey(entity));
                                } else {
                                    cached = initializationContext.get(handler.getEntityType(),
                                            handler.getKey(input));
                                }
                                return cached;
                            }
                            return null;
                        }
                    }));
                    wrapper.setPropertyValue(reference.getPropertyName(), collection);
                } catch (Exception e) {
                    throw new EntityInitializationError(entityHandler.getEntityType(), e);
                }
                try {
                    closeConnection(connection);
                } catch (SQLException e) {
                    throw new EntityInitializationError(entityHandler.getEntityType(), e);
                }
            }
        });
        initializationContext.unlock();
    }

    /**
     * Internal methods for handling partial data requests
     */

    private synchronized Collection<ColumnMetadata> getPartialEntityMetadata(Class<?> partialEntityType) {
        if (!partialEntityColumns.containsKey(partialEntityType)) {
            partialEntityColumns.put(partialEntityType, metadataCollector.collectMetadata(partialEntityType));
        }
        return partialEntityColumns.get(partialEntityType);
    }

    /**
     * Batch processing methods
     */

    private Boolean isInBatchMode() {
        return batch.get();
    }

    private synchronized void startBatch() {
        log.info("Starting batch operation");
        if (isInBatchMode()) {
            throw new BatchOperationAlreadyStartedError();
        }
        batch.set(true);
    }

    private synchronized List<Integer> endBatch() {
        if (!isInBatchMode()) {
            throw new NoBatchOperationError();
        }
        localCounts.get().clear();
        final List<BatchOperationDescriptor> descriptors = batchOperation.get();
        batchOperation.remove();
        batch.set(false);
        final ArrayList<Integer> result = new ArrayList<Integer>();
        if (descriptors == null) {
            return result;
        }
        log.info("There are " + descriptors.size() + " operation stack(s) to perform");
        while (!descriptors.isEmpty()) {
            final BatchOperationDescriptor descriptor = descriptors.get(0);
            descriptors.remove(0);
            final int[] batchResult;
            log.info("Executing batch operation for statement: " + descriptor.getSql());
            final PreparedStatement preparedStatement = descriptor.getPreparedStatement();
            final Connection connection;
            try {
                connection = preparedStatement.getConnection();
                long time = System.nanoTime();
                batchResult = preparedStatement.executeBatch();
                connection.commit();
                log.info(batchResult.length + " operation(s) completed successfully in "
                        + (System.nanoTime() - time) + "ns");
            } catch (SQLException e) {
                throw new BatchOperationExecutionError("Failed to execute operation batch", e);
            }
            if (StatementType.getStatementType(descriptor.getSql()).equals(StatementType.INSERT)) {
                try {
                    final List<Object> deferredEntities = deferredKeys.get();
                    final ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
                    while (generatedKeys.next()) {
                        final Object entity = deferredEntities.get(0);
                        deferredEntities.remove(0);
                        final EntityHandler<Object> entityHandler = entityHandlerContext.getHandler(entity);
                        entityHandler.setKey(entity, session.getDatabaseDialect().retrieveKey(generatedKeys));
                    }
                } catch (SQLException e) {
                    throw new BatchOperationExecutionError("Failed to retrieve generated keys", e);
                }
            }
            for (int i : batchResult) {
                result.add(i);
            }
            cleanUpStatement(preparedStatement);
        }
        return result;
    }

    /**
     * Partial data access support
     */

    @Override
    public <O> List<O> executeQuery(O sample) {
        final Class<?> partialEntity = sample.getClass();
        //noinspection unchecked
        return (List<O>) executeQuery(partialEntity,
                mapCreator.toMap(getPartialEntityMetadata(partialEntity), sample));
    }

    @Override
    public <O> List<O> executeQuery(Class<O> resultType) {
        return executeQuery(resultType, Collections.<String, Object>emptyMap());
    }

    @Override
    public <O> List<O> executeQuery(Class<O> resultType, Map<String, Object> values) {
        if (!resultType.isAnnotationPresent(Partial.class)) {
            throw new PartialEntityDefinitionError("Expected to find @Partial on " + resultType.getCanonicalName());
        }
        final Partial annotation = resultType.getAnnotation(Partial.class);
        final List<Map<String, Object>> maps = executeUntypedQuery(annotation.targetEntity(), annotation.query(),
                values);
        final ArrayList<O> result = new ArrayList<O>();
        for (Map<String, Object> map : maps) {
            final O partialEntity;
            try {
                partialEntity = beanInitializer.initialize(resultType, new Class[0]);
            } catch (BeanInstantiationException e) {
                throw new EntityInitializationError(resultType, e);
            }
            result.add(entityCreator.fromMap(partialEntity, getPartialEntityMetadata(resultType), map));
        }
        return result;
    }

    @Override
    public <E> List<Map<String, Object>> executeUntypedQuery(Class<E> entityType, String queryName,
            Map<String, Object> values) {
        final ArrayList<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        final Statement statement = getStatement(entityType, queryName, null, StatementType.QUERY);
        final Connection connection = openConnection();
        final PreparedStatement preparedStatement = openStatement(statement.prepare(connection, null, values));
        final ResultSet resultSet;
        try {
            resultSet = preparedStatement.executeQuery();
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError(
                    "Failed to execute query " + entityType.getCanonicalName() + "." + queryName, e);
        }
        try {
            while (resultSet.next()) {
                result.add(rowHandler.handleRow(resultSet));
            }
            resultSet.close();
            closeStatement(preparedStatement);
            closeConnection(connection);
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to load result set", e);
        }
        return result;
    }

    @Override
    public <E, R> List<R> executeTypedQuery(Class<E> entityType, String queryName, Class<R> resultType,
            Map<String, Object> values) {
        final List<Map<String, Object>> maps = executeUntypedQuery(entityType, queryName, values);
        final ArrayList<R> list = new ArrayList<R>();
        for (Map<String, Object> map : maps) {
            if (map.size() > 1) {
                throw new QueryDefinitionError(entityType, queryName,
                        "Query has more than one column to choose from");
            } else if (map.isEmpty()) {
                continue;
            }
            final Object value = map.values().iterator().next();
            if (resultType.isInstance(value)) {
                list.add(resultType.cast(value));
            } else {
                throw new QueryDefinitionError(entityType, queryName,
                        "Expected value to be of type " + resultType.getCanonicalName() + " while it was " + value);
            }
        }
        return list;
    }

    @Override
    public <E> List<Object> executeTypedQuery(Class<E> entityType, String queryName, Map<String, Object> values) {
        return executeTypedQuery(entityType, queryName, Object.class, values);
    }

    @Override
    public <O> int executeUpdate(O sample) {
        final Class<?> resultType = sample.getClass();
        if (!resultType.isAnnotationPresent(Partial.class)) {
            throw new PartialEntityDefinitionError("Expected to find @Partial on " + resultType.getCanonicalName());
        }
        final Partial annotation = resultType.getAnnotation(Partial.class);
        final Statement statement = getStatement(annotation.targetEntity(), annotation.query(), null,
                StatementType.INSERT, StatementType.DELETE, StatementType.UPDATE);
        final PreparedStatement preparedStatement = openStatement(
                statement.prepare(openConnection(), mapCreator, sample));
        try {
            final int affectedRows = preparedStatement.executeUpdate();
            cleanUpStatement(preparedStatement);
            return affectedRows;
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to execute update", e);
        }
    }

    /**
     * Data access methods
     */

    @Override
    public <E> E save(E entity) {
        final Map<Object, Object> saveQueue = this.saveQueue.get();
        if (saveQueue.containsKey(entity)) {
            //noinspection unchecked
            return (E) saveQueue.get(entity);
        }
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final E enhancedEntity = getEnhancedEntity(entity);
        saveQueueLock.set(saveQueueLock.get() + 1);
        if (!enhancedEntity.equals(entity)) {
            saveQueue.put(entity, enhancedEntity);
        }
        eventHandler.beforeSave(enhancedEntity);
        boolean shouldUpdate = entityHandler.hasKey() && entityHandler.isKeyAutoGenerated()
                && entityHandler.getKey(enhancedEntity) != null;
        if (!shouldUpdate) {
            insert(enhancedEntity);
        } else {
            update(enhancedEntity);
        }
        eventHandler.afterSave(enhancedEntity);
        entityHandler.copy(enhancedEntity, entity);
        saveQueueLock.set(saveQueueLock.get() - 1);
        if (saveQueueLock.get() == 0) {
            saveQueue.remove(entity);
            for (Object object : deferredSaveQueue.get()) {
                saveQueue.remove(object);
            }
            deferredSaveQueue.get().clear();
        } else {
            deferredSaveQueue.get().add(entity);
        }
        return enhancedEntity;
    }

    @Override
    public <E> E insert(E entity) {
        final Map<Object, Object> saveQueue = this.saveQueue.get();
        if (saveQueue.containsKey(entity)) {
            //noinspection unchecked
            return (E) saveQueue.get(entity);
        }
        saveQueueLock.set(saveQueueLock.get() + 1);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final E enhancedEntity = getEnhancedEntity(entity);
        saveQueue.put(entity, enhancedEntity);
        final InitializedEntity<E> initializedEntity = getInitializedEntity(enhancedEntity);
        initializedEntity.freeze();
        entityHandler.initializeVersion(enhancedEntity);
        final Map<String, Object> sequenceValues = new HashMap<String, Object>();
        sequenceValues.putAll(session.getDatabaseDialect().loadTableValues(
                session.getTableMetadataRegistry().getTableMetadata(TableKeyGeneratorEntity.class),
                session.getTableMetadataRegistry().getTableMetadata(entityHandler.getEntityType()), session));
        sequenceValues.putAll(session.getDatabaseDialect().loadSequenceValues(
                session.getTableMetadataRegistry().getTableMetadata(entityHandler.getEntityType())));
        entityHandler.fromMap(enhancedEntity, sequenceValues);
        entityHandler.saveDependencyRelations(enhancedEntity, this);
        eventHandler.beforeInsert(enhancedEntity);
        final PreparedStatement preparedStatement = internalExecuteUpdate(entityHandler.getEntityType(),
                Statements.Manipulation.INSERT, MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value."));
        if (entityHandler.hasKey() && entityHandler.isKeyAutoGenerated()) {
            if (isInBatchMode()) {
                deferredKeys.get().add(enhancedEntity);
            } else {
                try {
                    final ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
                    if (generatedKeys.next()) {
                        entityHandler.setKey(enhancedEntity,
                                session.getDatabaseDialect().retrieveKey(generatedKeys));
                    }
                    if (initializedEntity.getInitializationContext() == null) {
                        final DefaultEntityInitializationContext entityInitializationContext = new DefaultEntityInitializationContext(
                                this, initializationContext);
                        entityInitializationContext.register(entityHandler.getEntityType(),
                                entityHandler.getKey(enhancedEntity), enhancedEntity);
                        initializedEntity.setInitializationContext(entityInitializationContext);
                    }
                } catch (SQLException e) {
                    throw new UnsupportedOperationException("Failed to retrieve auto-generated keys", e);
                }
            }
        }
        entityHandler.incrementVersion(enhancedEntity);
        if (!isInBatchMode()) {
            cleanUpStatement(preparedStatement);
        }
        initializedEntity.setOriginalCopy(enhancedEntity);
        eventHandler.afterInsert(enhancedEntity);
        saveDependents(entityHandler, enhancedEntity);
        saveQueueLock.set(saveQueueLock.get() - 1);
        if (saveQueueLock.get() == 0) {
            saveQueue.remove(entity);
            for (Object object : deferredSaveQueue.get()) {
                saveQueue.remove(object);
            }
            deferredSaveQueue.get().clear();
        } else {
            deferredSaveQueue.get().add(entity);
        }
        entityHandler.copy(enhancedEntity, entity);
        initializedEntity.unfreeze();
        return enhancedEntity;
    }

    private <E> void saveDependents(final EntityHandler<E> entityHandler, E entity) {
        entityHandler.saveDependentRelations(entity, this, entityContext);
        final Map<TableMetadata<?>, Set<ManyToManyMiddleEntity>> relatedObjects = entityHandler
                .getManyToManyRelatedObjects(entity);
        final Connection connection = openConnection();
        try {
            final Cache<TableMetadata<?>, ManyToManyActionHelper> helpers = new SimpleDataDispenser<TableMetadata<?>, ManyToManyActionHelper>() {
                @Override
                protected ManyToManyActionHelper produce(TableMetadata<?> tableMetadata) {
                    return new ManyToManyActionHelper(statementPreparator, connection,
                            session.getDatabaseDialect().getStatementBuilderContext(), tableMetadata,
                            session.getTableMetadataRegistry().getTableMetadata(entityHandler.getEntityType()),
                            null, entityContext);
                }
            };
            for (Map.Entry<TableMetadata<?>, Set<ManyToManyMiddleEntity>> entry : relatedObjects.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    continue;
                }
                final ManyToManyMiddleEntity someRelation = entry.getValue().iterator().next();
                final ManyToManyActionHelper helper = helpers.read(entry.getKey());
                helper.delete(someRelation);
                if (entry.getValue().size() == 1 && !someRelation.isComplete()) {
                    helper.close();
                    continue;
                }
                for (ManyToManyMiddleEntity middleEntity : entry.getValue()) {
                    helper.insert(middleEntity);
                    helper.invalidate(middleEntity, initializationContext, entityHandlerContext);
                }
                helper.close();
            }
            closeConnection(connection);
        } catch (Exception e) {
            throw new UnsuccessfulOperationError("Failed to save dependent many-to-many objects", e);
        }
    }

    private void cleanUpStatement(PreparedStatement preparedStatement) {
        try {
            final Connection connection = preparedStatement.getConnection();
            closeStatement(preparedStatement);
            closeConnection(connection);
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to clean up", e);
        }
    }

    @Override
    public <E> E update(E entity) {
        final Map<Object, Object> saveQueue = this.saveQueue.get();
        if (saveQueue.containsKey(entity)) {
            //noinspection unchecked
            return (E) saveQueue.get(entity);
        }
        saveQueueLock.set(saveQueueLock.get() + 1);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final E enhancedEntity = getEnhancedEntity(entity);
        if (entityHandler.hasKey() && entityHandler.getKey(enhancedEntity) != null) {
            initializationContext.delete(entityHandler.getEntityType(), entityHandler.getKey(enhancedEntity));
        }
        saveQueue.put(entity, enhancedEntity);
        final InitializedEntity<E> initializedEntity = getInitializedEntity(enhancedEntity);
        entityHandler.saveDependencyRelations(enhancedEntity, this);
        eventHandler.beforeUpdate(enhancedEntity);
        final Map<String, Object> current = entityHandler.toMap(enhancedEntity);
        final Map<String, Object> values = new HashMap<String, Object>();
        values.putAll(MapTools.prefixKeys(current, "value."));
        values.putAll(MapTools.prefixKeys(current, "new."));
        if (initializedEntity.getOriginalCopy() == null) {
            initializedEntity.setOriginalCopy(enhancedEntity);
        }
        final E originalCopy = initializedEntity.getOriginalCopy();
        initializedEntity.freeze();
        final Map<String, Object> original = entityHandler.toMap(originalCopy);
        values.putAll(MapTools.prefixKeys(original, "old."));
        for (String key : original.keySet()) {
            if (!values.containsKey("value." + key)) {
                values.put("value." + key, original.get(key));
            }
        }
        final PreparedStatement preparedStatement = internalExecuteUpdate(entityHandler.getEntityType(),
                Statements.Manipulation.UPDATE, values);
        try {
            final boolean updated = preparedStatement.getUpdateCount() > 0;
            if (entityHandler.isLockable() && !updated) {
                throw new OptimisticLockingFailureError(entityHandler.getEntityType());
            }
            entityHandler.incrementVersion(enhancedEntity);
            eventHandler.afterUpdate(enhancedEntity, updated);
        } catch (SQLException e) {
            throw new UnsuccessfulOperationError("Failed to count the number of updated elements", e);
        }
        cleanUpStatement(preparedStatement);
        saveDependents(entityHandler, enhancedEntity);
        saveQueueLock.set(saveQueueLock.get() - 1);
        if (saveQueueLock.get() == 0) {
            saveQueue.remove(entity);
            for (Object object : deferredSaveQueue.get()) {
                saveQueue.remove(object);
            }
            deferredSaveQueue.get().clear();
        } else {
            deferredSaveQueue.get().add(entity);
        }
        entityHandler.copy(enhancedEntity, entity);
        if (entityHandler.hasKey() && entityHandler.getKey(enhancedEntity) != null) {
            initializationContext.register(entityHandler.getEntityType(), entityHandler.getKey(enhancedEntity),
                    enhancedEntity);
        }
        initializedEntity.unfreeze();
        return enhancedEntity;
    }

    @Override
    public <E> void delete(E entity) {
        final Set<Object> deleteQueue = this.deleteQueue.get();
        if (deleteQueue.contains(entity)) {
            return;
        }
        deleteQueue.add(entity);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entity);
        final E enhancedEntity = getEnhancedEntity(entity);
        final InitializedEntity<E> initializedEntity = getInitializedEntity(enhancedEntity);
        initializedEntity.freeze();
        eventHandler.beforeDelete(enhancedEntity);
        final Map<String, Object> map = MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value.");
        if ((entityHandler.hasKey() && entityHandler.getKey(enhancedEntity) != null
                && exists(entityHandler.getEntityType(), entityHandler.getKey(enhancedEntity)))
                || exists(entityHandler.getEntityType(), map)) {
            deleteDependencies(entityHandler, enhancedEntity);
            final Statements.Manipulation statement;
            if (entityHandler.hasKey() && entityHandler.getKey(enhancedEntity) != null) {
                statement = Statements.Manipulation.DELETE_ONE;
            } else {
                statement = Statements.Manipulation.DELETE_LIKE;
            }
            cleanUpStatement(internalExecuteUpdate(entityHandler.getEntityType(), statement, map));
            entityHandler.deleteDependentRelations(enhancedEntity, this);
        }
        eventHandler.afterDelete(enhancedEntity);
        initializedEntity.unfreeze();
        deleteQueue.remove(entity);
    }

    private <E> void deleteDependencies(final EntityHandler<E> entityHandler, final E enhancedEntity) {
        entityHandler.deleteDependencyRelations(enhancedEntity, this);
        final TableMetadata<E> tableMetadata = session.getTableMetadataRegistry()
                .getTableMetadata(entityHandler.getEntityType());
        final Connection connection = openConnection();
        with(tableMetadata.getForeignReferences()).forThose(new Filter<RelationMetadata<E, ?>>() {
            @Override
            public boolean accepts(RelationMetadata<E, ?> item) {
                return item.getCascadeMetadata().cascadeRemove()
                        && RelationType.MANY_TO_MANY.equals(item.getType());
            }
        }, new Processor<RelationMetadata<E, ?>>() {
            @Override
            public void process(RelationMetadata<E, ?> reference) {
                final TableMetadata<?> middleTable = reference.getForeignTable();
                final ManyToManyActionHelper helper = new ManyToManyActionHelper(statementPreparator, connection,
                        session.getDatabaseDialect().getStatementBuilderContext(), middleTable, tableMetadata,
                        reference, entityContext);
                final ManyToManyMiddleEntity middleEntity = new ManyToManyMiddleEntity();
                final BeanWrapper<ManyToManyMiddleEntity> middleEntityWrapper = new MethodBeanWrapper<ManyToManyMiddleEntity>(
                        middleEntity);
                try {
                    middleEntityWrapper.setPropertyValue(with(middleTable.getColumns())
                            .find(new ColumnNameFilter(tableMetadata.getName())).getPropertyName(), enhancedEntity);
                } catch (Exception e) {
                    throw new EntityInitializationError(entityHandler.getEntityType(), e);
                }
                //delete foreign relations by cascading
                final List<Object> found = helper.find(middleEntity, null, null);
                helper.delete(middleEntity);
                for (Object instance : found) {
                    delete(instance);
                }
            }
        });
        try {
            closeConnection(connection);
        } catch (SQLException e) {
            throw new EntityInitializationError(entityHandler.getEntityType(), e);
        }
    }

    @Override
    public <E, K extends Serializable> void delete(Class<E> entityType, K key) {
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entityType);
        final E instance = entityContext.getInstance(entityType);
        entityHandler.setKey(instance, key);
        eventHandler.beforeDelete(entityType, key);
        delete(instance);
        initializationContext.delete(entityType, key);
        eventHandler.afterDelete(entityType, key);
    }

    @Override
    public <E> void deleteAll(final Class<E> entityType) {
        eventHandler.beforeDeleteAll(entityType);
        deleteDependents(entityType);
        cleanUpStatement(internalExecuteUpdate(entityType, Statements.Manipulation.DELETE_ALL));
        initializationContext.delete(entityType);
        deleteDependencies(entityType);
        eventHandler.afterDeleteAll(entityType);
    }

    private synchronized <E> void deleteDependents(Class<E> entityType) {
        prepareDeleteAllStatements(entityType, Statements.Manipulation.DELETE_DEPENDENTS);
        with(deleteAllStatements.get().get(entityType).get(Statements.Manipulation.DELETE_DEPENDENTS))
                .each(new Processor<Statement>() {
                    @Override
                    public void process(Statement statement) {
                        cleanUpStatement(internalExecuteUpdate(statement, Collections.<String, Object>emptyMap()));
                    }
                });
    }

    private synchronized <E> void deleteDependencies(Class<E> entityType) {
        prepareDeleteAllStatements(entityType, Statements.Manipulation.DELETE_DEPENDENCIES);
        with(deleteAllStatements.get().get(entityType).get(Statements.Manipulation.DELETE_DEPENDENCIES))
                .each(new Processor<Statement>() {
                    @Override
                    public void process(Statement statement) {
                        internalExecuteUpdate(statement, Collections.<String, Object>emptyMap());
                    }
                });
    }

    private <E> void prepareDeleteAllStatements(Class<E> entityType, final Statements.Manipulation statementType) {
        if (!deleteAllStatements.get().containsKey(entityType)
                || !deleteAllStatements.get().get(entityType).containsKey(statementType)) {
            final Map<Statements.Manipulation, Set<Statement>> map = deleteAllStatements.get()
                    .containsKey(entityType) ? deleteAllStatements.get().get(entityType)
                            : new HashMap<Statements.Manipulation, Set<Statement>>();
            final Set<Statement> statements = map.containsKey(statementType) ? map.get(statementType)
                    : new HashSet<Statement>();
            final TableMetadata<E> tableMetadata = session.getTableMetadataRegistry().getTableMetadata(entityType);
            with(tableMetadata.getForeignReferences()).forThose(new Filter<RelationMetadata<E, ?>>() {
                @Override
                public boolean accepts(RelationMetadata<E, ?> relationMetadata) {
                    return relationMetadata.getCascadeMetadata().cascadeRemove()
                            && (statementType.equals(Statements.Manipulation.DELETE_DEPENDENCIES)
                                    ? relationMetadata.isOwner()
                                    : !relationMetadata.isOwner());
                }
            }, new Processor<RelationMetadata<E, ?>>() {
                @Override
                public void process(RelationMetadata<E, ?> relationMetadata) {
                    final StatementBuilder builder = session.getDatabaseDialect().getStatementBuilderContext()
                            .getManipulationStatementBuilder(statementType);
                    statements.add(builder.getStatement(tableMetadata, relationMetadata));
                }
            });
            map.put(statementType, statements);
            deleteAllStatements.get().put(entityType, map);
        }
    }

    @Override
    public <E> void truncate(Class<E> entityType) {
        eventHandler.beforeTruncate(entityType);
        internalExecuteUpdate(entityType, Statements.Manipulation.TRUNCATE);
        initializationContext.delete(entityType);
        eventHandler.afterTruncate(entityType);
    }

    @Override
    public <E> List<E> find(E sample) {
        return find(sample, null);
    }

    @Override
    public <E> List<E> find(E sample, String order) {
        return find(sample, order, -1, -1);
    }

    @Override
    public <E> List<E> find(E sample, int pageSize, int pageNumber) {
        return find(sample, null, pageSize, pageNumber);
    }

    @Override
    public <E> List<E> find(E sample, String order, int pageSize, int pageNumber) {
        final E enhancedEntity = getEnhancedEntity(sample);
        final InitializedEntity<E> initializedEntity = getInitializedEntity(enhancedEntity);
        initializedEntity.freeze();
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(sample);
        ResultOrderMetadata ordering;
        if (order != null) {
            ordering = new OrderExpressionParser(
                    session.getTableMetadataRegistry().getTableMetadata(entityHandler.getEntityType())).map(order);
        } else {
            ordering = null;
        }
        if (pageSize > 0 && pageNumber > 0) {
            if (ordering == null) {
                ordering = new DefaultPagedResultOrderMetadata(pageSize, pageNumber);
            } else {
                ordering = new DefaultPagedResultOrderMetadata(ordering, pageSize, pageNumber);
            }
        }
        eventHandler.beforeFind(enhancedEntity);
        final List<E> found = internalExecuteQuery(entityHandler.getEntityType(), Statements.Manipulation.FIND_LIKE,
                MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value."), ordering);
        eventHandler.afterFind(enhancedEntity, found);
        initializedEntity.unfreeze();
        return found;
    }

    @Override
    public <E, K extends Serializable> E find(Class<E> entityType, K key) {
        if (initializationContext.contains(entityType, key)) {
            return initializationContext.get(entityType, key);
        }
        final E instance = entityContext.getInstance(entityType);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entityType);
        entityHandler.setKey(instance, key);
        final Map<String, Object> map = MapTools.prefixKeys(entityHandler.toMap(instance), "value.");
        final List<E> list = internalExecuteQuery(entityType, Statements.Manipulation.FIND_ONE, map, null);
        final E result;
        if (list.isEmpty()) {
            result = null;
        } else if (list.size() == 1) {
            result = list.get(0);
        } else {
            throw new ObjectKeyError(entityType, key);
        }
        return eventHandler.afterFind(entityType, key, result);
    }

    @Override
    public <E> List<E> findAll(Class<E> entityType) {
        return findAll(entityType, null);
    }

    @Override
    public <E> List<E> findAll(Class<E> entityType, String order) {
        return findAll(entityType, order, -1, -1);
    }

    @Override
    public <E> List<E> findAll(Class<E> entityType, String order, int pageSize, int pageNumber) {
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entityType);
        ResultOrderMetadata ordering;
        if (order != null) {
            ordering = new OrderExpressionParser(
                    session.getTableMetadataRegistry().getTableMetadata(entityHandler.getEntityType())).map(order);
        } else {
            ordering = null;
        }
        if (pageSize > 0 && pageNumber > 0) {
            if (ordering == null) {
                ordering = new DefaultPagedResultOrderMetadata(pageSize, pageNumber);
            } else {
                ordering = new DefaultPagedResultOrderMetadata(ordering, pageSize, pageNumber);
            }
        }
        eventHandler.beforeFindAll(entityType);
        final List<E> found = internalExecuteQuery(entityHandler.getEntityType(), Statements.Manipulation.FIND_ALL,
                ordering);
        eventHandler.afterFindAll(entityType, found);
        return found;
    }

    @Override
    public <E> List<E> findAll(Class<E> entityType, int pageSize, int pageNumber) {
        return findAll(entityType, null, pageSize, pageNumber);
    }

    @Override
    public <E> int executeUpdate(Class<E> entityType, String queryName, Map<String, Object> values) {
        eventHandler.beforeExecuteUpdate(entityType, queryName, values);
        final int affectedItems = getUpdateCount(
                internalExecuteUpdate(entityType, queryName, MapTools.prefixKeys(values, "value.")));
        eventHandler.afterExecuteUpdate(entityType, queryName, values, affectedItems);
        return affectedItems;
    }

    @Override
    public <E> int executeUpdate(E sample, String queryName) {
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(sample);
        final E enhancedEntity = getEnhancedEntity(sample);
        eventHandler.beforeExecuteUpdate(enhancedEntity, queryName);
        final int affectedItems = getUpdateCount(internalExecuteUpdate(entityHandler.getEntityType(), queryName,
                MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value.")));
        eventHandler.afterExecuteUpdate(enhancedEntity, queryName, affectedItems);
        return affectedItems;
    }

    @Override
    public <E> List<E> executeQuery(Class<E> entityType, String queryName, Map<String, Object> values) {
        eventHandler.beforeExecuteQuery(entityType, queryName, values);
        final List<E> list = internalExecuteQuery(entityType, queryName, MapTools.prefixKeys(values, "value."),
                null);
        eventHandler.afterExecuteQuery(entityType, queryName, values, list);
        return list;
    }

    @Override
    public <E> List<E> executeQuery(E sample, String queryName) {
        final E enhancedEntity = getEnhancedEntity(sample);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(enhancedEntity);
        eventHandler.beforeExecuteQuery(enhancedEntity, queryName);
        final List<E> list = internalExecuteQuery(entityHandler.getEntityType(), queryName,
                MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value."), null);
        eventHandler.afterExecuteQuery(enhancedEntity, queryName, list);
        return list;
    }

    @Override
    public <E> List<?> call(Class<E> entityType, final String procedureName, Object... parameters) {
        if (isInBatchMode()) {
            throw new BatchOperationInterruptedByProcedureError();
        }
        log.info("Calling to stored procedure " + entityType.getCanonicalName() + "." + procedureName);
        final TableMetadata<E> tableMetadata = session.getTableMetadataRegistry().getTableMetadata(entityType);
        //noinspection unchecked
        final StoredProcedureMetadata procedureMetadata = with(tableMetadata.getProcedures())
                .keep(new Filter<StoredProcedureMetadata>() {
                    @Override
                    public boolean accepts(StoredProcedureMetadata item) {
                        return item.getName().equals(procedureName);
                    }
                }).first();
        if (procedureMetadata == null) {
            throw new NoSuchProcedureError(entityType, procedureName);
        }
        if (procedureMetadata.getParameters().size() != parameters.length) {
            throw new MismatchedParametersNumberError(entityType, procedureName,
                    procedureMetadata.getParameters().size(), parameters.length);
        }
        for (int i = 0; i < procedureMetadata.getParameters().size(); i++) {
            ParameterMetadata metadata = procedureMetadata.getParameters().get(i);
            if (metadata.getParameterMode().equals(ParameterMode.IN)) {
                if (parameters[i] != null
                        && !ReflectionUtils.mapType(metadata.getParameterType()).isInstance(parameters[i])) {
                    throw new MismatchedParameterTypeError(entityType, procedureName, i,
                            metadata.getParameterType(), parameters[i].getClass());
                }
            } else {
                if (parameters[i] == null) {
                    throw new NullPointerException(metadata.getParameterMode() + " parameter cannot be null");
                }
                if (!(parameters[i] instanceof Reference<?>)) {
                    throw new ReferenceParameterExpectedError(entityType, procedureName, i);
                }
            }
        }
        final ProcedureCallStatement statement = (ProcedureCallStatement) getStatement(entityType,
                "call." + procedureName, null, StatementType.CALL);
        final Map<String, Object> values = new HashMap<String, Object>();
        for (int i = 0; i < parameters.length; i++) {
            values.put("value.parameter" + i,
                    parameters[i] instanceof Reference ? ((Reference<?>) parameters[i]).getValue() : parameters[i]);
        }
        final CallableStatement callableStatement;
        final ArrayList<Object> result = new ArrayList<Object>();
        try {
            callableStatement = openStatement(statement.prepare(openConnection(), null, values));
            for (int i = 0; i < procedureMetadata.getParameters().size(); i++) {
                final ParameterMetadata metadata = procedureMetadata.getParameters().get(i);
                if (!metadata.getParameterMode().equals(ParameterMode.IN)) {
                    callableStatement.registerOutParameter(i + 1, metadata.getType());
                }
            }
            if (procedureMetadata.getResultType().equals(void.class)) {
                callableStatement.executeUpdate();
            } else {
                final ResultSet resultSet = callableStatement.executeQuery();
                final EntityHandler<Object> entityHandler;
                if (procedureMetadata.isPartial()) {
                    entityHandler = null;
                } else {
                    //noinspection unchecked
                    entityHandler = (EntityHandler<Object>) entityHandlerContext
                            .getHandler(procedureMetadata.getResultType());
                }
                while (resultSet.next()) {
                    final Map<String, Object> map = rowHandler.handleRow(resultSet);
                    if (procedureMetadata.isPartial()) {
                        try {
                            result.add(entityHandlerContext.fromMap(
                                    beanInitializer.initialize(procedureMetadata.getResultType(), new Class[0]),
                                    getPartialEntityMetadata(procedureMetadata.getResultType()), map));
                        } catch (BeanInstantiationException e) {
                            throw new EntityInitializationError(procedureMetadata.getResultType(), e);
                        }
                    } else {
                        assert entityHandler != null;
                        Object instance = entityContext.getInstance(entityHandler.getEntityType());
                        instance = entityHandler.fromMap(instance, map);
                        if (entityHandler.hasKey() && entityHandler.getKey(instance) != null) {
                            final Serializable key = entityHandler.getKey(instance);
                            if (initializationContext.contains(entityHandler.getEntityType(), key)) {
                                instance = initializationContext.get(entityHandler.getEntityType(), key);
                                result.add(instance);
                                continue;
                            }
                        }
                        prepareEntity(instance, map);
                        result.add(instance);
                    }
                }
                resultSet.close();
            }
            for (int i = 0; i < procedureMetadata.getParameters().size(); i++) {
                final ParameterMetadata metadata = procedureMetadata.getParameters().get(i);
                if (!metadata.getParameterMode().equals(ParameterMode.IN)) {
                    //noinspection unchecked
                    ((Reference) parameters[i]).setValue(callableStatement.getObject(i + 1));
                }
            }
            cleanUpStatement(callableStatement);
        } catch (SQLException e) {
            throw new ProcedureExecutionFailureError("Failed to call procedure " + procedureName, e);
        }
        return result;
    }

    @Override
    public <E> long countAll(Class<E> entityType) {
        return internalCount(entityType, Statements.Manipulation.COUNT_ALL, Collections.<String, Object>emptyMap());
    }

    @Override
    public <E> long count(E sample) {
        final E enhancedEntity = getEnhancedEntity(sample);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(sample);
        return internalCount(entityHandler.getEntityType(),
                entityHandler.hasKey() && entityHandler.getKey(enhancedEntity) != null
                        ? Statements.Manipulation.COUNT_ONE
                        : Statements.Manipulation.COUNT_LIKE,
                MapTools.prefixKeys(entityHandler.toMap(enhancedEntity), "value."));
    }

    @Override
    public <E> boolean exists(E sample) {
        return count(sample) != 0;
    }

    @Override
    public <E, K extends Serializable> boolean exists(Class<E> entityType, K key) {
        final E instance = entityContext.getInstance(entityType);
        final EntityHandler<E> entityHandler = entityHandlerContext.getHandler(entityType);
        entityHandler.setKey(instance, key);
        return count(instance) != 0;
    }

    private <E> long count(Class<E> entityType, Map<String, Object> values) {
        return internalCount(entityType, Statements.Manipulation.COUNT_LIKE, values);
    }

    private <E> boolean exists(Class<E> entityType, Map<String, Object> values) {
        return count(entityType, values) != 0;
    }

    @Override
    public List<Integer> run(BatchOperation batchOperation) {
        startBatch();
        log.info("Stacking operations for batch execution");
        batchOperation.execute(this);
        return endBatch();
    }

    @Override
    public <E> SelectQueryInitiator<E> from(E alias) {
        return new DefaultSelectQueryInitiator<E>(session, alias);
    }

    /**
     * Event handler context
     */

    @Override
    public void addHandler(DataAccessEventHandler eventHandler) {
        this.eventHandler.addHandler(eventHandler);
    }

}