org.apache.cayenne.access.DataDomain.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.access.DataDomain.java

Source

/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/

package org.apache.cayenne.access;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataChannel;
import org.apache.cayenne.DataChannelFilter;
import org.apache.cayenne.DataChannelFilterChain;
import org.apache.cayenne.DataChannelSyncCallbackAction;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.QueryResponse;
import org.apache.cayenne.access.jdbc.BatchQueryBuilderFactory;
import org.apache.cayenne.cache.NestedQueryCache;
import org.apache.cayenne.cache.QueryCache;
import org.apache.cayenne.configuration.Constants;
import org.apache.cayenne.configuration.ObjectContextFactory;
import org.apache.cayenne.di.BeforeScopeEnd;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.event.EventManager;
import org.apache.cayenne.graph.CompoundDiff;
import org.apache.cayenne.graph.GraphDiff;
import org.apache.cayenne.log.JdbcEventLogger;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.EntitySorter;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.QueryChain;
import org.apache.cayenne.util.ToStringBuilder;
import org.apache.commons.collections.Transformer;

/**
 * DataDomain performs query routing functions in Cayenne. DataDomain creates single data
 * source abstraction hiding multiple physical data sources from the user. When a child
 * DataContext sends a query to the DataDomain, it is transparently routed to an
 * appropriate DataNode.
 */
public class DataDomain implements QueryEngine, DataChannel {

    public static final String SHARED_CACHE_ENABLED_PROPERTY = "cayenne.DataDomain.sharedCache";
    public static final boolean SHARED_CACHE_ENABLED_DEFAULT = true;

    public static final String VALIDATING_OBJECTS_ON_COMMIT_PROPERTY = "cayenne.DataDomain.validatingObjectsOnCommit";
    public static final boolean VALIDATING_OBJECTS_ON_COMMIT_DEFAULT = true;

    public static final String USING_EXTERNAL_TRANSACTIONS_PROPERTY = "cayenne.DataDomain.usingExternalTransactions";
    public static final boolean USING_EXTERNAL_TRANSACTIONS_DEFAULT = false;

    /**
     * @since 3.1
     */
    @Inject
    protected JdbcEventLogger jdbcEventLogger;

    /**
     * @since 3.1
     */
    protected int maxIdQualifierSize;

    /**
     * @since 3.1
     */
    protected List<DataChannelFilter> filters;

    protected Map<String, DataNode> nodes;
    protected Map<String, DataNode> nodesByDataMapName;
    protected DataNode defaultNode;
    protected Map<String, String> properties;

    protected EntityResolver entityResolver;
    protected DataRowStore sharedSnapshotCache;
    protected TransactionDelegate transactionDelegate;
    protected String name;
    protected QueryCache queryCache;

    // these are initialized from properties...
    protected boolean sharedCacheEnabled;
    protected boolean validatingObjectsOnCommit;
    protected boolean usingExternalTransactions;

    /**
     * @since 1.2
     */
    protected EventManager eventManager;

    /**
     * @since 1.2
     */
    protected EntitySorter entitySorter;

    protected boolean stopped;

    /**
     * Factory for creating QueryBuilders. Might be null, then default one will be used.
     * Server-only.
     * 
     * @deprecated since 3.1 BatchQueryBuilderFactory is injected into JdbcAdapter.
     */
    @Deprecated
    private BatchQueryBuilderFactory queryBuilderFactory;

    /**
     * Creates a DataDomain and assigns it a name.
     */
    public DataDomain(String name) {
        init(name);
        resetProperties();
    }

    /**
     * Creates new DataDomain.
     * 
     * @param name DataDomain name. Domain can be located using its name in the
     *            Configuration object.
     * @param properties A Map containing domain configuration properties.
     */
    public DataDomain(String name, Map properties) {
        init(name);
        initWithProperties(properties);
    }

    private void init(String name) {

        this.filters = new CopyOnWriteArrayList<DataChannelFilter>();
        this.nodesByDataMapName = new ConcurrentHashMap<String, DataNode>();
        this.nodes = new ConcurrentHashMap<String, DataNode>();

        // properties are read-only, so no need for concurrent map, or any specific map
        // for that matter
        this.properties = Collections.EMPTY_MAP;

        setName(name);
    }

    /**
     * Checks that Domain is not stopped. Throws DomainStoppedException otherwise.
     * 
     * @since 3.0
     */
    protected void checkStopped() throws DomainStoppedException {
        if (stopped) {
            throw new DomainStoppedException(
                    "Domain " + name + " was shutdown and can no longer be used to access the database");
        }
    }

    /**
     * @since 3.1
     */
    public EntitySorter getEntitySorter() {
        return entitySorter;
    }

    /**
     * @since 3.1
     */
    public void setEntitySorter(EntitySorter entitySorter) {
        this.entitySorter = entitySorter;
    }

    /**
     * @since 1.1
     */
    protected void resetProperties() {
        properties = Collections.EMPTY_MAP;

        sharedCacheEnabled = SHARED_CACHE_ENABLED_DEFAULT;
        validatingObjectsOnCommit = VALIDATING_OBJECTS_ON_COMMIT_DEFAULT;
        usingExternalTransactions = USING_EXTERNAL_TRANSACTIONS_DEFAULT;
    }

    /**
     * Reinitializes domain state with a new set of properties.
     * 
     * @since 1.1
     */
    public void initWithProperties(Map<String, String> properties) {

        // clone properties to ensure that it is read-only internally
        properties = properties != null ? new HashMap<String, String>(properties) : Collections.EMPTY_MAP;

        String sharedCacheEnabled = properties.get(SHARED_CACHE_ENABLED_PROPERTY);
        String validatingObjectsOnCommit = properties.get(VALIDATING_OBJECTS_ON_COMMIT_PROPERTY);
        String usingExternalTransactions = properties.get(USING_EXTERNAL_TRANSACTIONS_PROPERTY);

        // init ivars from properties
        this.sharedCacheEnabled = (sharedCacheEnabled != null) ? "true".equalsIgnoreCase(sharedCacheEnabled)
                : SHARED_CACHE_ENABLED_DEFAULT;
        this.validatingObjectsOnCommit = (validatingObjectsOnCommit != null)
                ? "true".equalsIgnoreCase(validatingObjectsOnCommit)
                : VALIDATING_OBJECTS_ON_COMMIT_DEFAULT;
        this.usingExternalTransactions = (usingExternalTransactions != null)
                ? "true".equalsIgnoreCase(usingExternalTransactions)
                : USING_EXTERNAL_TRANSACTIONS_DEFAULT;

        this.properties = properties;
    }

    /**
     * Returns EventManager used by this DataDomain.
     * 
     * @since 1.2
     */
    public EventManager getEventManager() {
        return eventManager;
    }

    /**
     * Sets EventManager used by this DataDomain.
     * 
     * @since 1.2
     */
    public void setEventManager(EventManager eventManager) {
        this.eventManager = eventManager;

        if (sharedSnapshotCache != null) {
            sharedSnapshotCache.setEventManager(eventManager);
        }
    }

    /**
     * Returns "name" property value.
     */
    public String getName() {
        return name;
    }

    /**
     * Sets "name" property to a new value.
     */
    public synchronized void setName(String name) {
        this.name = name;
        if (sharedSnapshotCache != null) {
            this.sharedSnapshotCache.setName(name);
        }
    }

    /**
     * Returns <code>true</code> if DataContexts produced by this DataDomain are using
     * shared DataRowStore. Returns <code>false</code> if each DataContext would work with
     * its own DataRowStore. Note that this setting can be overwritten per DataContext.
     * See {@link #createDataContext(boolean)}.
     */
    public boolean isSharedCacheEnabled() {
        return sharedCacheEnabled;
    }

    public void setSharedCacheEnabled(boolean sharedCacheEnabled) {
        this.sharedCacheEnabled = sharedCacheEnabled;
    }

    /**
     * Returns whether child DataContexts default behavior is to perform object validation
     * before commit is executed.
     * 
     * @since 1.1
     */
    public boolean isValidatingObjectsOnCommit() {
        return validatingObjectsOnCommit;
    }

    /**
     * Sets the property defining whether child DataContexts should perform object
     * validation before commit is executed.
     * 
     * @since 1.1
     */
    public void setValidatingObjectsOnCommit(boolean flag) {
        this.validatingObjectsOnCommit = flag;
    }

    /**
     * Returns whether this DataDomain should internally commit all transactions, or let
     * container do that.
     * 
     * @since 1.1
     */
    public boolean isUsingExternalTransactions() {
        return usingExternalTransactions;
    }

    /**
     * Sets a property defining whether this DataDomain should internally commit all
     * transactions, or let container do that.
     * 
     * @since 1.1
     */
    public void setUsingExternalTransactions(boolean flag) {
        this.usingExternalTransactions = flag;
    }

    /**
     * @since 1.1
     * @return a Map of properties for this DataDomain.
     */
    public Map<String, String> getProperties() {
        return properties;
    }

    /**
     * @since 1.1
     * @return TransactionDelegate associated with this DataDomain, or null if no delegate
     *         exist.
     */
    public TransactionDelegate getTransactionDelegate() {
        return transactionDelegate;
    }

    /**
     * Initializes TransactionDelegate used by all DataContexts associated with this
     * DataDomain.
     * 
     * @since 1.1
     */
    public void setTransactionDelegate(TransactionDelegate transactionDelegate) {
        this.transactionDelegate = transactionDelegate;
    }

    /**
     * Returns snapshots cache for this DataDomain, lazily initializing it on the first
     * call if 'sharedCacheEnabled' flag is true.
     */
    public DataRowStore getSharedSnapshotCache() {
        if (sharedSnapshotCache == null && sharedCacheEnabled) {
            this.sharedSnapshotCache = nonNullSharedSnapshotCache();
        }

        return sharedSnapshotCache;
    }

    /**
     * Returns a guaranteed non-null shared snapshot cache regardless of the
     * 'sharedCacheEnabled' flag setting.
     */
    synchronized DataRowStore nonNullSharedSnapshotCache() {
        if (sharedSnapshotCache == null) {
            this.sharedSnapshotCache = new DataRowStore(name, properties, eventManager);
        }

        return sharedSnapshotCache;
    }

    /**
     * Shuts down the previous cache instance, sets cache to the new DataSowStore instance
     * and updates two properties of the new DataSowStore: name and eventManager.
     */
    public synchronized void setSharedSnapshotCache(DataRowStore snapshotCache) {
        if (this.sharedSnapshotCache != snapshotCache) {
            if (this.sharedSnapshotCache != null) {
                this.sharedSnapshotCache.shutdown();
            }
            this.sharedSnapshotCache = snapshotCache;

            if (snapshotCache != null) {
                snapshotCache.setEventManager(getEventManager());
                snapshotCache.setName(getName());
            }
        }
    }

    /**
     * Registers new DataMap with this domain.
     * 
     * @deprecated since 3.1 use a more consistently named {@link #addDataMap(DataMap)}.
     */
    @Deprecated
    public void addMap(DataMap map) {
        addDataMap(map);
    }

    public void addDataMap(DataMap dataMap) {
        getEntityResolver().addDataMap(dataMap);
        refreshEntitySorter();
    }

    /**
     * Returns DataMap matching <code>name</code> parameter.
     * 
     * @deprecated since 3.1 use a more consistently named {@link #getDataMap(String)}.
     */
    public DataMap getMap(String mapName) {
        return getEntityResolver().getDataMap(mapName);
    }

    /**
     * @since 3.1
     */
    public DataMap getDataMap(String mapName) {
        return getEntityResolver().getDataMap(mapName);
    }

    /**
     * Removes named DataMap from this DataDomain and any underlying DataNodes that
     * include it.
     * 
     * @deprecated since 3.1 use a more consistently named {@link #removeDataMap(String)}.
     */
    @Deprecated
    public void removeMap(String mapName) {
        removeDataMap(mapName);
    }

    /**
     * Removes named DataMap from this DataDomain and any underlying DataNodes that
     * include it.
     * 
     * @since 3.1
     */
    public void removeDataMap(String mapName) {
        DataMap map = getDataMap(mapName);
        if (map == null) {
            return;
        }

        // remove from data nodes
        for (DataNode node : nodes.values()) {
            node.removeDataMap(mapName);
        }

        nodesByDataMapName.remove(mapName);

        // remove from EntityResolver
        getEntityResolver().removeDataMap(map);

        refreshEntitySorter();
    }

    /**
     * Removes a DataNode from DataDomain. Any maps previously associated with this node
     * within domain will still be kept around, however they wan't be mapped to any node.
     */
    public void removeDataNode(String nodeName) {
        DataNode removed = nodes.remove(nodeName);
        if (removed != null) {

            removed.setEntityResolver(null);

            Iterator<DataNode> it = nodesByDataMapName.values().iterator();
            while (it.hasNext()) {
                if (it.next() == removed) {
                    it.remove();
                }
            }
        }
    }

    /**
     * Returns a collection of registered DataMaps.
     */
    public Collection<DataMap> getDataMaps() {
        return getEntityResolver().getDataMaps();
    }

    /**
     * Returns an unmodifiable collection of DataNodes associated with this domain.
     */
    public Collection<DataNode> getDataNodes() {
        return Collections.unmodifiableCollection(nodes.values());
    }

    /**
     * Closes all data nodes, removes them from the list of available nodes.
     * 
     * @deprecated since 3.1 unused and unneeded
     */
    @Deprecated
    public void reset() {

        nodes.clear();
        nodesByDataMapName.clear();

        if (entityResolver != null) {
            entityResolver.clearCache();
            entityResolver = null;
        }
    }

    /**
     * Clears the list of internal DataMaps.
     * 
     * @deprecated since 3.1 unused and unneeded
     */
    @Deprecated
    public void clearDataMaps() {
        getEntityResolver().setDataMaps(Collections.EMPTY_LIST);
    }

    /**
     * Adds new DataNode.
     */
    public void addNode(DataNode node) {

        // add node to name->node map
        nodes.put(node.getName(), node);
        node.setEntityResolver(getEntityResolver());

        // add node to "ent name->node" map
        for (DataMap map : node.getDataMaps()) {
            addDataMap(map);
            nodesByDataMapName.put(map.getName(), node);
        }
    }

    /**
     * Creates and returns a new DataContext. If this DataDomain is configured to use
     * shared cache, returned DataContext will use shared cache as well. Otherwise a new
     * instance of DataRowStore will be used as its local cache.
     * 
     * @deprecated since 3.1 as context creation is done via {@link ObjectContextFactory}
     *             and injection.
     */
    @Deprecated
    public DataContext createDataContext() {
        return createDataContext(isSharedCacheEnabled());
    }

    /**
     * Creates a new DataContext.
     * 
     * @param useSharedCache determines whether resulting DataContext should use shared
     *            vs. local cache. This setting overrides default behavior configured for
     *            this DataDomain via {@link #SHARED_CACHE_ENABLED_PROPERTY}.
     * @since 1.1
     * @deprecated since 3.1 as context creation is done via {@link ObjectContextFactory}
     *             and injection.
     */
    @Deprecated
    public DataContext createDataContext(boolean useSharedCache) {
        // for new dataRowStores use the same name for all stores
        // it makes it easier to track the event subject
        DataRowStore snapshotCache = (useSharedCache) ? nonNullSharedSnapshotCache()
                : new DataRowStore(name, properties, eventManager);

        DataContext context = new DataContext(this, new ObjectStore(snapshotCache));

        if (queryCache != null) {
            context.setQueryCache(new NestedQueryCache(queryCache));
        }

        context.setValidatingObjectsOnCommit(isValidatingObjectsOnCommit());
        return context;
    }

    /**
     * Creates and returns a new inactive transaction. Returned transaction is bound to
     * the current execution thread.
     * <p>
     * If there is a TransactionDelegate, adds the delegate to the newly created
     * Transaction. Behavior of the returned Transaction depends on
     * "usingInternalTransactions" property setting.
     * </p>
     * 
     * @since 1.1
     */
    public Transaction createTransaction() {
        if (isUsingExternalTransactions()) {
            Transaction transaction = Transaction.externalTransaction(getTransactionDelegate());
            transaction.setJdbcEventLogger(jdbcEventLogger);
            return transaction;
        } else {
            Transaction transaction = Transaction.internalTransaction(getTransactionDelegate());
            transaction.setJdbcEventLogger(jdbcEventLogger);
            return transaction;
        }
    }

    /**
     * Returns registered DataNode whose name matches <code>name</code> parameter.
     * 
     * @deprecated since 3.1, use a more consistently named {@link #getDataNode(String)}.
     */
    @Deprecated
    public DataNode getNode(String nodeName) {
        return getDataNode(nodeName);
    }

    /**
     * Returns registered DataNode whose name matches <code>name</code> parameter.
     * 
     * @since 3.1
     */
    public DataNode getDataNode(String nodeName) {
        return nodes.get(nodeName);
    }

    /**
     * Updates internal index of DataNodes stored by the entity name.
     * 
     * @deprecated since 3.1 - unneeded and unused.
     */
    @Deprecated
    public void reindexNodes() {
        nodesByDataMapName.clear();

        for (DataNode node : getDataNodes()) {
            for (DataMap map : node.getDataMaps()) {
                addDataMap(map);
                nodesByDataMapName.put(map.getName(), node);
            }
        }
    }

    /**
     * Returns a DataNode that should handle queries for all entities in a DataMap.
     * 
     * @since 1.1
     */
    public DataNode lookupDataNode(DataMap map) {

        DataNode node = nodesByDataMapName.get(map.getName());
        if (node == null) {

            // see if one of the node states has changed, and the map is now linked...
            for (DataNode n : getDataNodes()) {
                for (DataMap m : n.getDataMaps()) {
                    if (m == map) {
                        nodesByDataMapName.put(map.getName(), n);
                        node = n;
                        break;
                    }
                }

                if (node != null) {
                    break;
                }
            }

            if (node == null) {

                if (defaultNode != null) {
                    nodesByDataMapName.put(map.getName(), defaultNode);
                    node = defaultNode;
                } else {
                    throw new CayenneRuntimeException("No DataNode configured for DataMap '" + map.getName()
                            + "' and no default DataNode set");
                }
            }
        }

        return node;
    }

    /**
     * Sets EntityResolver. If not set explicitly, DataDomain creates a default
     * EntityResolver internally on demand.
     * 
     * @since 1.1
     */
    public void setEntityResolver(EntityResolver entityResolver) {
        this.entityResolver = entityResolver;
    }

    // creates default entity resolver if there is none set yet
    private synchronized void createEntityResolver() {
        if (entityResolver == null) {
            // entity resolver will be self-indexing as we add all our maps
            // to it as they are added to the DataDomain
            entityResolver = new EntityResolver();
        }
    }

    /**
     * Shutdowns all owned data nodes and marks this domain as stopped.
     */
    @BeforeScopeEnd
    public void shutdown() {
        if (!stopped) {
            stopped = true;

            if (sharedSnapshotCache != null) {
                sharedSnapshotCache.shutdown();
            }

            // deprecated - noop code for backwards compatibility as DataNode shutdown is
            // no longer needed
            for (DataNode node : getDataNodes()) {
                node.shutdown();
            }
        }
    }

    /**
     * Routes queries to appropriate DataNodes for execution.
     */
    public void performQueries(final Collection<? extends Query> queries, final OperationObserver callback) {

        runInTransaction(new Transformer() {

            public Object transform(Object input) {
                new DataDomainLegacyQueryAction(DataDomain.this, new QueryChain(queries), callback).execute();
                return null;
            }
        });
    }

    // ****** DataChannel methods:

    /**
     * Runs query returning generic QueryResponse.
     * 
     * @since 1.2
     */
    public QueryResponse onQuery(final ObjectContext originatingContext, final Query query) {
        checkStopped();

        return new DataDomainQueryFilterChain().onQuery(originatingContext, query);
    }

    QueryResponse onQueryNoFilters(final ObjectContext originatingContext, final Query query) {
        // transaction note:
        // we don't wrap this code in transaction to reduce transaction scope to
        // just the DB operation for better performance ... query action will
        // start a transaction itself when and if needed
        return new DataDomainQueryAction(originatingContext, DataDomain.this, query).execute();
    }

    /**
     * Returns an EntityResolver that stores mapping information for this domain.
     */
    public EntityResolver getEntityResolver() {
        if (entityResolver == null) {
            createEntityResolver();
        }

        return entityResolver;
    }

    /**
     * Only handles commit-type synchronization, ignoring any other type.
     * 
     * @since 1.2
     */
    public GraphDiff onSync(final ObjectContext originatingContext, final GraphDiff changes, int syncType) {

        checkStopped();

        return new DataDomainSyncFilterChain().onSync(originatingContext, changes, syncType);
    }

    GraphDiff onSyncNoFilters(final ObjectContext originatingContext, final GraphDiff changes, int syncType) {
        DataChannelSyncCallbackAction callbackAction = DataChannelSyncCallbackAction.getCallbackAction(
                getEntityResolver().getCallbackRegistry(), originatingContext.getGraphManager(), changes, syncType);

        callbackAction.applyPreCommit();

        GraphDiff result;
        switch (syncType) {
        case DataChannel.ROLLBACK_CASCADE_SYNC:
            result = onSyncRollback(originatingContext);
            break;
        // "cascade" and "no_cascade" are the same from the DataDomain
        // perspective,
        // including transaction handling logic
        case DataChannel.FLUSH_NOCASCADE_SYNC:
        case DataChannel.FLUSH_CASCADE_SYNC:
            result = (GraphDiff) runInTransaction(new Transformer() {

                public Object transform(Object input) {
                    return onSyncFlush(originatingContext, changes);
                }
            });
            break;
        default:
            throw new CayenneRuntimeException("Invalid synchronization type: " + syncType);
        }

        callbackAction.applyPostCommit();
        return result;
    }

    GraphDiff onSyncRollback(ObjectContext originatingContext) {
        // if there is a transaction in progress, roll it back

        Transaction transaction = Transaction.getThreadTransaction();
        if (transaction != null) {
            transaction.setRollbackOnly();
        }

        return new CompoundDiff();
    }

    GraphDiff onSyncFlush(ObjectContext originatingContext, GraphDiff childChanges) {

        if (!(originatingContext instanceof DataContext)) {
            throw new CayenneRuntimeException(
                    "No support for committing ObjectContexts that are not DataContexts yet. "
                            + "Unsupported context: " + originatingContext);
        }

        DataDomainFlushAction action = new DataDomainFlushAction(this);
        action.setJdbcEventLogger(jdbcEventLogger);

        return action.flush((DataContext) originatingContext, childChanges);
    }

    /**
     * Executes Transformer.transform() method in a transaction. Transaction policy is to
     * check for the thread transaction, and use it if one exists. If it doesn't, a new
     * transaction is created, with a scope limited to this method.
     */
    // WARNING: (andrus) if we ever decide to make this method protected or public, we
    // need to change the signature to avoid API dependency on commons-collections
    Object runInTransaction(Transformer operation) {

        // user or container-managed or nested transaction
        if (Transaction.getThreadTransaction() != null) {
            return operation.transform(null);
        }

        // Cayenne-managed transaction

        Transaction transaction = createTransaction();
        Transaction.bindThreadTransaction(transaction);

        try {
            // implicit begin..
            Object result = operation.transform(null);
            transaction.commit();
            return result;
        } catch (Exception ex) {
            transaction.setRollbackOnly();

            // must rethrow
            if (ex instanceof CayenneRuntimeException) {
                throw (CayenneRuntimeException) ex;
            } else {
                throw new CayenneRuntimeException(ex);
            }
        } finally {
            Transaction.bindThreadTransaction(null);
            if (transaction.getStatus() == Transaction.STATUS_MARKED_ROLLEDBACK) {
                try {
                    transaction.rollback();
                } catch (Exception rollbackEx) {
                    // although we don't expect an exception here, print the stack, as
                    // there have been some Cayenne bugs already (CAY-557) that were
                    // masked by this 'catch' clause.
                    jdbcEventLogger.logQueryError(rollbackEx);
                }
            }
        }
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("name", name).toString();
    }

    /**
     * Returns shared {@link QueryCache} used by this DataDomain.
     * 
     * @since 3.0
     */
    public QueryCache getQueryCache() {
        return queryCache;
    }

    public void setQueryCache(QueryCache queryCache) {
        this.queryCache = queryCache;
    }

    /**
     * Sets factory for creating QueryBuilders
     * 
     * @deprecated since 3.1 BatchQueryBuilderFactory is injected into JdbcAdapter.
     */
    @Deprecated
    public void setQueryBuilderFactory(BatchQueryBuilderFactory queryBuilderFactory) {
        this.queryBuilderFactory = queryBuilderFactory;
    }

    /**
     * @return factory for creating QueryBuilders. Might be null
     * @deprecated since 3.1 BatchQueryBuilderFactory is injected into JdbcAdapter.
     */
    @Deprecated
    public BatchQueryBuilderFactory getQueryBuilderFactory() {
        return queryBuilderFactory;
    }

    /**
     * @since 3.1
     */
    JdbcEventLogger getJdbcEventLogger() {
        return jdbcEventLogger;
    }

    void refreshEntitySorter() {
        if (entitySorter != null) {
            entitySorter.setEntityResolver(getEntityResolver());
        }
    }

    /**
     * Returns an unmodifiable list of filters registered with this DataDomain.
     * <p>
     * Filter ordering note: filters are applied in reverse order of their occurrence in
     * the filter list. I.e. the last filter in the list called first in the chain.
     * 
     * @since 3.1
     */
    public List<DataChannelFilter> getFilters() {
        return Collections.unmodifiableList(filters);
    }

    /**
     * Adds a new filter, calling its 'init' method.
     * 
     * @since 3.1
     */
    public void addFilter(DataChannelFilter filter) {
        filter.init(this);
        filters.add(filter);
    }

    /**
     * Removes a filter from the filter chain.
     * 
     * @since 3.1
     */
    public void removeFilter(DataChannelFilter filter) {
        filters.remove(filter);
    }

    abstract class DataDomainFilterChain implements DataChannelFilterChain {

        private int i;

        DataDomainFilterChain() {
            i = filters != null ? filters.size() : 0;
        }

        DataChannelFilter nextFilter() {
            // filters are ordered innermost to outermost
            i--;
            return i >= 0 ? filters.get(i) : null;
        }
    }

    final class DataDomainQueryFilterChain extends DataDomainFilterChain {

        public QueryResponse onQuery(ObjectContext originatingContext, Query query) {

            DataChannelFilter filter = nextFilter();
            return (filter != null) ? filter.onQuery(originatingContext, query, this)
                    : onQueryNoFilters(originatingContext, query);
        }

        public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType) {
            throw new UnsupportedOperationException("It is illegal to call 'onSync' inside 'onQuery' chain");
        }
    }

    final class DataDomainSyncFilterChain extends DataDomainFilterChain {

        public GraphDiff onSync(final ObjectContext originatingContext, final GraphDiff changes, int syncType) {

            DataChannelFilter filter = nextFilter();
            return (filter != null) ? filter.onSync(originatingContext, changes, syncType, this)
                    : onSyncNoFilters(originatingContext, changes, syncType);
        }

        public QueryResponse onQuery(ObjectContext originatingContext, Query query) {
            throw new UnsupportedOperationException("It is illegal to call 'onQuery' inside 'onSync' chain");
        }
    }

    /**
     * An optional DataNode that is used for DataMaps that are not linked to a DataNode
     * explicitly.
     * 
     * @since 3.1
     */
    public DataNode getDefaultNode() {
        return defaultNode;
    }

    /**
     * @since 3.1
     */
    public void setDefaultNode(DataNode defaultNode) {
        this.defaultNode = defaultNode;
    }

    /**
     * Returns a maximum number of object IDs to match in a single query for queries that
     * select objects based on collection of ObjectIds. This affects queries generated by
     * Cayenne when processing paginated queries and DISJOINT_BY_ID prefetches and is
     * intended to address database limitations on the size of SQL statements as well as
     * to cap memory use in Cayenne when generating such queries. The default is 10000. It
     * can be changed either by calling {@link #setMaxIdQualifierSize(int)} or changing
     * the value for property {@link Constants#SERVER_MAX_ID_QUALIFIER_SIZE_PROPERTY}.
     * 
     * @since 3.1
     */
    public int getMaxIdQualifierSize() {
        return maxIdQualifierSize;
    }

    /**
     * @since 3.1
     */
    public void setMaxIdQualifierSize(int maxIdQualifierSize) {
        this.maxIdQualifierSize = maxIdQualifierSize;
    }
}