本文隶属于专题系列: 通用数据库访问层dexcoder-dal

注意:本组件已重构并全新发布,更加方便易于使用。代码全部开源,详情访问 Github仓库码云

前面我们已经定义好了接口,也贴出了如何使用的测试类,现在来说说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;
//......
}

代码都差不多贴完了,最后讲讲几个方法一些细微区别的地方。

  1. insert和save方法区别:insert会处理主键而save方法不会,需要你设置好主键值。这个在有时候需要插入一条标识记录时十分有用,比如我要插入一条主键为-1的记录。
  2. update方法entity和criteria方式:传入entity时是不可以把字段更新为null的,因为null值的属性都被忽略了,这是为了防止有时只想更新一个字段时不小心把整条记录给清空。而criteria设置单个属性值时是可以设置为null的,想把字段更新为null时可以用criteria方式。
  3. deleteAll方法因为使用的是TRUNCATE,所以数据库可能会需要有ddl权限。
  4. querySingleResult方法在没有结果时是返回null,这跟spring的queryForXXX方法无结果时的抛出异常不同。另外当有多个结果时它是取第一条,需要注意。
  5. entity和criteria方式是可以混合使用的,但是推荐尽量少用。
  6. 其它的,自己看源码吧

dao讲的差不多了,接下来就该讲讲分页的实现了,待续,,,

[spring-jdbc2]:

你可能感兴趣的内容
Spring 之 JMS 监听JMS消息 收藏,4403 浏览
Spring 之 JMS 基于JMS的RPC 收藏,3253 浏览
Spring的BeanFactory和FactoryBean 收藏,10405 浏览
1条评论
huimark 1年前
黑名单,白名单 是什么意思哦?

selfly

交流QQ群:32261424
Owner