org.alfresco.repo.domain.node.ibatis.NodeDAOImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.domain.node.ibatis.NodeDAOImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.domain.node.ibatis;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.ibatis.IdsEntity;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.domain.node.AbstractNodeDAOImpl;
import org.alfresco.repo.domain.node.ChildAssocEntity;
import org.alfresco.repo.domain.node.ChildPropertyEntity;
import org.alfresco.repo.domain.node.Node;
import org.alfresco.repo.domain.node.NodeAspectsEntity;
import org.alfresco.repo.domain.node.NodeAssocEntity;
import org.alfresco.repo.domain.node.NodeEntity;
import org.alfresco.repo.domain.node.NodeExistsException;
import org.alfresco.repo.domain.node.NodeIdAndAclId;
import org.alfresco.repo.domain.node.NodePropertyEntity;
import org.alfresco.repo.domain.node.NodePropertyKey;
import org.alfresco.repo.domain.node.NodePropertyValue;
import org.alfresco.repo.domain.node.NodeUpdateEntity;
import org.alfresco.repo.domain.node.NodeVersionKey;
import org.alfresco.repo.domain.node.PrimaryChildrenAclUpdateEntity;
import org.alfresco.repo.domain.node.ServerEntity;
import org.alfresco.repo.domain.node.StoreEntity;
import org.alfresco.repo.domain.node.Transaction;
import org.alfresco.repo.domain.node.TransactionEntity;
import org.alfresco.repo.domain.node.TransactionQueryEntity;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.apache.ibatis.executor.result.DefaultResultContext;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.util.Assert;

/**
 * iBatis-specific extension of the Node abstract DAO 
 * 
 * @author Derek Hulley
 * @since 3.4
 */
public class NodeDAOImpl extends AbstractNodeDAOImpl {
    private static final String SELECT_SERVER_BY_IPADDRESS = "alfresco.node.select_ServerByIpAddress";
    private static final String INSERT_SERVER = "alfresco.node.insert.insert_Server";
    private static final String INSERT_TRANSACTION = "alfresco.node.insert.insert_Transaction";
    private static final String UPDATE_TRANSACTION_COMMIT_TIME = "alfresco.node.update_TransactionCommitTime";
    private static final String DELETE_TRANSACTION_BY_ID = "alfresco.node.delete_TransactionById";
    private static final String INSERT_STORE = "alfresco.node.insert.insert_Store";
    private static final String UPDATE_STORE_ROOT = "alfresco.node.update_StoreRoot";
    private static final String UPDATE_STORE = "alfresco.node.update_Store";
    private static final String UPDATE_NODES_IN_STORE = "alfresco.node.update_NodesInStore";
    private static final String SELECT_STORE_ALL = "alfresco.node.select_StoreAll";
    private static final String SELECT_STORE_BY_REF = "alfresco.node.select_StoreByRef";
    private static final String SELECT_STORE_ROOT_NODE_BY_REF = "alfresco.node.select_StoreRootNodeByRef";
    private static final String INSERT_NODE = "alfresco.node.insert.insert_Node";
    private static final String UPDATE_NODE = "alfresco.node.update_Node";
    private static final String UPDATE_NODE_BULK_TOUCH = "alfresco.node.update_NodeBulkTouch";
    private static final String DELETE_NODE_BY_ID = "alfresco.node.delete_NodeById";
    private static final String DELETE_NODES_BY_TXN_COMMIT_TIME = "alfresco.node.delete.delete_NodesByTxnCommitTime";
    private static final String DELETE_NODE_PROPS_BY_TXN_COMMIT_TIME = "alfresco.node.delete.delete_NodePropsByTxnCommitTime";
    private static final String SELECT_NODE_BY_ID = "alfresco.node.select_NodeById";
    private static final String SELECT_NODE_BY_NODEREF = "alfresco.node.select_NodeByNodeRef";
    private static final String SELECT_NODES_BY_UUIDS = "alfresco.node.select_NodesByUuids";
    private static final String SELECT_NODES_BY_IDS = "alfresco.node.select_NodesByIds";
    private static final String SELECT_NODE_PROPERTIES = "alfresco.node.select_NodeProperties";
    private static final String SELECT_PROPERTIES_BY_TYPES = "alfresco.node.select_PropertiesByTypes";
    private static final String SELECT_PROPERTIES_BY_ACTUAL_TYPE = "alfresco.node.select_PropertiesByActualType";
    private static final String SELECT_NODE_ASPECTS = "alfresco.node.select_NodeAspects";
    private static final String INSERT_NODE_PROPERTY = "alfresco.node.insert_NodeProperty";
    private static final String UPDATE_PRIMARY_CHILDREN_SHARED_ACL = "alfresco.node.update.update_PrimaryChildrenSharedAcl";
    private static final String INSERT_NODE_ASPECT = "alfresco.node.insert_NodeAspect";
    private static final String DELETE_NODE_ASPECTS = "alfresco.node.delete_NodeAspects";
    private static final String DELETE_NODE_PROPERTIES = "alfresco.node.delete_NodeProperties";
    private static final String SELECT_NODE_MIN_ID = "alfresco.node.select_NodeMinId";
    private static final String SELECT_NODE_MAX_ID = "alfresco.node.select_NodeMaxId";
    private static final String SELECT_NODE_INTERVAL_BY_TYPE = "alfresco.node.select_MinMaxNodeIdForNodeType";
    private static final String SELECT_NODES_WITH_ASPECT_IDS = "alfresco.node.select_NodesWithAspectIds";
    private static final String INSERT_NODE_ASSOC = "alfresco.node.insert.insert_NodeAssoc";
    private static final String UPDATE_NODE_ASSOC = "alfresco.node.update_NodeAssoc";
    private static final String DELETE_NODE_ASSOC = "alfresco.node.delete_NodeAssoc";
    private static final String DELETE_NODE_ASSOCS = "alfresco.node.delete_NodeAssocs";
    private static final String SELECT_NODE_ASSOCS = "alfresco.node.select_NodeAssocs";
    private static final String SELECT_NODE_ASSOCS_BY_SOURCE_AND_PROPERTY_VALUE = "alfresco.node.select_NodeAssocsBySourceAndPropertyValue";
    private static final String SELECT_NODE_ASSOCS_BY_TARGET = "alfresco.node.select_NodeAssocsByTarget";
    private static final String SELECT_NODE_ASSOC_BY_ID = "alfresco.node.select_NodeAssocById";
    private static final String SELECT_NODE_ASSOCS_MAX_INDEX = "alfresco.node.select_NodeAssocsMaxId";
    private static final String SELECT_CHILD_NODE_IDS = "alfresco.node.select.children.select_ChildNodeIds_Limited";
    private static final String SELECT_NODE_PRIMARY_CHILD_ACLS = "alfresco.node.select_NodePrimaryChildAcls";
    private static final String INSERT_CHILD_ASSOC = "alfresco.node.insert.insert_ChildAssoc";
    private static final String DELETE_CHILD_ASSOCS = "alfresco.node.delete_ChildAssocs";
    private static final String UPDATE_CHILD_ASSOCS_INDEX = "alfresco.node.update_ChildAssocsIndex";
    private static final String UPDATE_CHILD_ASSOC_UNIQUE_NAME = "alfresco.node.update_ChildAssocUniqueName";
    private static final String SELECT_CHILD_ASSOC_BY_ID = "alfresco.node.select_ChildAssocById";
    private static final String COUNT_CHILD_ASSOC_BY_PARENT_ID = "alfresco.node.count_ChildAssocByParentId";
    private static final String SELECT_CHILD_ASSOCS_BY_PROPERTY_VALUE = "alfresco.node.select_ChildAssocsByPropertyValue";
    private static final String SELECT_CHILD_ASSOCS_OF_PARENT = "alfresco.node.select_ChildAssocsOfParent";
    private static final String SELECT_CHILD_ASSOCS_OF_PARENT_LIMITED = "alfresco.node.select.children.select_ChildAssocsOfParent_Limited";
    private static final String SELECT_CHILD_ASSOC_OF_PARENT_BY_NAME = "alfresco.node.select_ChildAssocOfParentByName";
    private static final String SELECT_CHILD_ASSOCS_OF_PARENT_WITHOUT_PARENT_ASSOCS_OF_TYPE = "alfresco.node.select_ChildAssocsOfParentWithoutParentAssocsOfType";
    private static final String SELECT_CHILD_ASSOCS_OF_PARENT_WITHOUT_NODE_ASSOCS_OF_TYPE = "alfresco.node.select_ChildAssocsOfParentWithoutNodeAssocsOfType";
    private static final String SELECT_PARENT_ASSOCS_OF_CHILD = "alfresco.node.select_ParentAssocsOfChild";
    private static final String UPDATE_PARENT_ASSOCS_OF_CHILD = "alfresco.node.update_ParentAssocsOfChild";
    private static final String DELETE_SUBSCRIPTIONS = "alfresco.node.delete_NodeSubscriptions";

    private static final String UPDATE_MOVE_PARENT_ASSOCS = "alfresco.node.update_MoveParentAssocs";
    private static final String UPDATE_MOVE_CHILD_ASSOCS = "alfresco.node.update_MoveChildAssocs";
    private static final String UPDATE_MOVE_SOURCE_ASSOCS = "alfresco.node.update_MoveSourceAssocs";
    private static final String UPDATE_MOVE_TARGET_ASSOCS = "alfresco.node.update_MoveTargetAssocs";
    private static final String UPDATE_MOVE_PROPERTIES = "alfresco.node.update_MoveProperties";
    private static final String UPDATE_MOVE_ASPECTS = "alfresco.node.update_MoveAspects";

    private static final String SELECT_TXN_LAST = "alfresco.node.select_TxnLast";
    private static final String SELECT_TXN_NODES = "alfresco.node.select_TxnNodes";
    private static final String SELECT_TXNS = "alfresco.node.select_Txns";
    private static final String SELECT_TXN_COUNT = "alfresco.node.select_TxnCount";
    private static final String SELECT_TXNS_UNUSED = "alfresco.node.select_TxnsUnused";
    private static final String DELETE_TXNS_UNUSED = "alfresco.node.delete_Txns_Unused";
    private static final String SELECT_TXN_MIN_COMMIT_TIME = "alfresco.node.select_TxnMinCommitTime";
    private static final String SELECT_TXN_MAX_COMMIT_TIME = "alfresco.node.select_TxnMaxCommitTime";
    private static final String SELECT_TXN_MIN_COMMIT_TIME_FOR_NODE_TYPE = "alfresco.node.select_TxnMinCommitTimeForNodeType";
    private static final String SELECT_TXN_MIN_ID = "alfresco.node.select_TxnMinId";
    private static final String SELECT_TXN_MAX_ID = "alfresco.node.select_TxnMaxId";
    private static final String SELECT_TXN_UNUSED_MIN_COMMIT_TIME = "alfresco.node.select_TxnMinUnusedCommitTime";
    private static final String SELECT_ONE_TXNS_BY_COMMIT_TIME_DESC = "alfresco.node.select_OneTxnsByCommitTimeDescending";

    protected QNameDAO qnameDAO;
    protected DictionaryService dictionaryService;

    private SqlSessionTemplate template;

    public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
        this.template = sqlSessionTemplate;
    }

    @Override
    public void setQnameDAO(QNameDAO qnameDAO) {
        this.qnameDAO = qnameDAO;
        super.setQnameDAO(qnameDAO);
    }

    @Override
    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
        super.setDictionaryService(dictionaryService);
    }

    public void startBatch() {
        // TODO
        /*
        try
        {
        template.getSqlMapClient().startBatch();
        }
        catch (SQLException e)
        {
        throw new RuntimeException("Failed to start DAO batch.", e);
        }
        */
    }

    public void executeBatch() {
        // TODO
        /*
        try
        {
        template.getSqlMapClient().executeBatch();
        }
        catch (SQLException e)
        {
        throw new RuntimeException("Failed to start DAO batch.", e);
        }
        */
    }

    @SuppressWarnings("unchecked")
    @Override
    protected ServerEntity selectServer(String ipAddress) {
        ServerEntity entity = new ServerEntity();
        entity.setIpAddress(ipAddress);
        // Potentially more results if there is a case issue (unlikely)
        List<ServerEntity> results = template.selectList(SELECT_SERVER_BY_IPADDRESS, entity);
        for (ServerEntity serverEntity : results) {
            // Take the first one that matches regardless of case
            if (serverEntity.getIpAddress().equalsIgnoreCase(ipAddress)) {
                return serverEntity;
            }
        }
        // There was no match
        return null;
    }

    @Override
    protected Long insertServer(String ipAddress) {
        ServerEntity entity = new ServerEntity();
        entity.setVersion(1L);
        entity.setIpAddress(ipAddress);
        template.insert(INSERT_SERVER, entity);
        return entity.getId();
    }

    @Override
    protected Long insertTransaction(Long serverId, String changeTxnId, Long commitTimeMs) {
        ServerEntity server = new ServerEntity();
        server.setId(serverId);
        TransactionEntity transaction = new TransactionEntity();
        transaction.setServer(server);
        transaction.setVersion(1L);
        transaction.setChangeTxnId(changeTxnId);
        transaction.setCommitTimeMs(commitTimeMs);
        template.insert(INSERT_TRANSACTION, transaction);
        return transaction.getId();
    }

    @Override
    protected int updateTransaction(Long txnId, Long commitTimeMs) {
        TransactionEntity transaction = new TransactionEntity();
        transaction.setId(txnId);
        transaction.setCommitTimeMs(commitTimeMs);
        return template.update(UPDATE_TRANSACTION_COMMIT_TIME, transaction);
    }

    @Override
    protected int deleteTransaction(Long txnId) {
        TransactionEntity transaction = new TransactionEntity();
        transaction.setId(txnId);
        return template.delete(DELETE_TRANSACTION_BY_ID, transaction);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<StoreEntity> selectAllStores() {
        return template.selectList(SELECT_STORE_ALL);
    }

    @Override
    protected StoreEntity selectStore(StoreRef storeRef) {
        StoreEntity store = new StoreEntity();
        store.setProtocol(storeRef.getProtocol());
        store.setIdentifier(storeRef.getIdentifier());
        return template.selectOne(SELECT_STORE_BY_REF, store);
    }

    @Override
    protected NodeEntity selectStoreRootNode(StoreRef storeRef) {
        StoreEntity store = new StoreEntity();
        store.setProtocol(storeRef.getProtocol());
        store.setIdentifier(storeRef.getIdentifier());
        return template.selectOne(SELECT_STORE_ROOT_NODE_BY_REF, store);
    }

    @Override
    protected Long insertStore(StoreEntity store) {
        store.setVersion(1L);
        template.insert(INSERT_STORE, store);
        return store.getId();
    }

    @Override
    protected int updateStoreRoot(StoreEntity store) {
        return template.update(UPDATE_STORE_ROOT, store);
    }

    @Override
    protected int updateStore(StoreEntity store) {
        return template.update(UPDATE_STORE, store);
    }

    @Override
    protected int updateNodesInStore(Long txnId, Long storeId) {
        IdsEntity ids = new IdsEntity();
        ids.setIdOne(txnId);
        ids.setIdTwo(storeId);
        return template.update(UPDATE_NODES_IN_STORE, ids);
    }

    @Override
    protected Long insertNode(NodeEntity node) {
        node.setVersion(1L);
        template.insert(INSERT_NODE, node);
        return node.getId();
    }

    @Override
    protected int updateNode(NodeUpdateEntity nodeUpdate) {
        // Increment the version
        nodeUpdate.incrementVersion();
        return template.update(UPDATE_NODE, nodeUpdate);
    }

    @Override
    protected int updateNodes(Long txnId, List<Long> nodeIds) {
        if (nodeIds.size() == 0) {
            return 0;
        }
        IdsEntity ids = new IdsEntity();
        ids.setIdOne(txnId);
        ids.setIds(nodeIds);
        return template.update(UPDATE_NODE_BULK_TOUCH, ids);
    }

    @Override
    public Long getMinNodeId() {
        return (Long) template.selectOne(SELECT_NODE_MIN_ID);
    }

    @Override
    public Long getMaxNodeId() {
        return (Long) template.selectOne(SELECT_NODE_MAX_ID);
    }

    @Override
    public Pair<Long, Long> getNodeIdsIntervalForType(QName type, Long startTxnTime, Long endTxnTime) {
        final Pair<Long, Long> intervalPair = new Pair<Long, Long>(LONG_ZERO, LONG_ZERO);
        Pair<Long, QName> typePair = qnameDAO.getQName(type);
        if (typePair == null) {
            // Return default
            return intervalPair;
        }
        TransactionQueryEntity txnQuery = new TransactionQueryEntity();
        txnQuery.setTypeQNameId(typePair.getFirst());
        txnQuery.setMinCommitTime(startTxnTime);
        txnQuery.setMaxCommitTime(endTxnTime);

        ResultHandler resultHandler = new ResultHandler() {
            @SuppressWarnings("unchecked")
            public void handleResult(ResultContext context) {
                Map<Long, Long> result = (Map<Long, Long>) context.getResultObject();
                if (result != null) {
                    intervalPair.setFirst(result.get("minId"));
                    intervalPair.setSecond(result.get("maxId"));
                }
            }
        };
        template.select(SELECT_NODE_INTERVAL_BY_TYPE, txnQuery, resultHandler);
        return intervalPair;
    }

    @Override
    protected void updatePrimaryChildrenSharedAclId(Long txnId, Long primaryParentNodeId,
            Long optionalOldSharedAlcIdInAdditionToNull, Long newSharedAlcId) {
        PrimaryChildrenAclUpdateEntity primaryChildrenAclUpdateEntity = new PrimaryChildrenAclUpdateEntity();
        primaryChildrenAclUpdateEntity.setTxnId(txnId);
        primaryChildrenAclUpdateEntity.setPrimaryParentNodeId(primaryParentNodeId);
        primaryChildrenAclUpdateEntity
                .setOptionalOldSharedAclIdInAdditionToNull(optionalOldSharedAlcIdInAdditionToNull);
        primaryChildrenAclUpdateEntity.setNewSharedAclId(newSharedAlcId);

        template.update(UPDATE_PRIMARY_CHILDREN_SHARED_ACL, primaryChildrenAclUpdateEntity);
    }

    @Override
    protected int deleteNodeById(Long nodeId) {
        NodeEntity node = new NodeEntity();
        node.setId(nodeId);
        return template.delete(DELETE_NODE_BY_ID, node);
    }

    @Override
    protected int deleteNodesByCommitTime(long fromTxnCommitTimeMs, long toTxnCommitTimeMs) {
        // Get the deleted nodes
        Pair<Long, QName> deletedTypePair = qnameDAO.getQName(ContentModel.TYPE_DELETED);
        if (deletedTypePair == null) {
            // Nothing to do
            return 0;
        }
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setTypeQNameId(deletedTypePair.getFirst());
        query.setMinCommitTime(fromTxnCommitTimeMs);
        query.setMaxCommitTime(toTxnCommitTimeMs);
        // TODO: Fix ALF-16030 Use ON DELETE CASCADE for node aspects and properties 
        // First clean up properties
        template.delete(DELETE_NODE_PROPS_BY_TXN_COMMIT_TIME, query);
        // Finally remove the nodes
        return template.delete(DELETE_NODES_BY_TXN_COMMIT_TIME, query);
    }

    @Override
    protected NodeEntity selectNodeById(Long id) {
        NodeEntity node = new NodeEntity();
        node.setId(id);

        return template.selectOne(SELECT_NODE_BY_ID, node);
    }

    @Override
    protected NodeEntity selectNodeByNodeRef(NodeRef nodeRef) {
        StoreEntity store = new StoreEntity();
        StoreRef storeRef = nodeRef.getStoreRef();
        store.setProtocol(storeRef.getProtocol());
        store.setIdentifier(storeRef.getIdentifier());

        NodeEntity node = new NodeEntity();
        // Store
        node.setStore(store);
        // UUID
        String uuid = nodeRef.getId();
        if (uuid.length() > 36) {
            return null; // Avoid DB2 query failure if someone passes in a made-up UUID
        }
        node.setUuid(uuid);

        return template.selectOne(SELECT_NODE_BY_NODEREF, node);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<Node> selectNodesByUuids(Long storeId, SortedSet<String> uuids) {
        NodeBatchLoadEntity nodeBatchLoadEntity = new NodeBatchLoadEntity();
        // Store ID
        nodeBatchLoadEntity.setStoreId(storeId);
        // UUID
        nodeBatchLoadEntity.setUuids(new ArrayList<String>(uuids));

        return template.selectList(SELECT_NODES_BY_UUIDS, nodeBatchLoadEntity);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<Node> selectNodesByIds(SortedSet<Long> ids) {
        NodeBatchLoadEntity nodeBatchLoadEntity = new NodeBatchLoadEntity();
        // IDs
        nodeBatchLoadEntity.setIds(new ArrayList<Long>(ids));

        return template.selectList(SELECT_NODES_BY_IDS, nodeBatchLoadEntity);
    }

    /**
     * Pull out the key-value pairs from the rows
     */
    private Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> makePersistentPropertiesMap(
            List<NodePropertyEntity> rows) {
        Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> results = new HashMap<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>>(
                3);
        for (NodePropertyEntity row : rows) {
            Long nodeId = row.getNodeId();
            Long nodeVersion = row.getNodeVersion();
            if (nodeId == null || nodeVersion == null) {
                throw new RuntimeException("Expect results with a Node and Version: " + row);
            }
            NodeVersionKey nodeTxnKey = new NodeVersionKey(nodeId, nodeVersion);
            Map<NodePropertyKey, NodePropertyValue> props = results.get(nodeTxnKey);
            if (props == null) {
                props = new HashMap<NodePropertyKey, NodePropertyValue>(17);
                results.put(nodeTxnKey, props);
            }
            props.put(row.getKey(), row.getValue());
        }
        // Done
        return results;
    }

    /**
     * Convert key-value pairs into rows
     */
    private List<NodePropertyEntity> makePersistentRows(Long nodeId, Map<NodePropertyKey, NodePropertyValue> map) {
        List<NodePropertyEntity> rows = new ArrayList<NodePropertyEntity>(map.size());
        for (Map.Entry<NodePropertyKey, NodePropertyValue> entry : map.entrySet()) {
            NodePropertyEntity row = new NodePropertyEntity();
            row.setNodeId(nodeId);
            row.setKey(entry.getKey());
            row.setValue(entry.getValue());
            rows.add(row);
        }
        // Done
        return rows;
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Set<Long> nodeIds) {
        if (nodeIds.size() == 0) {
            return Collections.emptyMap();
        }
        NodePropertyEntity prop = new NodePropertyEntity();
        prop.setNodeIds(new ArrayList<Long>(nodeIds));

        List<NodePropertyEntity> rows = template.selectList(SELECT_NODE_PROPERTIES, prop);
        return makePersistentPropertiesMap(rows);
    }

    @Override
    protected Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId) {
        return selectNodeProperties(nodeId, Collections.<Long>emptySet());
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId,
            Set<Long> qnameIds) {
        NodePropertyEntity prop = new NodePropertyEntity();
        // Node
        prop.setNodeId(nodeId);
        // QName(s)
        switch (qnameIds.size()) {
        case 0:
            // Ignore
            break;
        case 1:
            prop.setKey(new NodePropertyKey());
            prop.getKey().setQnameId(qnameIds.iterator().next());
            break;
        default:
            prop.setQnameIds(new ArrayList<Long>(qnameIds));
        }

        List<NodePropertyEntity> rows = template.selectList(SELECT_NODE_PROPERTIES, prop);
        return makePersistentPropertiesMap(rows);
    }

    @Override
    public List<NodePropertyEntity> selectNodePropertiesByTypes(Set<QName> qnames) {
        final List<NodePropertyEntity> properties = new ArrayList<NodePropertyEntity>();

        // qnames of properties that are encrypted
        Set<Long> qnameIds = qnameDAO.convertQNamesToIds(qnames, false);
        if (qnameIds.size() > 0) {
            IdsEntity param = new IdsEntity();
            param.setIds(new ArrayList<Long>(qnameIds));
            // TODO - use a callback approach
            template.select(SELECT_PROPERTIES_BY_TYPES, param, new ResultHandler() {
                @Override
                public void handleResult(ResultContext context) {
                    properties.add((NodePropertyEntity) context.getResultObject());
                }
            });
        }

        return properties;
    }

    @Override
    public List<NodePropertyEntity> selectNodePropertiesByDataType(QName dataType, long minNodeId, long maxNodeId) {
        int typeOrdinal = NodePropertyValue.convertToTypeOrdinal(dataType);

        IdsEntity ids = new IdsEntity();
        ids.setIdOne((long) typeOrdinal);
        ids.setIdTwo(minNodeId);
        ids.setIdThree(maxNodeId);
        final List<NodePropertyEntity> properties = new ArrayList<NodePropertyEntity>();

        template.select(SELECT_PROPERTIES_BY_ACTUAL_TYPE, ids, new ResultHandler() {
            @Override
            public void handleResult(ResultContext context) {
                properties.add((NodePropertyEntity) context.getResultObject());
            }
        });

        return properties;
    }

    @Override
    protected int deleteNodeProperties(Long nodeId, Set<Long> qnameIds) {
        NodePropertyEntity prop = new NodePropertyEntity();
        // Node
        prop.setNodeId(nodeId);
        // QNames
        if (qnameIds != null) {
            if (qnameIds.isEmpty()) {
                return 0; // Nothing to do
            }
            prop.setQnameIds(new ArrayList<Long>(qnameIds));
        }

        return template.delete(DELETE_NODE_PROPERTIES, prop);
    }

    @Override
    protected int deleteNodeProperties(Long nodeId, List<NodePropertyKey> propKeys) {
        Assert.notNull(nodeId, "Must have 'nodeId'");
        Assert.notNull(nodeId, "Must have 'propKeys'");

        if (propKeys.size() == 0) {
            return 0;
        }

        NodePropertyEntity prop = new NodePropertyEntity();
        // Node
        prop.setNodeId(nodeId);

        startBatch();
        int count = 0;
        try {
            for (NodePropertyKey propKey : propKeys) {
                prop.setKey(propKey);
                count += template.delete(DELETE_NODE_PROPERTIES, prop);
            }
        } finally {
            executeBatch();
        }
        return count;
    }

    @Override
    protected void insertNodeProperties(Long nodeId, Map<NodePropertyKey, NodePropertyValue> persistableProps) {
        if (persistableProps.isEmpty()) {
            return;
        }

        List<NodePropertyEntity> rows = makePersistentRows(nodeId, persistableProps);

        startBatch();
        try {
            for (NodePropertyEntity row : rows) {
                template.insert(INSERT_NODE_PROPERTY, row);
            }
        } finally {
            executeBatch();
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    protected Map<NodeVersionKey, Set<QName>> selectNodeAspects(Set<Long> nodeIds) {
        if (nodeIds.size() == 0) {
            return Collections.emptyMap();
        }
        NodeAspectsEntity aspects = new NodeAspectsEntity();
        aspects.setNodeIds(new ArrayList<Long>(nodeIds));

        List<NodeAspectsEntity> rows = template.selectList(SELECT_NODE_ASPECTS, aspects);

        Map<NodeVersionKey, Set<QName>> results = new HashMap<NodeVersionKey, Set<QName>>(rows.size() * 2);
        for (NodeAspectsEntity nodeAspectsEntity : rows) {
            Long nodeId = nodeAspectsEntity.getNodeId();
            Long nodeVersion = nodeAspectsEntity.getNodeVersion();
            NodeVersionKey nodeVersionKey = new NodeVersionKey(nodeId, nodeVersion);
            if (results.containsKey(nodeVersionKey)) {
                throw new IllegalStateException("Found existing key while querying for node aspects: " + nodeIds);
            }
            Set<Long> aspectIds = new HashSet<Long>(nodeAspectsEntity.getAspectQNameIds());
            Set<QName> aspectQNames = qnameDAO.convertIdsToQNames(aspectIds);
            results.put(nodeVersionKey, aspectQNames);
        }
        return results;
    }

    @Override
    protected void insertNodeAspect(Long nodeId, Long qnameId) {
        Map<String, Long> aspectParameters = new HashMap<String, Long>(5);
        aspectParameters.put("nodeId", nodeId);
        aspectParameters.put("qnameId", qnameId);
        template.insert(INSERT_NODE_ASPECT, aspectParameters);
    }

    @Override
    protected int deleteNodeAspects(Long nodeId, Set<Long> qnameIds) {
        NodeAspectsEntity nodeAspects = new NodeAspectsEntity();
        nodeAspects.setNodeId(nodeId);
        if (qnameIds != null && !qnameIds.isEmpty()) {
            nodeAspects.setAspectQNameIds(new ArrayList<Long>(qnameIds)); // Null means all
        }
        return template.delete(DELETE_NODE_ASPECTS, nodeAspects);
    }

    @Override
    protected void selectNodesWithAspects(List<Long> qnameIds, Long minNodeId, Long maxNodeId,
            final NodeRefQueryCallback resultsCallback) {
        ResultHandler resultHandler = new ResultHandler() {
            public void handleResult(ResultContext context) {
                NodeEntity entity = (NodeEntity) context.getResultObject();
                Pair<Long, NodeRef> nodePair = new Pair<Long, NodeRef>(entity.getId(), entity.getNodeRef());
                resultsCallback.handle(nodePair);
            }
        };

        IdsEntity parameters = new IdsEntity();
        parameters.setIdOne(minNodeId);
        parameters.setIdTwo(maxNodeId);
        parameters.setIds(qnameIds);
        template.select(SELECT_NODES_WITH_ASPECT_IDS, parameters, resultHandler);
    }

    @Override
    protected Long insertNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId, int assocIndex) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        assoc.setVersion(1L);
        assoc.setTypeQNameId(assocTypeQNameId);
        // Source
        NodeEntity sourceNode = new NodeEntity();
        sourceNode.setId(sourceNodeId);
        assoc.setSourceNode(sourceNode);
        // Target
        NodeEntity targetNode = new NodeEntity();
        targetNode.setId(targetNodeId);
        assoc.setTargetNode(targetNode);
        // Index
        assoc.setAssocIndex(assocIndex);

        template.insert(INSERT_NODE_ASSOC, assoc);
        return assoc.getId();
    }

    @Override
    protected int updateNodeAssoc(Long id, int assocIndex) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        assoc.setId(id);
        assoc.setAssocIndex(assocIndex);

        return template.update(UPDATE_NODE_ASSOC, assoc);
    }

    @Override
    protected int deleteNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        assoc.setTypeQNameId(assocTypeQNameId);
        // Source
        NodeEntity sourceNode = new NodeEntity();
        sourceNode.setId(sourceNodeId);
        assoc.setSourceNode(sourceNode);
        // Target
        NodeEntity targetNode = new NodeEntity();
        targetNode.setId(targetNodeId);
        assoc.setTargetNode(targetNode);

        return template.delete(DELETE_NODE_ASSOC, assoc);
    }

    @Override
    protected int deleteNodeAssocs(List<Long> ids) {
        IdsEntity param = new IdsEntity();
        param.setIds(ids);
        return template.delete(DELETE_NODE_ASSOCS, param);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<NodeAssocEntity> selectNodeAssocs(Long nodeId) {
        // Node
        NodeEntity node = new NodeEntity();
        node.setId(nodeId);

        return template.selectList(SELECT_NODE_ASSOCS, node);
    }

    @Override
    protected List<NodeAssocEntity> selectNodeAssocsBySource(Long sourceNodeId, Long typeQNameId) {
        return selectNodeAssocsBySourceAndPropertyValue(sourceNodeId, typeQNameId, null, null);
    }

    @Override
    protected List<NodeAssocEntity> selectNodeAssocsBySourceAndPropertyValue(Long sourceNodeId, Long typeQNameId,
            Long propertyQNameId, NodePropertyValue nodeValue) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        // Source
        NodeEntity sourceNode = new NodeEntity();
        sourceNode.setId(sourceNodeId);
        assoc.setSourceNode(sourceNode);
        // Type
        assoc.setTypeQNameId(typeQNameId);

        // Property
        assoc.setPropertyQNameId(propertyQNameId);
        assoc.setPropertyValue(nodeValue);

        return template.selectList(SELECT_NODE_ASSOCS_BY_SOURCE_AND_PROPERTY_VALUE, assoc);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<NodeAssocEntity> selectNodeAssocsByTarget(Long targetNodeId, Long typeQNameId) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        // Target
        NodeEntity targetNode = new NodeEntity();
        targetNode.setId(targetNodeId);
        assoc.setTargetNode(targetNode);
        // Type
        assoc.setTypeQNameId(typeQNameId);

        return template.selectList(SELECT_NODE_ASSOCS_BY_TARGET, assoc);
    }

    @Override
    protected NodeAssocEntity selectNodeAssocById(Long assocId) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        assoc.setId(assocId);

        return template.selectOne(SELECT_NODE_ASSOC_BY_ID, assoc);
    }

    @Override
    protected int selectNodeAssocMaxIndex(Long sourceNodeId, Long assocTypeQNameId) {
        NodeAssocEntity assoc = new NodeAssocEntity();
        // Source
        NodeEntity sourceNode = new NodeEntity();
        sourceNode.setId(sourceNodeId);
        assoc.setSourceNode(sourceNode);
        // Assoc
        assoc.setTypeQNameId(assocTypeQNameId);

        Integer maxIndex = template.selectOne(SELECT_NODE_ASSOCS_MAX_INDEX, assoc);
        return maxIndex == null ? 0 : maxIndex.intValue();
    }

    @Override
    protected Long insertChildAssoc(ChildAssocEntity assoc) {
        assoc.setVersion(1L);
        template.insert(INSERT_CHILD_ASSOC, assoc);
        return assoc.getId();
    }

    @Override
    protected int deleteChildAssocs(List<Long> ids) {
        IdsEntity idsEntity = new IdsEntity();
        // IDs
        idsEntity.setIds(ids);

        return template.delete(DELETE_CHILD_ASSOCS, idsEntity);
    }

    @Override
    protected int updateChildAssocIndex(Long parentNodeId, Long childNodeId, QName assocTypeQName, QName assocQName,
            int index) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);
        // Type QName
        assoc.setTypeQNameAll(qnameDAO, assocTypeQName, true);
        // QName
        assoc.setQNameAll(qnameDAO, assocQName, true);
        // Index
        assoc.setAssocIndex(index);

        return template.update(UPDATE_CHILD_ASSOCS_INDEX, assoc);
    }

    @Override
    protected int updateChildAssocUniqueName(Long assocId, String name) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        assoc.setId(assocId);
        // Name
        assoc.setChildNodeNameAll(null, null, name);
        return template.update(UPDATE_CHILD_ASSOC_UNIQUE_NAME, assoc);
    }

    @Override
    protected ChildAssocEntity selectChildAssoc(Long assocId) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        assoc.setId(assocId);

        return template.selectOne(SELECT_CHILD_ASSOC_BY_ID, assoc);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<ChildAssocEntity> selectChildNodeIds(Long nodeId, Boolean isPrimary,
            Long minChildNodeIdInclusive, int maxResults) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(nodeId);
        NodeEntity childNode = new NodeEntity();
        childNode.setId(minChildNodeIdInclusive);
        assoc.setParentNode(parentNode);
        assoc.setPrimary(isPrimary);
        assoc.setChildNode(childNode);

        RowBounds rowBounds = new RowBounds(0, maxResults);
        return template.selectList(SELECT_CHILD_NODE_IDS, assoc, rowBounds);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<NodeIdAndAclId> selectPrimaryChildAcls(Long nodeId) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(nodeId);
        assoc.setParentNode(parentNode);
        // Primary
        assoc.setPrimary(true);

        return template.selectList(SELECT_NODE_PRIMARY_CHILD_ACLS, assoc);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<ChildAssocEntity> selectChildAssoc(Long parentNodeId, Long childNodeId, QName assocTypeQName,
            QName assocQName) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);
        // Type QName
        if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
            return Collections.emptyList(); // Shortcut
        }
        // QName
        if (!assoc.setQNameAll(qnameDAO, assocQName, false)) {
            return Collections.emptyList(); // Shortcut
        }
        // Ordered
        assoc.setOrdered(false);

        return template.selectList(SELECT_CHILD_ASSOCS_OF_PARENT, assoc);
    }

    /**
     * Filter to allow the {@link ChildAssocResultHandler} to filter results.
     * 
     * @author Derek Hulley
     * @since 3.4
     */
    private interface ChildAssocResultHandlerFilter {
        boolean isResult(ChildAssocEntity assoc);
    }

    /**
     * Class that pushes results to a {@link ChildAssocRefQueryCallback}.
     * 
     * @author Derek Hulley
     * @since 3.4
     */
    private class ChildAssocResultHandler implements ResultHandler {
        private final ChildAssocResultHandlerFilter filter;
        private final ChildAssocRefQueryCallback resultsCallback;
        private boolean more = true;

        private ChildAssocResultHandler(ChildAssocRefQueryCallback resultsCallback) {
            this(null, resultsCallback);
        }

        private ChildAssocResultHandler(ChildAssocResultHandlerFilter filter,
                ChildAssocRefQueryCallback resultsCallback) {
            this.filter = filter;
            this.resultsCallback = resultsCallback;
        }

        public void handleResult(ResultContext context) {
            // Do nothing if no further results are required
            // TODO: Use iBatis' new feature (when we upgrade) to kill the resultset walking
            if (!more) {
                return;
            }
            ChildAssocEntity assoc = (ChildAssocEntity) context.getResultObject();
            if (filter != null && !filter.isResult(assoc)) {
                // Filtered out
                return;
            }
            Pair<Long, ChildAssociationRef> childAssocPair = assoc.getPair(qnameDAO);
            Pair<Long, NodeRef> parentNodePair = assoc.getParentNode().getNodePair();
            Pair<Long, NodeRef> childNodePair = assoc.getChildNode().getNodePair();
            // Call back
            boolean more = resultsCallback.handle(childAssocPair, parentNodePair, childNodePair);
            if (!more) {
                this.more = false;
            }
        }
    }

    @Override
    protected void selectChildAssocs(Long parentNodeId, Long childNodeId, QName assocTypeQName, QName assocQName,
            Boolean isPrimary, Boolean sameStore, ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Child
        if (childNodeId != null) {
            NodeEntity childNode = new NodeEntity();
            childNode.setId(childNodeId);
            assoc.setChildNode(childNode);
        }
        // Type QName
        if (assocTypeQName != null) {
            if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // QName
        if (assocQName != null) {
            if (!assoc.setQNameAll(qnameDAO, assocQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // Primary
        if (isPrimary != null) {
            assoc.setPrimary(isPrimary);
        }
        // Same store
        if (sameStore != null) {
            assoc.setSameStore(sameStore);
        }
        // Ordered
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        template.select(SELECT_CHILD_ASSOCS_OF_PARENT, assoc, resultHandler);

        resultsCallback.done();
    }

    @Override
    public void selectChildAssocs(Long parentNodeId, QName assocTypeQName, QName assocQName, final int maxResults,
            ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);

        // Type QName
        if (assocTypeQName != null) {
            if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // QName
        if (assocQName != null) {
            if (!assoc.setQNameAll(qnameDAO, assocQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // Order
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        RowBounds rowBounds = new RowBounds(0, maxResults);
        List<?> entities = template.selectList(SELECT_CHILD_ASSOCS_OF_PARENT_LIMITED, assoc, rowBounds);
        final DefaultResultContext resultContext = new DefaultResultContext();
        for (Object entity : entities) {
            resultContext.nextResultObject(entity);
            resultHandler.handleResult(resultContext);
        }

        resultsCallback.done();
    }

    @Override
    protected void selectChildAssocs(Long parentNodeId, Set<QName> assocTypeQNames,
            ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Type QNames
        Set<Long> assocTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false);
        if (assocTypeQNameIds.size() == 0) {
            resultsCallback.done();
            return; // Shortcut as they don't exist
        }
        assoc.setTypeQNameIds(new ArrayList<Long>(assocTypeQNameIds));
        // Ordered
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        template.select(SELECT_CHILD_ASSOCS_OF_PARENT, assoc, resultHandler);

        resultsCallback.done();
    }

    @Override
    protected ChildAssocEntity selectChildAssoc(Long parentNodeId, QName assocTypeQName, String childName) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Type QName
        if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
            return null; // Shortcut
        }
        // Child name
        assoc.setChildNodeNameAll(null, assocTypeQName, childName);
        // Ordered
        assoc.setOrdered(false);

        // Note: This single results was assumed from inception of the original method.  It's correct.
        return template.selectOne(SELECT_CHILD_ASSOC_OF_PARENT_BY_NAME, assoc);
    }

    @Override
    protected void selectChildAssocs(Long parentNodeId, QName assocTypeQName, Collection<String> childNames,
            ChildAssocRefQueryCallback resultsCallback) {
        if (childNames.size() == 0) {
            resultsCallback.done();
            return;
        } else if (childNames.size() > 1000) {
            throw new IllegalArgumentException("Unable to process more than 1000 child names in getChildAssocs");
        }
        // Work out the child names to query on
        final Set<String> childNamesShort = new HashSet<String>(childNames.size());
        final List<Long> childNamesCrc = new ArrayList<Long>(childNames.size());
        for (String childName : childNames) {
            String childNameLower = childName.toLowerCase();
            String childNameShort = ChildAssocEntity.getChildNodeNameShort(childNameLower);
            Long childNameCrc = ChildAssocEntity.getChildNodeNameCrc(childNameLower);
            childNamesShort.add(childNameShort);
            childNamesCrc.add(childNameCrc);
        }
        // Create a filter that checks that the name CRC is present
        ChildAssocResultHandlerFilter filter = new ChildAssocResultHandlerFilter() {
            public boolean isResult(ChildAssocEntity assoc) {
                return childNamesShort.contains(assoc.getChildNodeName());
            }
        };

        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Type QName
        if (assocTypeQName != null) {
            if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // Child names
        assoc.setChildNodeNameCrcs(childNamesCrc);
        // Ordered
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(filter, resultsCallback);

        template.select(SELECT_CHILD_ASSOCS_OF_PARENT, assoc, resultHandler);

        resultsCallback.done();
    }

    @Override
    protected void selectChildAssocsByPropertyValue(Long parentNodeId, QName propertyQName,
            NodePropertyValue nodeValue, ChildAssocRefQueryCallback resultsCallback) {
        ChildPropertyEntity assocProp = new ChildPropertyEntity();
        // Parent
        assocProp.setParentNodeId(parentNodeId);
        // Property name
        Pair<Long, QName> propName = qnameDAO.getQName(propertyQName);
        if (propName == null) {
            resultsCallback.done();
            return;
        }

        // Property
        assocProp.setValue(nodeValue);
        assocProp.setPropertyQNameId(propName.getFirst());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);
        template.select(SELECT_CHILD_ASSOCS_BY_PROPERTY_VALUE, assocProp, resultHandler);
        resultsCallback.done();
    }

    @Override
    protected void selectChildAssocsByChildTypes(Long parentNodeId, Set<QName> childNodeTypeQNames,
            ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Child Node Type QNames
        Set<Long> childNodeTypeQNameIds = qnameDAO.convertQNamesToIds(childNodeTypeQNames, false);
        if (childNodeTypeQNameIds.size() == 0) {
            resultsCallback.done();
            return; // Shortcut as they don't exist
        }
        assoc.setChildNodeTypeQNameIds(new ArrayList<Long>(childNodeTypeQNameIds));
        // Ordered
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        template.select(SELECT_CHILD_ASSOCS_OF_PARENT, assoc, resultHandler);
        resultsCallback.done();
    }

    @Override
    protected void selectChildAssocsWithoutParentAssocsOfType(Long parentNodeId, QName assocTypeQName,
            ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Type QName
        if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
            resultsCallback.done();
            return; // Shortcut
        }
        // Ordered
        assoc.setOrdered(resultsCallback.orderResults());

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        template.select(SELECT_CHILD_ASSOCS_OF_PARENT_WITHOUT_PARENT_ASSOCS_OF_TYPE, assoc, resultHandler);
        resultsCallback.done();
    }

    @Override
    public List<Node> selectChildAssocsWithoutNodeAssocsOfTypes(Long parentNodeId, Long minNodeId, Long maxNodeId,
            Set<QName> assocTypeQNames) {
        IdsEntity idsEntity = new IdsEntity();

        // Parent node id
        Assert.notNull(parentNodeId, "The parent node id must not be null.");
        idsEntity.setIdOne(parentNodeId);
        // Node ids selection interval
        idsEntity.setIdTwo(minNodeId);
        idsEntity.setIdThree(maxNodeId);
        // Associations types to exclude
        if (assocTypeQNames != null) {
            Set<Long> childNodeTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false);
            if (childNodeTypeQNameIds.size() > 0) {
                idsEntity.setIds(new ArrayList<Long>(childNodeTypeQNameIds));
            }
        }

        return template.selectList(SELECT_CHILD_ASSOCS_OF_PARENT_WITHOUT_NODE_ASSOCS_OF_TYPE, idsEntity);
    }

    @Override
    protected List<ChildAssocEntity> selectPrimaryParentAssocs(Long childNodeId) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);
        // Primary
        assoc.setPrimary(Boolean.TRUE);

        return template.selectList(SELECT_PARENT_ASSOCS_OF_CHILD, assoc);
    }

    @Override
    protected void selectParentAssocs(Long childNodeId, QName assocTypeQName, QName assocQName, Boolean isPrimary,
            ChildAssocRefQueryCallback resultsCallback) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);
        // Type QName
        if (assocTypeQName != null) {
            if (!assoc.setTypeQNameAll(qnameDAO, assocTypeQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // QName
        if (assocQName != null) {
            if (!assoc.setQNameAll(qnameDAO, assocQName, false)) {
                resultsCallback.done();
                return; // Shortcut
            }
        }
        // Primary
        if (isPrimary != null) {
            assoc.setPrimary(isPrimary);
        }

        ChildAssocResultHandler resultHandler = new ChildAssocResultHandler(resultsCallback);

        template.select(SELECT_PARENT_ASSOCS_OF_CHILD, assoc, resultHandler);

        resultsCallback.done();
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<ChildAssocEntity> selectParentAssocs(Long childNodeId) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);

        return template.selectList(SELECT_PARENT_ASSOCS_OF_CHILD, assoc);
    }

    @Override
    protected int updatePrimaryParentAssocs(Long childNodeId, Long parentNodeId, QName assocTypeQName,
            QName assocQName, String childNodeName) {
        ChildAssocEntity assoc = new ChildAssocEntity();
        // Parent
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        assoc.setParentNode(parentNode);
        // Child
        NodeEntity childNode = new NodeEntity();
        childNode.setId(childNodeId);
        assoc.setChildNode(childNode);
        // Type QName
        if (assocTypeQName != null) {
            assoc.setTypeQNameAll(qnameDAO, assocTypeQName, true);
            // Have to recalculate the crc values for the association
            assoc.setChildNodeNameAll(dictionaryService, assocTypeQName, childNodeName);
        }
        // QName
        if (assocQName != null) {
            assoc.setQNameAll(qnameDAO, assocQName, true);
        }
        // Primary
        assoc.setPrimary(Boolean.TRUE);

        return template.update(UPDATE_PARENT_ASSOCS_OF_CHILD, assoc);
    }

    @Override
    protected void moveNodeData(Long fromNodeId, Long toNodeId) {
        IdsEntity params = new IdsEntity();
        params.setIdOne(fromNodeId);
        params.setIdTwo(toNodeId);

        int countPA = template.update(UPDATE_MOVE_PARENT_ASSOCS, params);
        int countCA = template.update(UPDATE_MOVE_CHILD_ASSOCS, params);
        int countSA = template.update(UPDATE_MOVE_SOURCE_ASSOCS, params);
        int countTA = template.update(UPDATE_MOVE_TARGET_ASSOCS, params);
        int countP = template.update(UPDATE_MOVE_PROPERTIES, params);
        int countA = template.update(UPDATE_MOVE_ASPECTS, params);
        if (isDebugEnabled) {
            logger.debug("Moved node data: \n" + "   From: " + fromNodeId + "\n" + "   To:   " + toNodeId + "\n"
                    + "   PA:   " + countPA + "\n" + "   CA:   " + countCA + "\n" + "   SA:   " + countSA + "\n"
                    + "   TA:   " + countTA + "\n" + "   P:    " + countP + "\n" + "   A:    " + countA);
        }
    }

    /**
     * The default implementation relies on <b>ON DELETE CASCADE</b> and the
     * subscriptions avoiding deleted nodes - NoOp.
     */
    @Override
    protected void deleteSubscriptions(Long nodeId) {
    }

    @SuppressWarnings("unchecked")
    @Override
    protected Transaction selectLastTxnBeforeCommitTime(Long maxCommitTime) {
        Assert.notNull(maxCommitTime, "maxCommitTime");

        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setMaxCommitTime(maxCommitTime);

        List<Transaction> txns = template.selectList(SELECT_TXN_LAST, query, new RowBounds(0, 1));
        if (txns.size() > 0) {
            return txns.get(0);
        } else {
            return null;
        }
    }

    @Override
    protected int selectTransactionCount() {
        return template.selectOne(SELECT_TXN_COUNT);
    }

    @Override
    protected Transaction selectTxnById(Long txnId) {
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setId(txnId);

        return template.selectOne(SELECT_TXNS, query);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<NodeEntity> selectTxnChanges(Long txnId, Long storeId) {
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setId(txnId);
        if (storeId != null) {
            query.setStoreId(storeId);
        }

        // TODO: Return List<Node> for quicker node_deleted access
        return template.selectList(SELECT_TXN_NODES, query);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<Transaction> selectTxns(Long fromTimeInclusive, Long toTimeExclusive, Integer count,
            List<Long> includeTxnIds, List<Long> excludeTxnIds, Long excludeServerId, Boolean ascending) {
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setMinCommitTime(fromTimeInclusive);
        query.setMaxCommitTime(toTimeExclusive);

        if ((includeTxnIds != null) && (includeTxnIds.size() > 0)) {
            query.setIncludeTxnIds(includeTxnIds);
        }

        if ((excludeTxnIds != null) && (excludeTxnIds.size() > 0)) {
            query.setExcludeTxnIds(excludeTxnIds);
        }

        query.setExcludeServerId(excludeServerId);
        query.setAscending(ascending);

        if (count == null) {
            return template.selectList(SELECT_TXNS, query);
        } else {
            return template.selectList(SELECT_TXNS, query, new RowBounds(0, count));
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    protected List<Long> selectTxnsUnused(Long minTxnId, Long maxCommitTime, Integer count) {
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setMinId(minTxnId);
        query.setMaxCommitTime(maxCommitTime);
        if (count == null) {
            return template.selectList(SELECT_TXNS_UNUSED, query);
        } else {
            return template.selectList(SELECT_TXNS_UNUSED, query, new RowBounds(0, count));
        }
    }

    @Override
    public int deleteTxnsUnused(long fromCommitTime, long toCommitTime) {
        TransactionQueryEntity txnQuery = new TransactionQueryEntity();
        txnQuery.setMinCommitTime(fromCommitTime);
        txnQuery.setMaxCommitTime(toCommitTime);
        int numDeleted = template.delete(DELETE_TXNS_UNUSED, txnQuery);
        return numDeleted;
    }

    @Override
    protected Long selectMinTxnCommitTime() {
        return template.selectOne(SELECT_TXN_MIN_COMMIT_TIME);
    }

    @Override
    protected Long selectMaxTxnCommitTime() {
        return template.selectOne(SELECT_TXN_MAX_COMMIT_TIME);
    }

    @Override
    protected Long selectMinTxnCommitTimeForDeletedNodes() {
        // Get the deleted nodes
        Pair<Long, QName> deletedTypePair = qnameDAO.getQName(ContentModel.TYPE_DELETED);
        if (deletedTypePair == null) {
            // Nothing to do
            return LONG_ZERO;
        }
        TransactionQueryEntity txnQuery = new TransactionQueryEntity();
        txnQuery.setTypeQNameId(deletedTypePair.getFirst());
        return (Long) template.selectOne(SELECT_TXN_MIN_COMMIT_TIME_FOR_NODE_TYPE, txnQuery);
    }

    @Override
    protected Long selectMinTxnId() {
        return template.selectOne(SELECT_TXN_MIN_ID);
    }

    @Override
    protected Long selectMinUnusedTxnCommitTime() {
        return template.selectOne(SELECT_TXN_UNUSED_MIN_COMMIT_TIME);
    }

    @Override
    protected Long selectMaxTxnId() {
        return template.selectOne(SELECT_TXN_MAX_ID);
    }

    public int countChildAssocsByParent(Long parentNodeId, boolean isPrimary) {
        NodeEntity parentNode = new NodeEntity();
        parentNode.setId(parentNodeId);
        ChildAssocEntity childAssoc = new ChildAssocEntity();
        childAssoc.setParentNode(parentNode);
        childAssoc.setPrimary(Boolean.valueOf(isPrimary));
        return template.selectOne(COUNT_CHILD_ASSOC_BY_PARENT_ID, childAssoc);
    }

    /*
     * DAO OVERRIDES
     */

    /**
     * MSSQL requires some overrides to handle specific behaviour.
     */
    public static class MSSQL extends NodeDAOImpl {
        private SqlSessionTemplate template;

        public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
            super.setSqlSessionTemplate(sqlSessionTemplate);
            this.template = sqlSessionTemplate;
        }

        /**
         * Overrides the super class's NO-OP to cascade-delete subscriptions in code.
         */
        @Override
        protected void deleteSubscriptions(Long nodeId) {
            template.delete(DELETE_SUBSCRIPTIONS, nodeId);
        }
    }

    /**
     * MySQL (InnoDB) specific DAO overrides
     */
    public static class MySQL extends NodeDAOImpl {
        private static final String DELETE_TXNS_UNUSED_MYSQL = "alfresco.node.delete_Txns_Unused_MySQL";

        private SqlSessionTemplate template;

        public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
            super.setSqlSessionTemplate(sqlSessionTemplate);
            this.template = sqlSessionTemplate;
        }

        @Override
        public int deleteTxnsUnused(long fromCommitTime, long toCommitTime) {
            TransactionQueryEntity txnQuery = new TransactionQueryEntity();
            txnQuery.setMinCommitTime(fromCommitTime);
            txnQuery.setMaxCommitTime(toCommitTime);
            int numDeleted = template.delete(DELETE_TXNS_UNUSED_MYSQL, txnQuery);
            return numDeleted;
        }
    }

    /**
     * MySQL Cluster NDB specific DAO overrides
     *
     * WARNING: Experimental/unsupported - see AlfrescoMySQLClusterNDBDialect !
     * 
     * @author janv
     */
    public static class MySQLClusterNDB extends MySQL {
        @Override
        protected Long newNodeImplInsert(NodeEntity node) {
            Long id = null;
            try {
                // We need to handle existing deleted nodes.
                NodeRef targetNodeRef = node.getNodeRef();
                Node dbTargetNode = selectNodeByNodeRef(targetNodeRef);
                if (dbTargetNode != null) {
                    if (dbTargetNode.getDeleted(qnameDAO)) {
                        Long dbTargetNodeId = dbTargetNode.getId();
                        // This is OK.  It happens when we create a node that existed in the past.
                        // Remove the row completely
                        deleteNodeProperties(dbTargetNodeId, (Set<Long>) null);
                        deleteNodeById(dbTargetNodeId);
                    } else {
                        // A live node exists.
                        throw new NodeExistsException(dbTargetNode.getNodePair(), null);
                    }
                }

                id = insertNode(node);
            } catch (Throwable e) {
                if (e instanceof NodeExistsException) {
                    throw e;
                }

                // There does not appear to be any row that could prevent an insert
                throw new AlfrescoRuntimeException("Failed to insert new node: " + node, e);
            }

            return id;
        }

        @Override
        protected Long newChildAssocInsert(ChildAssocEntity assoc, QName assocTypeQName, String childNodeName) {
            // no in-txn retry / full rollback on sql exception
            return newChildAssocInsertImpl(assoc, assocTypeQName, childNodeName);
        }

        @Override
        protected int setChildAssocsUniqueNameImpl(Long childNodeId, String childName) {
            // no in-txn retry / full rollback on sql exception
            return updateChildAssocUniqueNameImpl(childNodeId, childName);
        }

        @Override
        protected void updatePrimaryParentAssocs(ChildAssocEntity primaryParentAssoc, Node newParentNode,
                Node childNode, Long newChildNodeId, String childNodeName, Long oldParentNodeId,
                QName assocTypeQName, QName assocQName) {
            // no in-txn retry / full rollback on sql exception
            updatePrimaryParentAssocsImpl(primaryParentAssoc, newParentNode, childNode, newChildNodeId,
                    childNodeName, oldParentNodeId, assocTypeQName, assocQName);
        }
    }

    /**
     * Get most recent transaction made in a given commit time range
     */
    @SuppressWarnings("unchecked")
    public List<Transaction> getOneTxnsByCommitTimeDescending(Long fromTimeInclusive, Long toTimeExclusive,
            boolean remoteOnly) {
        Long serverId = remoteOnly ? getServerId() : null;
        TransactionQueryEntity query = new TransactionQueryEntity();
        query.setMinCommitTime(fromTimeInclusive);
        query.setMaxCommitTime(toTimeExclusive);
        query.setExcludeServerId(serverId);
        return template.selectList(SELECT_ONE_TXNS_BY_COMMIT_TIME_DESC, query);
    }

}