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