First an example usage:
Criteria criteria = getSession().createCriteria(Foo.class); criteria.add(...); criteria.setResultTransformer( new BatchEvictResultTransformer(getSession(), criteria, 50)); // Read 50 at a time and evict after use ... = criteria.list();
Here is the implementation.
(There may be more correct ways of dealing with the internal Hibernate aspects of this; please feel free to contribute)
import java.util.*; import java.io.Serializable; import org.hibernate.Session; import org.hibernate.Criteria; import org.hibernate.EntityMode; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.engine.PersistenceContext; import org.hibernate.engine.EntityKey; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.transform.ResultTransformer; import org.hibernate.impl.CriteriaImpl; import org.hibernate.impl.SessionImpl; import org.hibernate.impl.SessionFactoryImpl; /** * Result transformer that * a) reads the result in batches of the size provided * b) evicts (= removes from session/first-level cache) the * returned objects as soon as you request the next one from the list, * for example by iterating through it. * @author Mattias Jiderhamn 2008-02-28 */ public class BatchEvictResultTransformer implements ResultTransformer { private final Session session; private final String entityClassName; private List<Serializable> ids; private final int batchSize; private final EntityPersister persister; private final PersistenceContext persistenceContext; public BatchEvictResultTransformer(Session session, Criteria criteria, int batchSize) { this.session = session; this.batchSize = batchSize; criteria.setProjection(Projections.id()); // Get IDs only entityClassName = ((CriteriaImpl) criteria).getEntityOrClassName(); persister = ((SessionFactoryImpl) session.getSessionFactory()) .getEntityPersister(entityClassName); persistenceContext = ((SessionImpl) session).getPersistenceContext(); } public Object transformTuple(Object[] tuple, String[] aliases) { return tuple[0]; // Get the single ID } public List transformList(List collection) { ids = collection; // Remember Ids return new BatchingEvictingList(); // Return batching/evicting proxy } /** * Sublist that will not throw IndexOutOfBoundsException, * but rather return the parts that match * @param list The list to extract a sublist form * @param offset The first index to extract from * @param limit The (maximum) number of records to return. * @return A sublist of the provided list, which may be empty * if the offset is beyond the size. * */ public static <E> List<E> safeSubList(List<E> list, int offset, int limit) { if(offset >= list.size()) // Offset beyond list return new ArrayList<E>(); // Empty result else if(offset + limit > list.size()) // Include rest of list return list.subList(offset, list.size()); else return list.subList(offset, offset + limit); } /** * List that acts as a proxy and handles batch reading and evicting. */ private class BatchingEvictingList<E> extends AbstractList<E> implements List<E> { private Integer lastIndex = null; /** Map that holds the objects currently in memory */ private Map<Serializable, E> currentBatch = new HashMap<Serializable,E>(); /////////////////////////////////////////////////////// // Methods for dealing with the cache /////////////////////////////////////////////////////// /** Remove object from interval cache and Hibernate session */ private void evict(int index) { Serializable id = ids.get(index); E e = currentBatch.remove(id); if(e != null) { session.evict(e); } } /** Read a batch of IDs into internal cache. * Will be stored in Hibernate session cache too. */ private void readBatch(int index) { final List<Serializable> batchIds = ExderUtils.safeSubList(ids, index, batchSize); session.createCriteria(entityClassName) .add(Restrictions.in("id", batchIds)).list(); // Place in persistence context final EntityMode entityMode = session.getEntityMode(); for(Serializable eid : batchIds) { E e = (E) persistenceContext.getEntity(new EntityKey(eid, persister, entityMode)); currentBatch.put(eid, e); // Remember as read } } /////////////////////////////////////////////////////// // Interface implementations /////////////////////////////////////////////////////// public int size() { return ids.size(); } public boolean isEmpty() { return ids.isEmpty(); } public boolean contains(Object o) { throw new UnsupportedOperationException(); } // iterator() implemented by AbstractList public Object[] toArray() { throw new UnsupportedOperationException(); } public Object[] toArray(Object[] a) { throw new UnsupportedOperationException(); } public boolean remove(Object o) { throw new UnsupportedOperationException(); } public boolean containsAll(Collection c) { throw new UnsupportedOperationException(); } public boolean addAll(Collection c) { if(! c.isEmpty()) // To handle this, we need a flag to stop changes after Hibernate is done loading // return ids.addAll(c); // Called from session throw new UnsupportedOperationException("This case needs to be handled"); else return false; } public boolean addAll(int index, Collection c) { throw new UnsupportedOperationException(); } public boolean removeAll(Collection c) { throw new UnsupportedOperationException(); } public boolean retainAll(Collection c) { throw new UnsupportedOperationException(); } public void clear() { throw new UnsupportedOperationException(); } public E get(int index) { if(lastIndex != null && index != lastIndex) { evict(lastIndex); lastIndex = null; } Serializable id = ids.get(index); if(! currentBatch.containsKey(id)) { // Current index not read from database (or read and evicted) readBatch(index); } lastIndex = index; return currentBatch.get(id); } // add(index): Parent causes UnsupportedOperationException // remove(): Parent causes UnsupportedOperationException public int indexOf(Object o) { throw new UnsupportedOperationException(); } public int lastIndexOf(Object o) { throw new UnsupportedOperationException(); } // listIterator() calls listIterator(0) in parent public ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } } }
Comments