Version 3

    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();
        }
      }
    }