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.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -67,9 +68,6 @@ import java.util.logging.Logger;
public abstract class DBBean {
private static final Logger logger = LogUtil.getLogger();
/** The id of the bean **/
protected Long id;
/**
* 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;
/** This value is for preventing recursive loops when updating */
protected boolean processing_update;
/** 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 */
protected ReentrantLock readLock;
protected DBBean(){
DBBeanConfig.getBeanConfig(this.getClass());
processing_save = false;
processing_update = false;
saveLock = new ReentrantLock();
readLock = new ReentrantLock();
}
/**
* 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
*
* @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")
public void save(DBConnection db, boolean recursive) throws SQLException{
if( processing_save )
return;
processing_save = true;
Class<? extends DBBean> c = this.getClass();
DBBeanConfig config = DBBeanConfig.getBeanConfig( c );
try {
// Generate the SQL
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 (saveLock.isHeldByCurrentThread()) // If the current thread already has a lock then this
return; // is a recursive call and we do not need to do anything
else if (saveLock.tryLock()) {
Class<? extends DBBean> c = this.getClass();
DBBeanConfig config = DBBeanConfig.getBeanConfig(c);
try {
// Generate the SQL
StringBuilder query = new StringBuilder();
if (this.id == null) {
this.id = db.getLastInsertID(stmt);
// as this is a new object so add it to the cache
DBBeanSQLResultHandler.cacheDBBean(this);
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) { // 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();
}
// 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 // 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;
}
} else {
// If we have concurrent saves, only save once and skip the other threads
saveLock.lock();
saveLock.unlock();
}
}
/**
@ -290,7 +304,7 @@ public abstract class DBBean {
Class<? extends DBBean> c = this.getClass();
DBBeanConfig config = DBBeanConfig.getBeanConfig( c );
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+"=?";
logger.finest("Delete Bean("+c.getName()+", id: "+this.getId()+") query: "+sql);
@ -457,6 +471,10 @@ public abstract class DBBean {
public final Long getId(){
return id;
}
final void setId(Long id){
this.id = id;
}

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

View file

@ -29,42 +29,25 @@ import zutil.db.SQLResultHandler;
import zutil.db.bean.DBBean.DBLinkTable;
import zutil.log.LogUtil;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
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 {
enableBeanGBC(true); // Initiate DBBeanGarbageCollector
}
/**
* 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 Class<? extends DBBean> beanClass;
private DBBeanConfig beanConfig;
private DBConnection db;
private boolean list;
/**
* 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
*/
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
*/
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
*/
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
*
@ -115,68 +99,13 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
* @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) {
this.bean_class = cl;
this.beanClass = cl;
this.list = list;
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.
*
@ -212,13 +141,15 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
protected DBBean createBean(ResultSet result) throws SQLException{
try {
Long id = result.getLong( "id" );
DBBean obj = getCachedDBBean(bean_class, id, result);
if( obj != null )
return obj;
logger.fine("Creating new Bean("+bean_class.getName()+") with id: "+id);
obj = bean_class.newInstance();
obj.id = id; // Set id field
cacheDBBean(obj);
// Check cache first
DBBean obj = DBBeanCache.get(beanClass, id, result);
if ( obj == null ) {
// Cache miss create a new object
logger.fine("Creating new Bean(" + beanClass.getName() + ") with id: " + id);
obj = beanClass.newInstance();
obj.setId(id);
DBBeanCache.add(obj);
}
// Update fields
updateBean( result, obj );
@ -238,125 +169,60 @@ public class DBBeanSQLResultHandler<T> implements SQLResultHandler<T>{
*/
@SuppressWarnings("unchecked")
protected void updateBean(ResultSet result, DBBean obj) throws SQLException{
try {
logger.fine("Updating Bean("+bean_class.getName()+") with id: "+obj.id);
obj.processing_update = true;
// Get the rest
for( Field field : bean_config.fields ){
String name = DBBeanConfig.getFieldName(field);
if (obj.readLock.tryLock()) {
try {
logger.fine("Updating Bean(" + beanClass.getName() + ") with id: " + obj.getId());
// Get the rest
for (Field field : beanConfig.fields) {
String name = DBBeanConfig.getFieldName(field);
// Another DBBean class
if( DBBean.class.isAssignableFrom( field.getType() )){
if(db != null){
Long subid = result.getLong( name );
DBBean subobj = getCachedDBBean( field.getType(), subid );
if( subobj == null )
subobj = DBBean.load(db, (Class<? extends DBBean>)field.getType(), subid);
obj.setFieldValue(field, subobj);
}
else
logger.warning("No DB available to read sub beans");
}
// A list of DBBeans
else if( List.class.isAssignableFrom( field.getType() ) &&
field.getAnnotation( DBLinkTable.class ) != null){
if(db != null){
DBLinkTable linkTable = field.getAnnotation( DBLinkTable.class );
DBBeanConfig subConfig = DBBeanConfig.getBeanConfig( linkTable.beanClass() );
String linkTableName = linkTable.table();
String subTable = subConfig.tableName;
String idcol = (linkTable.idColumn().isEmpty() ? bean_config.tableName : linkTable.idColumn() );
// 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;
logger.finest("List Load Query: "+subsql);
PreparedStatement subStmt = db.getPreparedStatement( subsql );
subStmt.setObject(1, obj.getId() );
List<? extends DBBean> list = DBConnection.exec(subStmt,
DBBeanSQLResultHandler.createList(linkTable.beanClass(), db));
obj.setFieldValue(field, list);
}
else
logger.warning("No DB available to read sub beans");
}
// Normal field
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);
}
// Another DBBean class
if (DBBean.class.isAssignableFrom(field.getType())) {
if (db != null) {
Long subid = result.getLong(name);
DBBean subobj = DBBeanCache.get(field.getType(), subid);
if (subobj == null)
subobj = DBBean.load(db, (Class<? extends DBBean>) field.getType(), subid);
obj.setFieldValue(field, subobj);
} else
logger.warning("No DB available to read sub beans");
}
// A list of DBBeans
else if (List.class.isAssignableFrom(field.getType()) &&
field.getAnnotation(DBLinkTable.class) != null) {
if (db != null) {
DBLinkTable linkTable = field.getAnnotation(DBLinkTable.class);
DBBeanConfig subConfig = DBBeanConfig.getBeanConfig(linkTable.beanClass());
String linkTableName = linkTable.table();
String subTable = subConfig.tableName;
String idcol = (linkTable.idColumn().isEmpty() ? beanConfig.tableName : linkTable.idColumn());
// 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;
logger.finest("List Load Query: " + subsql);
PreparedStatement subStmt = db.getPreparedStatement(subsql);
subStmt.setObject(1, obj.getId());
List<? extends DBBean> list = DBConnection.exec(subStmt,
DBBeanSQLResultHandler.createList(linkTable.beanClass(), db));
obj.setFieldValue(field, list);
} else
logger.warning("No DB available to read sub beans");
}
// Normal field
else {
obj.setFieldValue(field, result.getObject(name));
}
return bean;
}
}
// The cache is null
cache.get(c).remove(id);
}
logger.finer("Bean("+c.getName()+") cache miss for id: "+id);
return null;
}
/**
* 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);
}
obj.postUpdateAction();
} finally {
obj.readLock.unlock();
}
} else {
obj.readLock.lock();
obj.readLock.unlock();
}
}
}

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.Test;
import zutil.db.DBConnection;
import zutil.db.handler.SimpleSQLResult;
import zutil.db.bean.DBBeanTestBase.*;
import zutil.log.CompactLogFormatter;
import zutil.log.LogUtil;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
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
public void simpleClassCreate() 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";
SimpleTestClass obj = simpleClassInit(db);
obj.save(db);
assertEquals(1234,
@ -56,14 +43,7 @@ public class DBBeanSaveTest {
@Test
public void simpleClassUpdate() 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";
SimpleTestClass obj = simpleClassInit(db);
obj.save(db);
obj.intField = 1337;
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
public void aliasFieldsCreate() 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";
AliasFieldsTestClass obj = aliasFieldsInit(db);
obj.save(db);
assertEquals(1234,
@ -106,14 +71,7 @@ public class DBBeanSaveTest {
@Test
public void aliasFieldsUpdate() 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";
AliasFieldsTestClass obj = aliasFieldsInit(db);
obj.save(db);
obj.intField = 1337;
obj.strField = "monkey";
@ -128,31 +86,25 @@ public class DBBeanSaveTest {
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent")
private static class ParentTestClass extends DBBean{
@DBLinkTable(table = "subobject", idColumn = "parent_id",beanClass = SubObjectTestClass.class)
List<SubObjectTestClass> subobjs = new ArrayList<>();
}
@DBBean.DBTable("subobject")
private static class SubObjectTestClass extends DBBean{
int intField;
@Test
public void subObjectCreate() throws SQLException {
ParentTestClass obj = subObjectInit(db);
obj.save(db);
assertEquals(1234,
getColumnValue(db, "subobject", "intField"));
assertEquals(1,
getColumnValue(db, "subobject", "parent_id"));
}
@Test
public void subObjectCreate() 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 = 1337;
obj.subobjs.add(subObj);
public void subObjectUpdate() throws SQLException {
ParentTestClass obj = subObjectInit(db);
obj.save(db);
obj.subobjs.get(0).intField = 1337;
obj.save(db);
assertEquals("Check for duplicates",1, getRowCount(db, "subobject"));
assertEquals(1337,
getColumnValue(db, "subobject", "intField"));
assertEquals(1,
@ -162,30 +114,28 @@ public class DBBeanSaveTest {
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
@DBBean.DBTable("parent")
private static class ParentLinkTestClass extends DBBean{
@DBLinkTable(table = "link", idColumn = "parent_id",beanClass = SubObjectTestClass.class)
List<SubObjectTestClass> subobjs = new ArrayList<>();
@Test
public void subLinkObjectCreate() throws SQLException {
ParentLinkTestClass obj = subLinkObjectInit(db);
obj.save(db);
assertEquals(1,
getColumnValue(db, "link", "parent_id"));
assertEquals(1,
getColumnValue(db, "link", "id"));
assertEquals(1234,
getColumnValue(db, "subobject", "intField"));
}
@Test
public void subLinkObjectCreate() 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 = 1337;
obj.subobjs.add(subObj);
public void subLinkObjectUpdate() throws SQLException {
ParentLinkTestClass obj = subLinkObjectInit(db);
obj.save(db);
obj.subobjs.get(0).intField = 1337;
obj.save(db);
assertEquals("Check for duplicates",1, getRowCount(db, "subobject"));
assertEquals("Check for duplicate links",1, getRowCount(db, "link"));
assertEquals(1,
getColumnValue(db, "link", "parent_id"));
assertEquals(1,
@ -194,10 +144,4 @@ public class DBBeanSaveTest {
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)+");");
}
}