Skip to content

Instantly share code, notes, and snippets.

@cemo
Created March 8, 2013 07:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cemo/5114715 to your computer and use it in GitHub Desktop.
Save cemo/5114715 to your computer and use it in GitHub Desktop.
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