Refactored out DBBean cache logic and also refactored test cases

This commit is contained in:
Ziver Koc 2017-02-14 17:52:59 +01:00
parent cefd99f6c4
commit 0346fd21ba
7 changed files with 640 additions and 460 deletions

View file

@ -40,6 +40,7 @@ import java.sql.SQLException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -67,9 +68,6 @@ import java.util.logging.Logger;
public abstract class DBBean { public abstract class DBBean {
private static final Logger logger = LogUtil.getLogger(); private static final Logger logger = LogUtil.getLogger();
/** The id of the bean **/
protected Long id;
/** /**
* Sets the name of the table in the database * Sets the name of the table in the database
*/ */
@ -109,18 +107,24 @@ public abstract class DBBean {
} }
/** This value is for preventing recursive loops when saving */
protected boolean processing_save; /** A unique id of the bean **/
private Long id;
/** This lock is for preventing recursive loops and concurrency when saving */
protected ReentrantLock saveLock;
/** This value is for preventing recursive loops when updating */ /** This value is for preventing recursive loops when updating */
protected boolean processing_update; protected ReentrantLock readLock;
protected DBBean(){ protected DBBean(){
DBBeanConfig.getBeanConfig(this.getClass()); DBBeanConfig.getBeanConfig(this.getClass());
processing_save = false; saveLock = new ReentrantLock();
processing_update = false; readLock = new ReentrantLock();
} }
/** /**
* Saves the object and all the sub objects to the DB * Saves the object and all the sub objects to the DB
* *
@ -134,151 +138,161 @@ public abstract class DBBean {
* Saves the Object to the DB * Saves the Object to the DB
* *
* @param db is the DBMS connection * @param db is the DBMS connection
* @param recursive is if the method should save all sub objects * @param recursive is if all sub object also should be saved
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void save(DBConnection db, boolean recursive) throws SQLException{ public void save(DBConnection db, boolean recursive) throws SQLException{
if( processing_save ) if (saveLock.isHeldByCurrentThread()) // If the current thread already has a lock then this
return; return; // is a recursive call and we do not need to do anything
processing_save = true; else if (saveLock.tryLock()) {
Class<? extends DBBean> c = this.getClass(); Class<? extends DBBean> c = this.getClass();
DBBeanConfig config = DBBeanConfig.getBeanConfig( c ); DBBeanConfig config = DBBeanConfig.getBeanConfig(c);
try { try {
// Generate the SQL // Generate the SQL
StringBuilder query = new StringBuilder(); StringBuilder query = new StringBuilder();
if (this.id == null) {
query.append("INSERT INTO ").append(config.tableName).append(' ');
StringBuilder sqlCols = new StringBuilder();
StringBuilder sqlValues = new StringBuilder();
for (Field field : config.fields) {
if (!List.class.isAssignableFrom(field.getType())) {
if (sqlCols.length() == 0)
sqlCols.append("(");
else sqlCols.append(", ");
sqlCols.append(DBBeanConfig.getFieldName(field));
if (sqlValues.length() == 0)
sqlValues.append("VALUES(");
else sqlValues.append(", ");
sqlValues.append("?");
}
}
if (sqlCols.length() > 0) {
query.append(sqlCols).append(") ");
query.append(sqlValues).append(") ");
} else
query.append("DEFAULT VALUES");
} else {
query.append("UPDATE ").append(config.tableName).append(' ');
StringBuilder sqlSets = new StringBuilder();
for (Field field : config.fields) {
if (!List.class.isAssignableFrom(field.getType())) {
if (sqlSets.length() > 0)
sqlSets.append(", ");
sqlSets.append(DBBeanConfig.getFieldName(field));
sqlSets.append("=?");
}
}
if (sqlSets.length() > 0) {
query.append("SET ").append(sqlSets);
query.append("WHERE ").append(config.idColumn).append("=?");
} else
query = null; // Class has no fields that needs updating
}
// Check if we have a valid query to run, skip otherwise
if (query != null) {
String sql = query.toString();
logger.finest("Save Bean(" + c.getName() + ", id: " + this.getId() + ") query: " + sql);
PreparedStatement stmt = db.getPreparedStatement(sql);
// Put in the variables in the SQL
int index = 1;
for (Field field : config.fields) {
// Another DBBean class
if (DBBean.class.isAssignableFrom(field.getType())) {
DBBean subObj = (DBBean) getFieldValue(field);
if (subObj != null) {
if (recursive || subObj.getId() == null)
subObj.save(db);
stmt.setObject(index, subObj.getId());
} else
stmt.setObject(index, null);
index++;
}
// A list of DBBeans
else if (List.class.isAssignableFrom(field.getType()) &&
field.getAnnotation(DBLinkTable.class) != null) {
// Do stuff later
}
// Normal field
else {
Object value = getFieldValue(field);
stmt.setObject(index, value);
index++;
}
}
if (this.id != null)
stmt.setObject(index, this.id);
// Execute the SQL
DBConnection.exec(stmt);
if (this.id == null) { if (this.id == null) {
this.id = db.getLastInsertID(stmt); query.append("INSERT INTO ").append(config.tableName).append(' ');
// as this is a new object so add it to the cache StringBuilder sqlCols = new StringBuilder();
DBBeanSQLResultHandler.cacheDBBean(this); StringBuilder sqlValues = new StringBuilder();
for (Field field : config.fields) {
if (!List.class.isAssignableFrom(field.getType())) {
if (sqlCols.length() == 0)
sqlCols.append("(");
else sqlCols.append(", ");
sqlCols.append(DBBeanConfig.getFieldName(field));
if (sqlValues.length() == 0)
sqlValues.append("VALUES(");
else sqlValues.append(", ");
sqlValues.append("?");
}
}
if (sqlCols.length() > 0) { // Did we generate any query?
query.append(sqlCols).append(") ");
query.append(sqlValues).append(") ");
} else
query.append("DEFAULT VALUES");
} else {
query.append("UPDATE ").append(config.tableName).append(' ');
StringBuilder sqlSets = new StringBuilder();
for (Field field : config.fields) {
if (!List.class.isAssignableFrom(field.getType())) {
if (sqlSets.length() > 0)
sqlSets.append(", ");
sqlSets.append(DBBeanConfig.getFieldName(field));
sqlSets.append("=?");
}
}
if (sqlSets.length() > 0) { // Did we generate any query?
query.append("SET ").append(sqlSets);
query.append("WHERE ").append(config.idColumn).append("=?");
} else
query = null; // Class has no fields that needs updating
} }
// Check if we have a valid query to run, skip otherwise
if (query != null) {
String sql = query.toString();
logger.finest("Save Bean(" + c.getName() + ", id: " + this.getId() + ") query: " + sql);
PreparedStatement stmt = db.getPreparedStatement(sql);
// Put in the variables in the SQL
int index = 1;
for (Field field : config.fields) {
// Another DBBean class
if (DBBean.class.isAssignableFrom(field.getType())) {
DBBean subObj = (DBBean) getFieldValue(field);
if (subObj != null) {
if (recursive || subObj.getId() == null)
subObj.save(db);
stmt.setObject(index, subObj.getId());
} else
stmt.setObject(index, null);
index++;
}
// A list of DBBeans
else if (List.class.isAssignableFrom(field.getType()) &&
field.getAnnotation(DBLinkTable.class) != null) {
// Do stuff later
}
// Normal field
else {
Object value = getFieldValue(field);
stmt.setObject(index, value);
index++;
}
}
if (this.id != null)
stmt.setObject(index, this.id);
// Execute the SQL
DBConnection.exec(stmt);
if (this.id == null) {
this.id = db.getLastInsertID(stmt);
// Add this bean to the cache
DBBeanCache.add(this);
}
}
// Save sub beans, after we get the parent object id
for (Field field : config.fields) {
if (List.class.isAssignableFrom(field.getType()) &&
field.getAnnotation(DBLinkTable.class) != null) {
if (this.id == null)
throw new SQLException("Unknown parent object id");
List<DBBean> list = (List<DBBean>) getFieldValue(field);
if (list != null) {
DBLinkTable linkTableAnnotation = field.getAnnotation(DBLinkTable.class);
String linkTable = linkTableAnnotation.table();
String idCol = (linkTableAnnotation.idColumn().isEmpty() ? config.tableName : linkTableAnnotation.idColumn());
String subIdCol = "id";
DBBeanConfig subObjConfig = null;
for (DBBean subObj : list) {
// Save the sub bean
if (recursive || subObj.getId() == null)
subObj.save(db);
if (subObj.getId() == null) {
logger.severe("Unable to save field " + c.getSimpleName() + "." + field.getName() + " with " + subObj.getClass().getSimpleName());
continue;
}
// Get the Sub object configuration
if (subObjConfig == null) {
subObjConfig = DBBeanConfig.getBeanConfig(subObj.getClass());
subIdCol = subObjConfig.idColumn;
}
// Save links in link table
String sql;
if (linkTable.equals(subObjConfig.tableName))
sql = "UPDATE " + linkTable + " SET " + idCol + "=? WHERE " + subIdCol + "=?";
else
sql = "INSERT INTO " + linkTable + " (" + idCol + ", " + subIdCol + ") SELECT ?,? " +
"WHERE NOT EXISTS(SELECT 1 FROM " + linkTable + " WHERE " + idCol + "=? AND " + idCol + "=?);";
logger.finest("Save sub Bean(" + c.getName() + ", id: " + subObj.getId() + ") query: " + sql);
PreparedStatement subStmt = db.getPreparedStatement(sql);
subStmt.setLong(1, this.id);
subStmt.setLong(2, subObj.getId());
if (subStmt.getParameterMetaData().getParameterCount() > 2) {
subStmt.setLong(3, this.id);
subStmt.setLong(4, subObj.getId());
}
DBConnection.exec(subStmt);
}
}
}
}
} catch (SQLException e) {
throw e;
} catch (Exception e) {
throw new SQLException(e);
} finally {
saveLock.unlock();
} }
} else {
// Save sub beans, after we get the parent object id // If we have concurrent saves, only save once and skip the other threads
for(Field field : config.fields){ saveLock.lock();
if( List.class.isAssignableFrom( field.getType() ) && saveLock.unlock();
field.getAnnotation( DBLinkTable.class ) != null){ }
if (this.id == null)
throw new SQLException("Unknown parent object id");
List<DBBean> list = (List<DBBean>)getFieldValue(field);
if( list != null ){
DBLinkTable linkTableAnnotation = field.getAnnotation( DBLinkTable.class );
String linkTable = linkTableAnnotation.table();
String idCol = (linkTableAnnotation.idColumn().isEmpty() ? config.tableName : linkTableAnnotation.idColumn() );
String subIdCol = "id";
DBBeanConfig subObjConfig = null;
for(DBBean subObj : list){
// Save the sub bean
if( recursive || subObj.getId() == null )
subObj.save(db);
if( subObj.getId() == null ){
logger.severe("Unable to save field "+c.getSimpleName()+"."+field.getName()+" with "+subObj.getClass().getSimpleName());
continue;
}
// Get the Sub object configuration
if(subObjConfig == null){
subObjConfig = DBBeanConfig.getBeanConfig( subObj.getClass() );
subIdCol = subObjConfig.idColumn;
}
// Save links in link table
String sql;
if(linkTable.equals(subObjConfig.tableName))
sql = "UPDATE "+linkTable+" SET "+idCol+"=? WHERE "+subIdCol+"=?";
else // TODO: REPLACE will probably not work here
sql = "REPLACE INTO "+linkTable+" ("+idCol+", "+subIdCol+") VALUES(?, ?)";
logger.finest("Save sub Bean("+c.getName()+", id: "+subObj.getId()+") query: "+sql);
PreparedStatement subStmt = db.getPreparedStatement( sql );
subStmt.setLong(1, this.id );
subStmt.setLong(2, subObj.getId() );
DBConnection.exec(subStmt);
}
}
}
}
} catch (SQLException e) {
throw e;
} catch (Exception e) {
throw new SQLException(e);
} finally{
processing_save = false;
}
} }
/** /**
@ -290,7 +304,7 @@ public abstract class DBBean {
Class<? extends DBBean> c = this.getClass(); Class<? extends DBBean> c = this.getClass();
DBBeanConfig config = DBBeanConfig.getBeanConfig( c ); DBBeanConfig config = DBBeanConfig.getBeanConfig( c );
if( this.getId() == null ) if( this.getId() == null )
throw new NoSuchElementException("ID field is null? (Has the bean been saved?)"); throw new NullPointerException("ID field is null! (Has the bean been saved?)");
String sql = "DELETE FROM "+config.tableName+" WHERE "+config.idColumn+"=?"; String sql = "DELETE FROM "+config.tableName+" WHERE "+config.idColumn+"=?";
logger.finest("Delete Bean("+c.getName()+", id: "+this.getId()+") query: "+sql); logger.finest("Delete Bean("+c.getName()+", id: "+this.getId()+") query: "+sql);
@ -458,6 +472,10 @@ public abstract class DBBean {
return id; return id;
} }
final void setId(Long id){
this.id = id;
}
////////////////// EXTENDABLE METHODS ///////////////////////// ////////////////// EXTENDABLE METHODS /////////////////////////

View file

@ -0,0 +1,177 @@
package zutil.db.bean;
import zutil.log.LogUtil;
import java.lang.ref.WeakReference;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* This class contains a cache of all DBBeans that are allocated
*/
class DBBeanCache {
private static final Logger logger = LogUtil.getLogger();
/** This is the time to live for the cached items **/
public static final long CACHE_DATA_TTL = 1000*60*5; // 5 min in ms
/** A cache for detecting recursion **/
private static Map<Class<?>, Map<Long, CacheItem>> cache =
new ConcurrentHashMap<>();
private static Timer timer;
static {
enableBeanGBC(true); // Initiate DBBeanGarbageCollector
}
/**
* A cache container that contains a object and last read time
*/
private static class CacheItem{
public long updateTimestamp;
public WeakReference<DBBean> bean;
}
/**
* This function cancels the internal cache garbage collector in DBBean.
* GBC is enabled by default
*/
public static void enableBeanGBC(boolean enable){
if(enable){
if( timer == null ){
timer = new Timer( true ); // Run as daemon
timer.schedule( new DBBeanGarbageCollector(), CACHE_DATA_TTL, CACHE_DATA_TTL *2 );
logger.fine("Bean garbage collection daemon enabled");
}
}
else {
if (timer != null) {
timer.cancel();
timer = null;
logger.fine("Bean garbage collection daemon disabled");
}
}
}
/**
* This class acts as an garbage collector that removes old DBBeans
*/
private static class DBBeanGarbageCollector extends TimerTask {
public void run(){
if( cache == null ){
logger.severe("DBBeanSQLResultHandler not initialized, stopping DBBeanGarbageCollector timer.");
this.cancel();
return;
}
int removed = 0;
for(Object classKey : cache.keySet()){
if( classKey == null ) continue;
Map<Long, CacheItem> class_cache = cache.get(classKey);
for(Iterator<Map.Entry<Long, CacheItem>> it = class_cache.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Long, CacheItem> entry = it.next();
if( entry.getKey() == null ) continue;
// Check if session is still valid
if( entry.getValue().bean.get() == null ){
it.remove();
removed++;
}
}
}
if (removed > 0)
logger.info("DBBean GarbageCollector has cleared "+removed+" beans from cache.");
}
}
public static boolean contains(DBBean obj){
if (obj == null)
return false;
return contains(obj.getClass(), obj.getId());
}
public static boolean contains(Class<?> c, Long id){
if( cache.containsKey(c) ){
CacheItem cacheItem = cache.get(c).get(id);
// Check if the cache is valid
if( cacheItem != null && cacheItem.bean.get() != null ) {
return true;
} else {
// The bean has been deallocated
cache.get(c).remove(id);
}
}
return false;
}
/**
* Will check the cache if the given object exists
*
* @param c is the class of the bean
* @param id is the id of the bean
* @return a cached DBBean object, or null if there is no cached object or if the cache is to old
*/
public static DBBean get(Class<?> c, Long id){
try{
return get( c, id, null );
}catch(SQLException e){
throw new RuntimeException("This exception should not be thrown, Something went really wrong!", e);
}
}
/**
* Will check the cache if the given object exists and will update it if its old
*
* @param c is the class of the bean
* @param id is the id of the bean
* @param result is the ResultSet for this object, the object will be updated from this ResultSet if the object is to old, there will be no update if this parameter is null
* @return a cached DBBean object, might update the cached object if its old but only if the ResultSet parameter is set
*/
public static DBBean get(Class<?> c, Long id, ResultSet result) throws SQLException{
if(contains(c, id)){
CacheItem cacheItem = cache.get(c).get(id);
DBBean bean = cacheItem.bean.get();
// The cache is old, update and return it
if (cacheItem.updateTimestamp + CACHE_DATA_TTL < System.currentTimeMillis()) {
// There is no ResultSet to update from
if (result == null)
return null;
// Only update object if there is no update running now
logger.finer("Bean(" + c.getName() + ") cache to old for id: " + id);
// TODO:updateBean(result, bean);
}
return bean;
}
logger.finer("Bean("+c.getName()+") cache miss for id: "+id);
return null;
}
/**
* Will check if the object with the id already exists in the cahce,
* if not then it will add the given object to the cache.
*
* @param obj is the object to cache
*/
public synchronized static void add(DBBean obj) {
if (contains(obj))
return;
CacheItem cacheItem = new CacheItem();
cacheItem.updateTimestamp = System.currentTimeMillis();
cacheItem.bean = new WeakReference<>(obj);
if( cache.containsKey(obj.getClass()) )
cache.get(obj.getClass()).put(obj.getId(), cacheItem);
else{
Map<Long, CacheItem> map = new ConcurrentHashMap<>();
map.put(obj.getId(), cacheItem);
cache.put(obj.getClass(), map);
}
}
}

View file

@ -24,21 +24,17 @@
package zutil.db.bean; package zutil.db.bean;
import zutil.log.LogUtil;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.logging.Logger;
/** /**
* A Class that contains information about a bean * A Class that contains information about a bean
*/ */
class DBBeanConfig{ class DBBeanConfig{
private static final Logger logger = LogUtil.getLogger();
/** This is a cache of all the initialized beans */ /** This is a cache of all the initialized beans */
private static HashMap<String,DBBeanConfig> beanConfigs = new HashMap<String,DBBeanConfig>(); private static HashMap<String,DBBeanConfig> beanConfigs = new HashMap<>();
/** The name of the table in the DB **/ /** The name of the table in the DB **/
@ -48,8 +44,9 @@ class DBBeanConfig{
/** All the fields in the bean **/ /** All the fields in the bean **/
protected ArrayList<Field> fields; protected ArrayList<Field> fields;
protected DBBeanConfig(){
fields = new ArrayList<Field>(); private DBBeanConfig(){
fields = new ArrayList<>();
} }
@ -67,7 +64,6 @@ class DBBeanConfig{
* Caches the fields * Caches the fields
*/ */
private static void initBeanConfig(Class<? extends DBBean> c){ private static void initBeanConfig(Class<? extends DBBean> c){
//logger.fine("Initiating new BeanConfig( "+c.getName()+" )");
DBBeanConfig config = new DBBeanConfig(); DBBeanConfig config = new DBBeanConfig();
// Find the table name // Find the table name
DBBean.DBTable tableAnn = c.getAnnotation(DBBean.DBTable.class); DBBean.DBTable tableAnn = c.getAnnotation(DBBean.DBTable.class);
@ -102,12 +98,9 @@ class DBBeanConfig{
} }
protected static String getFieldName(Field field){ protected static String getFieldName(Field field){
String name = null;
DBBean.DBColumn colAnnotation = field.getAnnotation(DBBean.DBColumn.class); DBBean.DBColumn colAnnotation = field.getAnnotation(DBBean.DBColumn.class);
if(colAnnotation != null) if(colAnnotation != null)
name = colAnnotation.value(); return colAnnotation.value();
else return field.getName();
name = field.getName();
return name;
} }
} }

View file

@ -29,42 +29,25 @@ import zutil.db.SQLResultHandler;
import zutil.db.bean.DBBean.DBLinkTable; import zutil.db.bean.DBBean.DBLinkTable;
import zutil.log.LogUtil; import zutil.log.LogUtil;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger; import java.util.logging.Logger;
public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
private static final Logger logger = LogUtil.getLogger(); private static final Logger logger = LogUtil.getLogger();
/** This is the time to live for the cached items **/
public static final long CACHE_DATA_TTL = 1000*60*5; // 5 min in ms
/** A cache for detecting recursion **/
protected static Map<Class<?>, Map<Long,DBBeanCache>> cache =
new ConcurrentHashMap<>();
private static Timer timer;
static { private Class<? extends DBBean> beanClass;
enableBeanGBC(true); // Initiate DBBeanGarbageCollector private DBBeanConfig beanConfig;
}
/**
* A cache container that contains a object and last read time
*/
private static class DBBeanCache{
public long updateTimestamp;
public WeakReference<DBBean> bean;
}
private Class<? extends DBBean> bean_class;
private DBBeanConfig bean_config;
private DBConnection db; private DBConnection db;
private boolean list; private boolean list;
/** /**
* Creates a new instance of this class that returns only one bean * Creates a new instance of this class that returns only one bean
* *
@ -83,7 +66,7 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
* @return a new instance of this class * @return a new instance of this class
*/ */
public static <C extends DBBean> DBBeanSQLResultHandler<C> create(Class<C> cl, DBConnection db){ public static <C extends DBBean> DBBeanSQLResultHandler<C> create(Class<C> cl, DBConnection db){
return new DBBeanSQLResultHandler<C>(cl, db, false); return new DBBeanSQLResultHandler<>(cl, db, false);
} }
/** /**
@ -93,7 +76,7 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
* @return a new instance of this class * @return a new instance of this class
*/ */
public static <C extends DBBean> DBBeanSQLResultHandler<List<C>> createList(Class<C> cl){ public static <C extends DBBean> DBBeanSQLResultHandler<List<C>> createList(Class<C> cl){
return new DBBeanSQLResultHandler<List<C>>(cl, null, true); return new DBBeanSQLResultHandler<>(cl, null, true);
} }
/** /**
@ -104,9 +87,10 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
* @return a new instance of this class * @return a new instance of this class
*/ */
public static <C extends DBBean> DBBeanSQLResultHandler<List<C>> createList(Class<C> cl, DBConnection db){ public static <C extends DBBean> DBBeanSQLResultHandler<List<C>> createList(Class<C> cl, DBConnection db){
return new DBBeanSQLResultHandler<List<C>>(cl, db, true); return new DBBeanSQLResultHandler<>(cl, db, true);
} }
/** /**
* Creates a new instance of this class * Creates a new instance of this class
* *
@ -115,67 +99,12 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
* @param list is if the handler should return a list of beans instead of one * @param list is if the handler should return a list of beans instead of one
*/ */
protected DBBeanSQLResultHandler(Class<? extends DBBean> cl, DBConnection db, boolean list) { protected DBBeanSQLResultHandler(Class<? extends DBBean> cl, DBConnection db, boolean list) {
this.bean_class = cl; this.beanClass = cl;
this.list = list; this.list = list;
this.db = db; this.db = db;
this.bean_config = DBBeanConfig.getBeanConfig( cl ); this.beanConfig = DBBeanConfig.getBeanConfig( cl );
} }
/**
* This function cancels the internal cache garbage collector in DBBean.
* GBC is enabled by default
*/
private static void enableBeanGBC(boolean enable){
if(enable){
if( timer == null ){
timer = new Timer( true ); // Run as daemon
timer.schedule( new DBBeanGarbageCollector(), CACHE_DATA_TTL, CACHE_DATA_TTL *2 );
logger.fine("Bean garbage collection daemon enabled");
}
}
else {
if (timer != null) {
timer.cancel();
timer = null;
logger.fine("Bean garbage collection daemon disabled");
}
}
}
/**
* This class acts as an garbage collector that removes old DBBeans
*
* @author Ziver
*/
private static class DBBeanGarbageCollector extends TimerTask {
public void run(){
if( cache == null ){
logger.severe("DBBeanSQLResultHandler not initialized, stopping DBBeanGarbageCollector timer.");
this.cancel();
return;
}
int removed = 0;
for(Object classKey : cache.keySet()){
if( classKey == null ) continue;
Map<Long,DBBeanCache> class_cache = cache.get(classKey);
for(Iterator<Map.Entry<Long, DBBeanCache>> it = class_cache.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Long, DBBeanCache> entry = it.next();
if( entry.getKey() == null ) continue;
// Check if session is still valid
if( entry.getValue().bean.get() == null ){
it.remove();
removed++;
}
}
}
if (removed > 0)
logger.info("DBBean GarbageCollector has cleared "+removed+" beans from cache.");
}
}
/** /**
* Is called to handle a result from a query. * Is called to handle a result from a query.
@ -212,13 +141,15 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
protected DBBean createBean(ResultSet result) throws SQLException{ protected DBBean createBean(ResultSet result) throws SQLException{
try { try {
Long id = result.getLong( "id" ); Long id = result.getLong( "id" );
DBBean obj = getCachedDBBean(bean_class, id, result); // Check cache first
if( obj != null ) DBBean obj = DBBeanCache.get(beanClass, id, result);
return obj; if ( obj == null ) {
logger.fine("Creating new Bean("+bean_class.getName()+") with id: "+id); // Cache miss create a new object
obj = bean_class.newInstance(); logger.fine("Creating new Bean(" + beanClass.getName() + ") with id: " + id);
obj.id = id; // Set id field obj = beanClass.newInstance();
cacheDBBean(obj); obj.setId(id);
DBBeanCache.add(obj);
}
// Update fields // Update fields
updateBean( result, obj ); updateBean( result, obj );
@ -238,125 +169,60 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected void updateBean(ResultSet result, DBBean obj) throws SQLException{ protected void updateBean(ResultSet result, DBBean obj) throws SQLException{
try { if (obj.readLock.tryLock()) {
logger.fine("Updating Bean("+bean_class.getName()+") with id: "+obj.id); try {
obj.processing_update = true; logger.fine("Updating Bean(" + beanClass.getName() + ") with id: " + obj.getId());
// Get the rest // Get the rest
for( Field field : bean_config.fields ){ for (Field field : beanConfig.fields) {
String name = DBBeanConfig.getFieldName(field); String name = DBBeanConfig.getFieldName(field);
// Another DBBean class // Another DBBean class
if( DBBean.class.isAssignableFrom( field.getType() )){ if (DBBean.class.isAssignableFrom(field.getType())) {
if(db != null){ if (db != null) {
Long subid = result.getLong( name ); Long subid = result.getLong(name);
DBBean subobj = getCachedDBBean( field.getType(), subid ); DBBean subobj = DBBeanCache.get(field.getType(), subid);
if( subobj == null ) if (subobj == null)
subobj = DBBean.load(db, (Class<? extends DBBean>)field.getType(), subid); subobj = DBBean.load(db, (Class<? extends DBBean>) field.getType(), subid);
obj.setFieldValue(field, subobj); obj.setFieldValue(field, subobj);
} } else
else logger.warning("No DB available to read sub beans");
logger.warning("No DB available to read sub beans"); }
} // A list of DBBeans
// A list of DBBeans else if (List.class.isAssignableFrom(field.getType()) &&
else if( List.class.isAssignableFrom( field.getType() ) && field.getAnnotation(DBLinkTable.class) != null) {
field.getAnnotation( DBLinkTable.class ) != null){ if (db != null) {
if(db != null){ DBLinkTable linkTable = field.getAnnotation(DBLinkTable.class);
DBLinkTable linkTable = field.getAnnotation( DBLinkTable.class ); DBBeanConfig subConfig = DBBeanConfig.getBeanConfig(linkTable.beanClass());
DBBeanConfig subConfig = DBBeanConfig.getBeanConfig( linkTable.beanClass() ); String linkTableName = linkTable.table();
String linkTableName = linkTable.table(); String subTable = subConfig.tableName;
String subTable = subConfig.tableName; String idcol = (linkTable.idColumn().isEmpty() ? beanConfig.tableName : linkTable.idColumn());
String idcol = (linkTable.idColumn().isEmpty() ? bean_config.tableName : linkTable.idColumn() );
// Load list from link table
// Load list from link table String subsql = "SELECT subObjTable.* FROM " + linkTableName + " as linkTable, " + subTable + " as subObjTable WHERE linkTable." + idcol + "=? AND linkTable." + subConfig.idColumn + "=subObjTable." + subConfig.idColumn;
String subsql = "SELECT subObjTable.* FROM "+linkTableName+" as linkTable, "+subTable+" as subObjTable WHERE linkTable."+idcol+"=? AND linkTable."+subConfig.idColumn+"=subObjTable."+subConfig.idColumn; logger.finest("List Load Query: " + subsql);
logger.finest("List Load Query: "+subsql); PreparedStatement subStmt = db.getPreparedStatement(subsql);
PreparedStatement subStmt = db.getPreparedStatement( subsql ); subStmt.setObject(1, obj.getId());
subStmt.setObject(1, obj.getId() ); List<? extends DBBean> list = DBConnection.exec(subStmt,
List<? extends DBBean> list = DBConnection.exec(subStmt, DBBeanSQLResultHandler.createList(linkTable.beanClass(), db));
DBBeanSQLResultHandler.createList(linkTable.beanClass(), db)); obj.setFieldValue(field, list);
obj.setFieldValue(field, list); } else
} logger.warning("No DB available to read sub beans");
else }
logger.warning("No DB available to read sub beans"); // Normal field
} else {
// Normal field obj.setFieldValue(field, result.getObject(name));
else
obj.setFieldValue(field, result.getObject(name));
}
} finally{
obj.processing_update = false;
}
obj.postUpdateAction();
}
/**
* Will check the cache if the given object exists
*
* @param c is the class of the bean
* @param id is the id of the bean
* @return a cached DBBean object, or null if there is no cached object or if the cache is to old
*/
protected DBBean getCachedDBBean(Class<?> c, Long id){
try{
return getCachedDBBean( c, id, null );
}catch(SQLException e){
throw new RuntimeException("This exception should not be thrown, Something ent ready wrong!", e);
}
}
/**
* Will check the cache if the given object exists and will update it if its old
*
* @param c is the class of the bean
* @param id is the id of the bean
* @param result is the ResultSet for this object, the object will be updated from this ResultSet if the object is to old, there will be no update if this parameter is null
* @return a cached DBBean object, might update the cached object if its old but only if the ResultSet parameter is set
*/
protected DBBean getCachedDBBean(Class<?> c, Long id, ResultSet result) throws SQLException{
if( cache.containsKey(c) ){
DBBeanCache cacheItem = cache.get(c).get(id);
// Check if the cache is valid
if( cacheItem != null){
DBBean bean = cacheItem.bean.get();
if( bean != null) {
// The cache is old, update and return it
if (cacheItem.updateTimestamp + CACHE_DATA_TTL < System.currentTimeMillis()) {
// There is no ResultSet to update from
if (result == null)
return null;
// Only update object if there is no update running now
if (!bean.processing_update) {
logger.finer("Bean(" + c.getName() + ") cache to old for id: " + id);
updateBean(result, bean);
}
} }
return bean;
} }
}
// The cache is null obj.postUpdateAction();
cache.get(c).remove(id); } finally {
} obj.readLock.unlock();
logger.finer("Bean("+c.getName()+") cache miss for id: "+id); }
return null; } else {
obj.readLock.lock();
obj.readLock.unlock();
}
} }
/**
* Adds the given object to the cache
*
* @param obj is the object to cache
*/
protected static synchronized void cacheDBBean(DBBean obj) {
DBBeanCache cacheItem = new DBBeanCache();
cacheItem.updateTimestamp = System.currentTimeMillis();
cacheItem.bean = new WeakReference<DBBean>(obj);
if( cache.containsKey(obj.getClass()) )
cache.get(obj.getClass()).put(obj.getId(), cacheItem);
else{
Map<Long, DBBeanCache> map = new ConcurrentHashMap<Long, DBBeanCache>();
map.put(obj.getId(), cacheItem);
cache.put(obj.getClass(), map);
}
}
} }

View file

@ -0,0 +1,54 @@
package zutil.db.bean;
import org.junit.BeforeClass;
import org.junit.Test;
import zutil.db.DBConnection;
import zutil.log.CompactLogFormatter;
import zutil.log.LogUtil;
import java.sql.SQLException;
import java.util.logging.Level;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static zutil.db.bean.DBBeanTestBase.*;
/**
*
*/
public class DBBeanLoadTest {
private DBConnection db = new DBConnection(DBConnection.DBMS.SQLite, ":memory:");
public DBBeanLoadTest() throws Exception {}
@BeforeClass
public static void init(){
LogUtil.setGlobalFormatter(new CompactLogFormatter());
LogUtil.setGlobalLevel(Level.ALL);
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@Test
public void simpleClassLoad() throws SQLException {
simpleClassInit(db);
insert(db, "SimpleTestClass", "5", "1234", "\"helloworld\"");
SimpleTestClass obj = DBBean.load(db, SimpleTestClass.class, 5);
assertEquals((Long)5L, obj.getId());
assertEquals(1234, obj.intField);
assertEquals("helloworld", obj.strField);
}
@Test
public void simpleClassCache() throws SQLException {
simpleClassInit(db);
insert(db, "SimpleTestClass", "5", "1234", "\"helloworld\"");
SimpleTestClass obj1 = DBBean.load(db, SimpleTestClass.class, 5);
SimpleTestClass obj2 = DBBean.load(db, SimpleTestClass.class, 5);
assertSame(obj1, obj2);
}
}

View file

@ -3,16 +3,15 @@ package zutil.db.bean;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import zutil.db.DBConnection; import zutil.db.DBConnection;
import zutil.db.handler.SimpleSQLResult; import zutil.db.bean.DBBeanTestBase.*;
import zutil.log.CompactLogFormatter; import zutil.log.CompactLogFormatter;
import zutil.log.LogUtil; import zutil.log.LogUtil;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static zutil.db.bean.DBBeanTestBase.*;
/** /**
* *
@ -31,21 +30,9 @@ public class DBBeanSaveTest {
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
private static class SimpleTestClass extends DBBean{
int intField;
String strField;
}
@Test @Test
public void simpleClassCreate() throws SQLException { public void simpleClassCreate() throws SQLException {
db.exec("CREATE TABLE SimpleTestClass (" + SimpleTestClass obj = simpleClassInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"intField INTEGER, " +
"strField TEXT);");
SimpleTestClass obj = new SimpleTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
obj.save(db); obj.save(db);
assertEquals(1234, assertEquals(1234,
@ -56,14 +43,7 @@ public class DBBeanSaveTest {
@Test @Test
public void simpleClassUpdate() throws SQLException { public void simpleClassUpdate() throws SQLException {
db.exec("CREATE TABLE SimpleTestClass (" + SimpleTestClass obj = simpleClassInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"intField INTEGER, " +
"strField TEXT);");
SimpleTestClass obj = new SimpleTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
obj.save(db); obj.save(db);
obj.intField = 1337; obj.intField = 1337;
obj.strField = "monkey"; obj.strField = "monkey";
@ -78,24 +58,9 @@ public class DBBeanSaveTest {
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("aliasTable")
private static class AliasFieldsTestClass extends DBBean{
@DBColumn("aliasIntField")
int intField;
@DBColumn("aliasStrField")
String strField;
}
@Test @Test
public void aliasFieldsCreate() throws SQLException { public void aliasFieldsCreate() throws SQLException {
db.exec("CREATE TABLE aliasTable (" + AliasFieldsTestClass obj = aliasFieldsInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"aliasIntField INTEGER, " +
"aliasStrField TEXT);");
AliasFieldsTestClass obj = new AliasFieldsTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
obj.save(db); obj.save(db);
assertEquals(1234, assertEquals(1234,
@ -106,14 +71,7 @@ public class DBBeanSaveTest {
@Test @Test
public void aliasFieldsUpdate() throws SQLException { public void aliasFieldsUpdate() throws SQLException {
db.exec("CREATE TABLE aliasTable (" + AliasFieldsTestClass obj = aliasFieldsInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"aliasIntField INTEGER, " +
"aliasStrField TEXT);");
AliasFieldsTestClass obj = new AliasFieldsTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
obj.save(db); obj.save(db);
obj.intField = 1337; obj.intField = 1337;
obj.strField = "monkey"; obj.strField = "monkey";
@ -128,31 +86,25 @@ public class DBBeanSaveTest {
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent") @Test
private static class ParentTestClass extends DBBean{ public void subObjectCreate() throws SQLException {
@DBLinkTable(table = "subobject", idColumn = "parent_id",beanClass = SubObjectTestClass.class) ParentTestClass obj = subObjectInit(db);
List<SubObjectTestClass> subobjs = new ArrayList<>(); obj.save(db);
}
@DBBean.DBTable("subobject") assertEquals(1234,
private static class SubObjectTestClass extends DBBean{ getColumnValue(db, "subobject", "intField"));
int intField; assertEquals(1,
getColumnValue(db, "subobject", "parent_id"));
} }
@Test @Test
public void subObjectCreate() throws SQLException { public void subObjectUpdate() throws SQLException {
db.exec("CREATE TABLE parent (" + ParentTestClass obj = subObjectInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT);"); obj.save(db);
db.exec("CREATE TABLE subobject (" + obj.subobjs.get(0).intField = 1337;
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"parent_id INTEGER, " +
"intField INTEGER);");
ParentTestClass obj = new ParentTestClass();
SubObjectTestClass subObj = new SubObjectTestClass();
subObj.intField = 1337;
obj.subobjs.add(subObj);
obj.save(db); obj.save(db);
assertEquals("Check for duplicates",1, getRowCount(db, "subobject"));
assertEquals(1337, assertEquals(1337,
getColumnValue(db, "subobject", "intField")); getColumnValue(db, "subobject", "intField"));
assertEquals(1, assertEquals(1,
@ -162,30 +114,28 @@ public class DBBeanSaveTest {
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent") @Test
private static class ParentLinkTestClass extends DBBean{ public void subLinkObjectCreate() throws SQLException {
@DBLinkTable(table = "link", idColumn = "parent_id",beanClass = SubObjectTestClass.class) ParentLinkTestClass obj = subLinkObjectInit(db);
List<SubObjectTestClass> subobjs = new ArrayList<>(); obj.save(db);
assertEquals(1,
getColumnValue(db, "link", "parent_id"));
assertEquals(1,
getColumnValue(db, "link", "id"));
assertEquals(1234,
getColumnValue(db, "subobject", "intField"));
} }
@Test @Test
public void subLinkObjectCreate() throws SQLException { public void subLinkObjectUpdate() throws SQLException {
db.exec("CREATE TABLE parent (" + ParentLinkTestClass obj = subLinkObjectInit(db);
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT);"); obj.save(db);
db.exec("CREATE TABLE link (" + obj.subobjs.get(0).intField = 1337;
"parent_id INTEGER, " +
"id INTEGER);");
db.exec("CREATE TABLE subobject (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"parent_id INTEGER, " +
"intField INTEGER);");
ParentLinkTestClass obj = new ParentLinkTestClass();
SubObjectTestClass subObj = new SubObjectTestClass();
subObj.intField = 1337;
obj.subobjs.add(subObj);
obj.save(db); obj.save(db);
assertEquals("Check for duplicates",1, getRowCount(db, "subobject"));
assertEquals("Check for duplicate links",1, getRowCount(db, "link"));
assertEquals(1, assertEquals(1,
getColumnValue(db, "link", "parent_id")); getColumnValue(db, "link", "parent_id"));
assertEquals(1, assertEquals(1,
@ -194,10 +144,4 @@ public class DBBeanSaveTest {
getColumnValue(db, "subobject", "intField")); getColumnValue(db, "subobject", "intField"));
} }
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
public static Object getColumnValue(DBConnection db, String table, String column) throws SQLException {
return db.exec("SELECT "+column+" FROM "+table, new SimpleSQLResult<>());
}
} }

View file

@ -0,0 +1,128 @@
package zutil.db.bean;
import zutil.StringUtil;
import zutil.db.DBConnection;
import zutil.db.handler.SimpleSQLResult;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
*
*/
public class DBBeanTestBase {
public static class SimpleTestClass extends DBBean{
int intField;
String strField;
}
public static SimpleTestClass simpleClassInit(DBConnection db) throws SQLException {
db.exec("CREATE TABLE SimpleTestClass (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"intField INTEGER, " +
"strField TEXT);");
SimpleTestClass obj = new SimpleTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
return obj;
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("aliasTable")
public static class AliasFieldsTestClass extends DBBean{
@DBColumn("aliasIntField")
int intField;
@DBColumn("aliasStrField")
String strField;
}
public static AliasFieldsTestClass aliasFieldsInit(DBConnection db) throws SQLException {
db.exec("CREATE TABLE aliasTable (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"aliasIntField INTEGER, " +
"aliasStrField TEXT);");
AliasFieldsTestClass obj = new AliasFieldsTestClass();
obj.intField = 1234;
obj.strField = "helloworld";
return obj;
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent")
public static class ParentTestClass extends DBBean{
@DBLinkTable(table = "subobject", idColumn = "parent_id",beanClass = SubObjectTestClass.class)
List<SubObjectTestClass> subobjs = new ArrayList<>();
}
@DBBean.DBTable("subobject")
public static class SubObjectTestClass extends DBBean{
int intField;
}
public static ParentTestClass subObjectInit(DBConnection db) throws SQLException {
db.exec("CREATE TABLE parent (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT);");
db.exec("CREATE TABLE subobject (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"parent_id INTEGER, " +
"intField INTEGER);");
ParentTestClass obj = new ParentTestClass();
SubObjectTestClass subObj = new SubObjectTestClass();
subObj.intField = 1234;
obj.subobjs.add(subObj);
return obj;
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent")
public static class ParentLinkTestClass extends DBBean{
@DBLinkTable(table = "link", idColumn = "parent_id",beanClass = SubObjectTestClass.class)
List<SubObjectTestClass> subobjs = new ArrayList<>();
}
public static ParentLinkTestClass subLinkObjectInit(DBConnection db) throws SQLException {
db.exec("CREATE TABLE parent (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT);");
db.exec("CREATE TABLE link (" +
"parent_id INTEGER, " +
"id INTEGER);");
db.exec("CREATE TABLE subobject (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
"parent_id INTEGER, " +
"intField INTEGER);");
ParentLinkTestClass obj = new ParentLinkTestClass();
SubObjectTestClass subObj = new SubObjectTestClass();
subObj.intField = 1234;
obj.subobjs.add(subObj);
return obj;
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
public static Object getColumnValue(DBConnection db, String table, String column) throws SQLException {
return db.exec("SELECT "+column+" FROM "+table, new SimpleSQLResult<>());
}
public static Object getRowCount(DBConnection db, String table) throws SQLException {
return db.exec("SELECT count(*) FROM "+table, new SimpleSQLResult<>());
}
public static void insert(DBConnection db, String table, String... values) throws SQLException {
db.exec("INSERT INTO "+table+" VALUES("+
StringUtil.join(",", values)+");");
}
}