Created
March 8, 2013 07:04
-
-
Save cemo/5114715 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ecc.dashboard.persistent.repository; | |
import java.lang.reflect.Field; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Date; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import javax.persistence.Column; | |
import javax.persistence.Id; | |
import javax.persistence.Table; | |
import javax.persistence.Transient; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.jdbc.core.JdbcTemplate; | |
import org.springframework.jdbc.core.RowMapper; | |
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | |
import org.springframework.transaction.annotation.Isolation; | |
import org.springframework.transaction.annotation.Transactional; | |
import org.springframework.util.CollectionUtils; | |
import org.springframework.util.ReflectionUtils; | |
import com.google.common.base.Function; | |
import com.google.common.base.Joiner; | |
import com.google.common.base.Preconditions; | |
import com.google.common.collect.Lists; | |
import com.google.common.collect.Maps; | |
import com.dvilla.database.util.DateToDBFormat; | |
import com.dvilla.utils.Pair; | |
public abstract class DatabaseRepository<T, ID> implements Repository<T, ID> { | |
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseRepository.class); | |
@Autowired | |
protected JdbcTemplate jdbcTemplate; | |
/** | |
* As TypeEnrase in java, there is no way to obtain the generic type in usual way, so the 'type' property is used to | |
* make the generic type class accessible from the generic class. | |
* | |
* Every subclass should initial the class in their constructor, to make sure the contract doesn't get broken. | |
*/ | |
protected Class<T> type; | |
protected Class<? extends RowMapper<T>> rowMapper; | |
protected String tableName; | |
protected Field idField; | |
protected String idColName; | |
protected Map<String, Pair<String, Field>> fieldNameIdx; | |
/** | |
* Constructor. | |
* | |
* @param type | |
* the class<T> for generic class | |
*/ | |
public DatabaseRepository(Class<T> type, Class<ID> idType, Class<? extends RowMapper<T>> rowMapper) { | |
this.type = type; | |
this.rowMapper = rowMapper; | |
this.fieldNameIdx = Maps.newHashMap(); | |
Table table = type.getAnnotation(Table.class); | |
if (table != null) { | |
this.tableName = table.name(); | |
} | |
// ReflectionUtils.doWithFields will iterate all field includes ones in superclass | |
ReflectionUtils.doWithFields(type, new ReflectionUtils.FieldCallback() { | |
@Override | |
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { | |
// ignore transient annotated field | |
if (field.getAnnotation(Transient.class) != null) return; | |
String fieldName = field.getName(); | |
LOGGER.debug("Field name=>{}, type=>{}", fieldName, field.getType().getSimpleName()); | |
ReflectionUtils.makeAccessible(field); | |
// find id key | |
if (field.getAnnotation(Id.class) != null) { | |
idField = field; | |
} | |
Column col = field.getAnnotation(Column.class); | |
if (col == null) { | |
LOGGER.warn("No Column Annotation on field [{}]", fieldName); | |
fieldNameIdx.put(fieldName, Pair.of(fieldName, field)); | |
} else { | |
fieldNameIdx.put(fieldName, Pair.of(col.name(), field)); | |
} | |
} | |
}); | |
// Type T must have table set | |
Preconditions.checkNotNull(tableName, "tableName is not defined in class %s", type.getClass()); | |
// Type T must have id key | |
Preconditions.checkNotNull(idType, "ID field not found in class %s", type.getClass()); | |
// Set id col name | |
idColName = fieldNameIdx.get(idField.getName()).first(); | |
} | |
/** | |
* Find all entities in database. | |
* | |
* @return a list of entities | |
*/ | |
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED) | |
public List<T> findAll() { | |
try { | |
return jdbcTemplate.query("select * from " + tableName, rowMapper.newInstance()); | |
} catch (InstantiationException e) { | |
LOGGER.error("Can't generate RowMapper instance", e); | |
throw new RowMapperInstanceException(e); | |
} catch (IllegalAccessException e) { | |
LOGGER.error("Can't generate RowMapper instance", e); | |
throw new RowMapperInstanceException(e); | |
} | |
} | |
/** | |
* Find an identity by a unique key. | |
* | |
* @param id | |
* the unique key as id | |
* @return the identity object | |
*/ | |
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED) | |
public T find(ID id) { | |
try { | |
List<T> results = jdbcTemplate.query("select * from " + tableName + " where " + idColName + "=?", | |
new Object[] { id }, rowMapper.newInstance()); | |
if (CollectionUtils.isEmpty(results)) { | |
LOGGER.warn("No {} found for id = {}", type.getSimpleName(), id); | |
return null; | |
} else if (results.size() > 1) { | |
LOGGER.warn("There are more than 1 {} use the same ID # {}", type.getSimpleName(), id); | |
} | |
return results.get(0); | |
} catch (InstantiationException e) { | |
throw new RowMapperInstanceException(e); | |
} catch (IllegalAccessException e) { | |
throw new RowMapperInstanceException(e); | |
} | |
} | |
/** | |
* Find entities by fields' query criteria. | |
* | |
* @param queries | |
* map contains query criteria, fieldName=>value | |
* @return a list of entities meet the query criteria | |
*/ | |
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED) | |
public List<T> findBy(Map<String, ?> queries) { | |
StringBuffer sql = new StringBuffer("select * from ").append(tableName); | |
List<Object> vals = Lists.newArrayListWithCapacity(queries.size()); | |
if (!CollectionUtils.isEmpty(queries)) { | |
sql.append(" where "); | |
List<String> whereClause = Lists.newLinkedList(); | |
for (String fieldName : queries.keySet()) { | |
whereClause.add(fieldName + "=?"); | |
vals.add(normalize(queries.get(fieldName))); | |
} | |
sql.append(Joiner.on(',').join(whereClause)); | |
} | |
try { | |
return jdbcTemplate.query(sql.toString(), vals.toArray(), rowMapper.newInstance()); | |
} catch (InstantiationException e) { | |
throw new RowMapperInstanceException(e); | |
} catch (IllegalAccessException e) { | |
throw new RowMapperInstanceException(e); | |
} | |
} | |
/** | |
* Find entities by a list of unique key. | |
* | |
* @param ids | |
* a list of unique key | |
* @return a list of entities | |
*/ | |
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED) | |
public List<T> find(List<ID> ids) { | |
try { | |
return new NamedParameterJdbcTemplate(jdbcTemplate).query("select * from " + tableName + " where " | |
+ idColName + " in (:ids)", Collections.singletonMap("ids", ids), rowMapper.newInstance()); | |
} catch (InstantiationException e) { | |
throw new RowMapperInstanceException(e); | |
} catch (IllegalAccessException e) { | |
throw new RowMapperInstanceException(e); | |
} | |
} | |
/** | |
* Update a list of entities. | |
* | |
* @param list | |
* list of entities to be updated | |
*/ | |
@Transactional | |
public void update(List<T> list) { | |
final List<String> cols = Lists.newArrayListWithCapacity(fieldNameIdx.size() - 1); // columns' name | |
final List<Field> fs = Lists.newArrayListWithCapacity(fieldNameIdx.size() - 1); // objects' field | |
for (Pair<String, Field> val : fieldNameIdx.values()) { | |
if (val.second().isAnnotationPresent(Id.class)) { | |
continue; | |
} | |
cols.add(val.first()); | |
fs.add(val.second()); | |
} | |
internalUpdate(list, cols, fs); | |
} | |
/** | |
* Update a list of entities with the fields that provided. | |
* | |
* @param list | |
* a list of entities to be updated | |
* @param fields | |
* a list of fields that need update | |
*/ | |
@Transactional | |
public void update(List<T> list, List<String> fields) { | |
final List<String> cols = Lists.newArrayListWithCapacity(fields.size()); // columns' name | |
final List<Field> fs = Lists.newArrayListWithCapacity(fields.size()); // objects' field | |
// get the fields that need to be updated | |
for (int i = 0; i < fields.size(); i++) { | |
String fieldName = fields.get(i); | |
Pair<String, Field> col = fieldNameIdx.get(fieldName); | |
if (col == null) | |
throw new RuntimeException(new NoSuchFieldException("Field " + fieldName + " is not found in class " | |
+ type.getSimpleName())); | |
// Primary key can not be update | |
Preconditions.checkState(!col.second().isAnnotationPresent(Id.class), "ID field cannot be updated"); | |
cols.add(i, col.first()); | |
fs.add(i, col.second()); | |
} | |
internalUpdate(list, cols, fs); | |
} | |
/** | |
* Update an entity with <code>ID</code> id. | |
* | |
* @param id | |
* the id of the entity that will be updated | |
* @param fields | |
* the fields and values | |
*/ | |
@Transactional | |
public void update(ID id, Map<String, ?> fields) { | |
List<String> cols = Lists.newArrayListWithCapacity(fields.size()); | |
List<Object> vals = Lists.newArrayListWithCapacity(fields.size() + 1); | |
for (Entry<String, ?> entry : fields.entrySet()) { | |
String fieldName = entry.getKey(); | |
if (cols.contains(fieldName)) { | |
LOGGER.error("Field {} is not found in class [{}]", fieldName, type.getSimpleName()); | |
throw new RuntimeException(new NoSuchFieldException()); | |
} | |
cols.add(fieldNameIdx.get(entry.getKey()).first()); | |
vals.add(normalize(entry.getValue())); | |
} | |
// plus identity | |
vals.add(id); | |
jdbcTemplate.update("update " + tableName + " set " | |
+ Joiner.on(",").join(Lists.transform(cols, new Function<String, String>() { | |
@Override | |
public String apply(String input) { | |
return input + "=?"; | |
} | |
})) + " where " + idColName + "=?", vals.toArray()); | |
} | |
/** | |
* Update a list of entities by <code>ID</code> id, with fields and values provide. | |
* | |
* @param ids | |
* a list of IDs for the entities that will be updated | |
* @param fields | |
* fiels need update | |
* @param values | |
* new values | |
*/ | |
@Transactional | |
public void update(List<ID> ids, List<String> fields, List<Object[]> values) { | |
List<Object[]> vals = Lists.newLinkedList(); | |
for (int i = 0; i < values.size(); i++) { | |
Object[] ary = values.get(i); | |
Object[] newAry = Arrays.copyOf(ary, ary.length + 1); // copy all the values | |
newAry[ary.length] = ids.get(i); // plus id | |
vals.add(newAry); | |
} | |
jdbcTemplate.batchUpdate("update " + tableName + " set " | |
+ Joiner.on(",").join(Lists.transform(fields, new Function<String, String>() { | |
@Override | |
public String apply(String input) { | |
return input + "=?"; | |
} | |
})) + " where " + idColName + "=?", vals); | |
} | |
/** | |
* Create a new instance of an entity | |
* | |
* @param o | |
* the entity to create | |
*/ | |
@Transactional | |
public void create(final T o) { | |
final List<String> cols = Lists.newArrayListWithCapacity(fieldNameIdx.size()); // columns' name | |
final List<Field> fs = Lists.newArrayListWithCapacity(fieldNameIdx.size()); // objects' field | |
for (Pair<String, Field> val : fieldNameIdx.values()) { | |
cols.add(val.first()); | |
fs.add(val.second()); | |
} | |
// '?' placeholder | |
String[] placehdrs = new String[cols.size()]; | |
Arrays.fill(placehdrs, "?"); | |
jdbcTemplate.update("insert into " + tableName + " (" + Joiner.on(',').join(cols) + ") values (" | |
+ Joiner.on(',').join(placehdrs) + ")", Lists.transform(fs, new Function<Field, Object>() { | |
@Override | |
public Object apply(Field input) { | |
return normalize(ReflectionUtils.getField(input, o)); | |
} | |
}).toArray()); | |
} | |
/** | |
* Create new instances of a list of entities | |
* | |
* @param list | |
* the list of entities to create | |
*/ | |
@Transactional | |
public void create(List<T> list) { | |
final List<String> cols = Lists.newArrayListWithCapacity(fieldNameIdx.size()); // columns' name | |
final List<Field> fs = Lists.newArrayListWithCapacity(fieldNameIdx.size()); // objects' field | |
for (Pair<String, Field> val : fieldNameIdx.values()) { | |
cols.add(val.first()); | |
fs.add(val.second()); | |
} | |
// '?' placeholder | |
String[] placehdrs = new String[cols.size()]; | |
Arrays.fill(placehdrs, "?"); | |
jdbcTemplate.batchUpdate("insert into " + tableName + " (" + Joiner.on(',').join(cols) + ") values (" | |
+ Joiner.on(',').join(placehdrs) + ")", Lists.transform(list, new Function<T, Object[]>() { | |
@Override | |
public Object[] apply(T input) { | |
Object[] result = new Object[cols.size()]; | |
for (int i = 0; i < fs.size(); i++) { | |
result[i] = normalize(ReflectionUtils.getField(fs.get(i), input)); | |
} | |
return result; | |
} | |
})); | |
} | |
/** | |
* Deletes an existing entity | |
* <p> | |
* The existing entity should be identifiable from the <code>ID</code> | |
* | |
* @param id | |
* the id of entity that to delete | |
*/ | |
@Transactional | |
public void delete(ID id) { | |
jdbcTemplate.update("delete from " + tableName + " where " + idColName + " = ?", id); | |
} | |
/** | |
* Delete a list of entities | |
* | |
* @param ids | |
* the list of id for the entities that to delete | |
*/ | |
@Transactional | |
public void delete(List<ID> ids) { | |
jdbcTemplate.batchUpdate("delete from " + tableName + " where " + idColName + " = ?", Lists.transform(ids, | |
new Function<ID, Object[]>() { | |
@Override | |
public Object[] apply(ID input) { | |
return new Object[] { input }; | |
} | |
})); | |
} | |
/** | |
* Internal method for batch updates | |
* | |
* @param list | |
* a list of entities to update | |
* @param colNames | |
* the names of columns to update | |
* @param fields | |
* the fields to get value from entities | |
*/ | |
protected void internalUpdate(final List<T> list, final List<String> colNames, final List<Field> fields) { | |
// retrieve batchArgs | |
final List<Object[]> batchArgs = Lists.transform(list, new Function<T, Object[]>() { | |
@Override | |
public Object[] apply(T input) { | |
Object[] result = new Object[colNames.size() + 1]; | |
for (int i = 0; i < fields.size(); i++) { | |
result[i] = normalize(ReflectionUtils.getField(fields.get(i), input)); | |
} | |
// Last arg: ID | |
result[fields.size()] = ReflectionUtils.getField(idField, input); | |
return result; | |
} | |
}); | |
// do updates via jdbcTemplate | |
jdbcTemplate.batchUpdate("update " + tableName + " set " | |
+ Joiner.on(",").join(Lists.transform(colNames, new Function<String, String>() { | |
@Override | |
public String apply(String input) { | |
return input + "=?"; | |
} | |
})) + " where " + idColName + "=?", batchArgs); | |
} | |
/** | |
* Normalize the values persisted to db | |
* | |
* @param val | |
* an object will be persist to db | |
* @return normalized value | |
*/ | |
public static Object normalize(Object val) { | |
if (val == null) return null; | |
if (val instanceof Enum<?>) return val.toString(); | |
if (val instanceof Date) return DateToDBFormat.format((Date) val); | |
return val; | |
} | |
} | |
class RowMapperInstanceException extends RuntimeException { | |
/** | |
* | |
*/ | |
private static final long serialVersionUID = -6400270330157753961L; | |
public RowMapperInstanceException() { | |
} | |
public RowMapperInstanceException(Throwable cause) { | |
super(cause); | |
} | |
public RowMapperInstanceException(String message) { | |
super(message); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment