21. ledna 2009

Jak selektivně podle třídy mazat objekty z Hibernate session

Moje honba za LazyInitializationException byla úspěšná (musím se přiznat, že velkým pomocníkem mi byl Eclipse Memory Analyzer, který mi pomohl zjistit, kdo se na objekt odkazuje). Problém byl v tom, že jsem chtěl vymazat objekty ze Sessiony, ale protože jsem neznal jednotlivé instance, zavolal jsem metodu clear a znova zaasocioval to co jsem si myslel, že je důležité (a na něco jsem samozřejmě zapomněl).

V první fázi opravy chyby jsem tedy zaasocioval i další objekty a vše začalo fungovat. Pak mě ovšem postihla myšlenka, proč se mám já a můj kód, který by ideálně měl být na použitém ORM nezávislý, přizpůsobovat Hibernate. Tak jsem začal pátrat v javadoc, zda neexistuje nějaká možnost, jak zjistit jaké objekty jsou v Sessioně uložené. Pak by je stačilo selektivně odstranit pomocí metody evict.

Odpověď zní: existuje. Session má metodu getStatistics, která vrací SessionStatistics. Ten nám umožní zjistit jaké kolekce a jaké entity (včetně jejich identifikátoru) máme v sessioně uloženy. Takže stačí projít tyto údaje a v klidu vymazat co potřebujeme. Napsal jsem na to takovou drobnou třídu:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.engine.CollectionKey;
import org.hibernate.engine.EntityKey;
import org.hibernate.stat.SessionStatistics;

import cz.svt.util.Tuple;
import cz.svt.util.Tuple2;

/**
 * This class is capable to print the summary of Hibernate cache content and also is able to evict objects from cache.
 */
public class HibernateCachceEvicter {
  
/** The Hibernate session. */
  
protected Session session;

  
protected static final Log log = LogFactory.getLog(HibernateCachceEvicter.class);

  
/**
    * Creates the empty evicter.
    */
  
public HibernateCachceEvicter() {
     
super();
  
}

  
/**
    * Creates the evicter and sets the transaction.
    *
@param session The session
    */
  
public HibernateCachceEvicter(Session session) {
     
super();
      setSession
(session);
  
}

  
/**
    * Sets the Hibernate session.
    *
@param session The session
    */
  
public HibernateCachceEvicter setSession(Session session) {
     
this.session = session;
      return this;
  
}

  
/**
    * Prints the content of the Hibernate cache grouped by the collections role and entity name.
    *
@return The cache content, first is entities and second collections
    */
  
public Tuple2<TreeMap<String, Counter>, TreeMap<String, Counter>> logCacheContent() {
     
if ((session != null) && (log.isInfoEnabled())) {
        
try {
           
SessionStatistics statistics = session.getStatistics();
            TreeMap<String, Counter> entities =
new TreeMap<String, Counter>();
           
for (Object key : statistics.getEntityKeys())
              
getCounter(entities, ((EntityKey) key).getEntityName()).inc();
            TreeMap<String, Counter> collections =
new TreeMap<String, Counter>();
           
for (Object key : statistics.getCollectionKeys())
              
getCounter(collections, ((CollectionKey) key).getRole()).inc();
            log.info
("The hibernate cache contains " + statistics.getEntityCount() + " entities.");
           
for (String key : entities.keySet())
              
log.info("  " + key + "=" + entities.get(key).getValue());
            log.info
("The hibernate cache contains " + statistics.getCollectionCount() + " collections.");
           
for (String key : collections.keySet())
              
log.info("  " + key + "=" + collections.get(key).getValue());
           
return Tuple.newTuple(entities, collections);
        
} catch (HibernateException e) {
           
log.warn("Exception during walking through Hibernate cache to log its content", e);
        
}
      }
     
return Tuple.newTuple(null, null);
  
}

  
/**
    * Returns the counter from the map. If there is no such counter yet, creates new one and put it into map.
    *
@param map The map
    *
@param key The key
    *
@return The counter
    */
  
protected Counter getCounter(TreeMap<String, Counter> map, String key) {
     
Counter counter = map.get(key);
     
if (counter == null) map.put(key, counter = new Counter());
     
return counter;
  
}

  
/**
    * Evicts the object of the specified classes from Hibernate cache.
    *
@param classNames The array of the classNames to evict from cache
    */
  
public void evictFromCache(String... classNames) {
     
evictFromCache(new TreeSet<String>(Arrays.asList(classNames)));
  
}

  
/**
    * Evicts the object of the specified classes from Hibernate cache.
    *
@param classNames The sorted set of the classNames to evict from cache
    */
  
public void evictFromCache(SortedSet<String> classNames) {
     
if (session == null) return;
      log.info
("Cache before evicting:");
      TreeMap<String, Counter> entities = logCacheContent
().getV1();
     
try {
        
int entitiesToEvictCount = 0;
        
for (String key : entities.keySet())
           
if (classNames.contains(key)) entitiesToEvictCount += entities.get(key).getValue();
         List<Object> entitiesToEvict =
new ArrayList<Object>(entitiesToEvictCount);
        
for (Object key : session.getStatistics().getEntityKeys()) {
           
EntityKey eKey = (EntityKey) key;
           
if (classNames.contains(eKey.getEntityName()))
             
entitiesToEvict.add(session.load(eKey.getEntityName(), eKey.getIdentifier()));
        
}
        
for (Object entityToEvict : entitiesToEvict)
           
session.evict(entityToEvict);
         log.warn
("Evicted " + entitiesToEvictCount + "entities.");
     
} catch (HibernateException e) {
        
log.warn("Exception during evicting objects from cache", e);
     
}
     
log.info("Cache after evicting:");
      logCacheContent
();
  
}

  
/**
    * This class is counter.
    */
  
protected static class Counter {
     
/** The value of the counter. */
     
int value;

     
/**
       * Creates the counter a resets the value.
       */
     
public Counter() {
        
super();
        
value = 0;
     
}

     
/**
       * Increments the value of the counter.
       */
     
public void inc() {
        
value++;
     
}

     
/**
       * Returns the value.
       *
@return The actual counter value
       */
     
public int getValue() {
        
return value;
     
}

   }

}


Zajímavé je metoda evictFromCache, která provádí mazání objektů ze sessiony. Nejprve si pomocí logCacheContent vypíšu obsah cache (seskupený podle tříd) před mazáním. Pak si zjistím kolik instancí budu mazat (podle výstupu z vypsání obsahu sessiony) a vytvořím patřičně veliké pole. Následuje procházení sessiony a pokud narazím na třídu, kterou chci vymazat, podle identifikátoru instance ji natáhnu metodou load a vložím do pole. Teprve poté je mažu (byl jsem línej, zkoušet, zda by se to nějak nepralo, kdybych zároveň procházel obsah sessiony a zároveň z ní mazal). Na závěr si jenom pro porovnání udělám výpis obsahu po mazání.

Protože nechci mít v kódu odkaz na takovou ošklivou Hibernatí záležitost, udělal jsem si jednoduchý aspect, který mi na požadovaná místa dotlačí vymazání instancí patřičných tříd.

Předpokládám, že to již někdo řešil, ale pokud to někomu pomůže, pak jen berte inspiraci.

PS. Třídu Counter jsem v příkladu nechal jenom pro úplnost.

Žádné komentáře: