前面我们已经定义好了接口,也贴出了如何使用的测试类,现在来说说dao类的实现。
其实dao类的实现并没有什么技术含量,无非就是根据传入的参数进行一个sql的拼装并执行而已,关键在于如何做才能方便和优雅一些。
![spring-jdbc2][]
先来看看实现类定义的一些成员变量:
/**
* jdbc操作dao
*
* Created by liyd on 3/3/15.
*/
public class JdbcDaoImpl implements JdbcDao {
/** spring jdbcTemplate 对象 */
protected JdbcTemplate jdbcTemplate;
/** 名称处理器,为空按默认执行 */
protected NameHandler nameHandler;
/** rowMapper,为空按默认执行 */
protected String rowMapperClass;
/** 数据库方言 */
protected String dialect;
//.........
}
jdbcTemplate不多说了,必须的。这里之所以没有用注解的方式自动注入,是因为有时候这个反而会成为麻烦,比如当你有多个数据源对应多个jdbcTemplate的时候。
nameHandler名称处理器,前面已经介绍过了,有默认实现DefaultNameHandler,这里我又增加了一个方法,用来处理主键,目前主要是oracle:
/**
* 根据实体名获取主键值 自增类主键数据库直接返回null即可
*
* @param entityClass the entity class
* @param dialect the dialect
* @return pK value
*/
public String getPKValue(Class<?> entityClass, String dialect) {
if (StringUtils.equalsIgnoreCase(dialect, "oracle")) {
//获取序列就可以了,默认seq_加上表名为序列名
String tableName = this.getTableName(entityClass);
return String.format("SEQ_%s.NEXTVAL", tableName);
}
return null;
}
如果是oracle,让它返回序列名,自增类的数据库直接返回null。
rowMapperClass,为空时使用的是spring自带的BeanPropertyRowMapper.newInstance(clazz),如果数据库命名规范的话一般情况下都能满足。
dialect,数据库言,主要是为了判断如何处理主键id,看到上面的getPKValue方法就应该知道了,目前除了oracle其它都按自增类处理。
成员变量介绍完了,下面说说方法的实现。这里我们抽几个典型的样例方法。
首先当然是insert方法了,算是相对复杂点,因为它要处理主键的返回,主要代码如下:
/**
* 插入数据
*
* @param entity the entity
* @param criteria the criteria
* @return long long
*/
private Long insert(Object entity, Criteria criteria) {
Class<?> entityClass = SqlAssembleUtils.getEntityClass(entity, criteria);
NameHandler handler = this.getNameHandler();
String pkValue = handler.getPKValue(entityClass, this.dialect);
if (StringUtils.isNotBlank(pkValue)) {
String primaryName = handler.getPKName(entityClass);
if (criteria == null) {
criteria = Criteria.create(entityClass);
}
criteria.setPKValueName(NameUtils.getCamelName(primaryName), pkValue);
}
final BoundSql boundSql = SqlAssembleUtils.buildInsertSql(entity, criteria,
this.getNameHandler());
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement(boundSql.getSql(),
new String[] { boundSql.getPrimaryKey() });
int index = 0;
for (Object param : boundSql.getParams()) {
index++;
ps.setObject(index, param);
}
return ps;
}
}, keyHolder);
return keyHolder.getKey().longValue();
}
这里处理了两个方法的重载,并且当nameHandler的获取主键方法返回不为null时,在criteria中添加一个主键值名称的字段。
这个所谓的主键值名称可能一下子不怎么理解,简单的说它说是设置了主键字段的值为oracle的序列名,而不是获取的序列值,这样在插入数据时只要访问一次数据库就可以了。
接下来就是sql的拼装了,看到上面的设置主键值的方法,你可能也就猜到了,所有entity类的的属性的criteria中设置的属性,都是合并在一起成AutoField之后,然后再统一处理进行sql的拼装。
主要代码:
/**
* 构建insert语句
*
* @param entity 实体映射对象
* @param criteria the criteria
* @param nameHandler 名称转换处理器
* @return bound sql
*/
public static BoundSql buildInsertSql(Object entity, Criteria criteria, NameHandler nameHandler) {
Class<?> entityClass = getEntityClass(entity, criteria);
List<AutoField> autoFields = (criteria != null ? criteria.getAutoFields()
: new ArrayList<AutoField>());
List<AutoField> entityAutoField = getEntityAutoField(entity, AutoField.UPDATE_FIELD);
//添加到后面
autoFields.addAll(entityAutoField);
String tableName = nameHandler.getTableName(entityClass);
String pkName = nameHandler.getPKName(entityClass);
StringBuilder sql = new StringBuilder("INSERT INTO ");
List<Object> params = new ArrayList<Object>();
sql.append(tableName);
sql.append("(");
StringBuilder args = new StringBuilder();
args.append("(");
for (AutoField autoField : autoFields) {
if (autoField.getType() != AutoField.UPDATE_FIELD
&& autoField.getType() != AutoField.PK_VALUE_NAME) {
continue;
}
String columnName = nameHandler.getColumnName(autoField.getName());
Object value = autoField.getValues()[0];
sql.append(columnName);
//如果是主键,且是主键的值名称
if (StringUtils.equalsIgnoreCase(pkName, columnName)
&& autoField.getType() == AutoField.PK_VALUE_NAME) {
//参数直接append,传参方式会把值当成字符串造成无法调用序列的问题
args.append(value);
} else {
args.append("?");
params.add(value);
}
sql.append(",");
args.append(",");
}
sql.deleteCharAt(sql.length() - 1);
args.deleteCharAt(args.length() - 1);
args.append(")");
sql.append(")");
sql.append(" VALUES ");
sql.append(args);
return new BoundSql(sql.toString(), pkName, params);
}
这里要注意的一个就是判断了如果是主键并且是值名称类型的,直接把它拼装到sql语句而不使用传参的方式,因为使用传参数据库会把oracle的序列当成字符串来处理而不是调用序列。
查询列表方法:
@Override
public <T> List<T> queryList(T entity, Criteria criteria) {
BoundSql boundSql = SqlAssembleUtils.buildListSql(entity, criteria, this.getNameHandler());
List<?> list = jdbcTemplate.query(boundSql.getSql(), boundSql.getParams().toArray(),
this.getRowMapper(entity.getClass()));
return (List<T>) list;
}
这个比较简单,主要是sql的拼装,代码:
/**
* 构建列表查询sql
*
* @param entity the entity
* @param criteria the criteria
* @param nameHandler the name handler
* @return bound sql
*/
public static BoundSql buildListSql(Object entity, Criteria criteria, NameHandler nameHandler) {
BoundSql boundSql = SqlAssembleUtils.buildQuerySql(entity, criteria, nameHandler);
StringBuilder sb = new StringBuilder(" ORDER BY ");
if (criteria != null) {
for (AutoField autoField : criteria.getOrderByFields()) {
sb.append(nameHandler.getColumnName(autoField.getName())).append(" ")
.append(autoField.getFieldOperator()).append(",");
}
if (sb.length() > 10) {
sb.deleteCharAt(sb.length() - 1);
}
}
if (sb.length() < 11) {
sb.append(boundSql.getPrimaryKey()).append(" DESC");
}
boundSql.setSql(boundSql.getSql() + sb.toString());
return boundSql;
}
相对复杂点是因为列表查询要进行排序的处理。这里分成了两部,先把查询sql拼装出来,因为查询sql在get方法时也能用到,之后再追加排序字段,如果没有设置排序字段则按默认的主键倒序排列,即最新的数据在前面。
查询sql拼装代码:
/**
* 按设置的条件构建查询sql
*
* @param entity the entity
* @param criteria the criteria
* @param nameHandler the name handler
* @return bound sql
*/
public static BoundSql buildQuerySql(Object entity, Criteria criteria, NameHandler nameHandler) {
Class<?> entityClass = getEntityClass(entity, criteria);
List<AutoField> autoFields = (criteria != null ? criteria.getAutoFields()
: new ArrayList<AutoField>());
String tableName = nameHandler.getTableName(entityClass);
String primaryName = nameHandler.getPKName(entityClass);
List<AutoField> entityAutoField = getEntityAutoField(entity, AutoField.WHERE_FIELD);
autoFields.addAll(entityAutoField);
String columns = SqlAssembleUtils.buildColumnSql(entityClass, nameHandler,
criteria == null ? null : criteria.getIncludeFields(), criteria == null ? null
: criteria.getExcludeFields());
StringBuilder querySql = new StringBuilder("SELECT " + columns + " FROM ");
querySql.append(tableName);
List<Object> params = Collections.EMPTY_LIST;
if (!CollectionUtils.isEmpty(autoFields)) {
querySql.append(" WHERE ");
BoundSql boundSql = SqlAssembleUtils.builderWhereSql(autoFields, nameHandler);
params = boundSql.getParams();
querySql.append(boundSql.getSql());
}
return new BoundSql(querySql.toString(), primaryName, params);
}
这里又分成了几步,主要是处理了字段的白名单、黑名单,where条件的拼装。以下是各代码:
拼装查询字段代码:
/**
* 构建查询的列sql
*
* @param clazz the clazz
* @param nameHandler the name handler
* @param includeField the include field
* @param excludeField the exclude field
* @return string string
*/
public static String buildColumnSql(Class<?> clazz, NameHandler nameHandler,
List<String> includeField, List<String> excludeField) {
StringBuilder columns = new StringBuilder();
//获取属性信息
BeanInfo beanInfo = ClassUtils.getSelfBeanInfo(clazz);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
String fieldName = pd.getName();
//白名单 黑名单
if (!CollectionUtils.isEmpty(includeField) && !includeField.contains(fieldName)) {
continue;
} else if (!CollectionUtils.isEmpty(excludeField) && excludeField.contains(fieldName)) {
continue;
}
String columnName = nameHandler.getColumnName(fieldName);
columns.append(columnName);
columns.append(",");
}
columns.deleteCharAt(columns.length() - 1);
return columns.toString();
}
拼装where条件代码:
/**
* 构建where条件sql
*
* @param autoFields the auto fields
* @param nameHandler the name handler
* @return bound sql
*/
private static BoundSql builderWhereSql(List<AutoField> autoFields, NameHandler nameHandler) {
StringBuilder sql = new StringBuilder();
List<Object> params = new ArrayList<Object>();
Iterator<AutoField> iterator = autoFields.iterator();
while (iterator.hasNext()) {
AutoField autoField = iterator.next();
if (AutoField.WHERE_FIELD != autoField.getType()) {
continue;
}
//操作过,移除
iterator.remove();
if (sql.length() > 0) {
sql.append(" ").append(autoField.getSqlOperator()).append(" ");
}
String columnName = nameHandler.getColumnName(autoField.getName());
Object[] values = autoField.getValues();
if (StringUtils.equalsIgnoreCase(IN, StringUtils.trim(autoField.getFieldOperator()))
|| StringUtils.equalsIgnoreCase(NOT_IN,
StringUtils.trim(autoField.getFieldOperator()))) {
//in,not in的情况
sql.append(columnName).append(" ").append(autoField.getFieldOperator()).append(" ");
sql.append("(");
for (int j = 0; j < values.length; j++) {
sql.append(" ?");
params.add(values[j]);
if (j != values.length - 1) {
sql.append(",");
}
}
sql.append(")");
} else if (values == null) {
//null 值
sql.append(columnName).append(" ").append(autoField.getFieldOperator())
.append(" NULL");
} else if (values.length == 1) {
//一个值 =
sql.append(columnName).append(" ").append(autoField.getFieldOperator())
.append(" ?");
params.add(values[0]);
} else {
//多个值,or的情况
sql.append("(");
for (int j = 0; j < values.length; j++) {
sql.append(columnName).append(" ").append(autoField.getFieldOperator())
.append(" ?");
params.add(values[j]);
if (j != values.length - 1) {
sql.append(" OR ");
}
}
sql.append(")");
}
}
return new BoundSql(sql.toString(), null, params);
}
拼装时处理了in、not in、null值和or的情况,具体可以看源代码。
到这里,主要的内容就只剩一个Criteria 了,它内部其实就是保存了设置的需要操作的字段信息,看下面的定义就能明白个大概,具体可以看源码。
/**
* sql操作Criteria
*
* Created by liyd on 3/3/15.
*/
public class Criteria {
/** 操作的实体类 */
private Class<?> entityClass;
/** 操作的字段 */
private List<AutoField> autoFields;
/** 排序字段 */
private List<AutoField> orderByFields;
/** 白名单 */
private List<String> includeFields;
/** 黑名单 */
private List<String> excludeFields;
/** where标识 */
private boolean isWhere = false;
//......
}
代码都差不多贴完了,最后讲讲几个方法一些细微区别的地方。
- insert和save方法区别:insert会处理主键而save方法不会,需要你设置好主键值。这个在有时候需要插入一条标识记录时十分有用,比如我要插入一条主键为-1的记录。
- update方法entity和criteria方式:传入entity时是不可以把字段更新为null的,因为null值的属性都被忽略了,这是为了防止有时只想更新一个字段时不小心把整条记录给清空。而criteria设置单个属性值时是可以设置为null的,想把字段更新为null时可以用criteria方式。
- deleteAll方法因为使用的是TRUNCATE,所以数据库可能会需要有ddl权限。
- querySingleResult方法在没有结果时是返回null,这跟spring的queryForXXX方法无结果时的抛出异常不同。另外当有多个结果时它是取第一条,需要注意。
- entity和criteria方式是可以混合使用的,但是推荐尽量少用。
- 其它的,自己看源码吧
dao讲的差不多了,接下来就该讲讲分页的实现了,待续,,,
[spring-jdbc2]: