美文网首页程序员Java 杂谈
light-dao框架升级(基于接口的sql注入)

light-dao框架升级(基于接口的sql注入)

作者: littlersmall | 来源:发表于2018-03-23 17:15 被阅读128次

    1 起因

    在这篇文章中:

    https://www.jianshu.com/p/b49a89d5df34

    我们介绍了light-dao框架的基本实现。在使用了一段时间后我发现,这个框架在某些场景下,还是过重了。
    比如:

    select * from info where id = 10;
    

    如果使用light-dao中原本的做法,需要这样:

        @Select("select * from info where id = {0}")
        List<UserInfo> selectUserInfo(int id);
    

    当然,在sql比较简单时,这样写也很方便。但是,想要插入一条数据的时候,就很麻烦了。
    需要这样:

        @Update("insert into " + TABLE_NAME + "(id, name) values ({user.id}, {user.name});")
        int insert(@SqlParam("user") User user);
    

    再比如,如果user本身是分表的,目前的light-dao需要做较大的修改,需要把tableName作为参数定制,还是相当复杂的。

    因此,目前的light-dao有几个比较严重的问题:
    a 当表中字段比较多的时候,sql会比较复杂,很容易出问题
    b 当表中需要新增字段的时候,不仅需要改model层,还需要更改dao层的sql,很容易遗漏
    c 对于分表的情况支持的不好

    鉴于上面的问题,我一直在思考,能不能设计一套更简单的框架,能满足下面几个条件
    a 90%的条件下,不需要写sql
    b 和model能更好的映射,在插入,查找,删除的时候,代码能尽量的简单,在增删字段的时候,对dao层能透明
    c 比较好的支持分表需求

    2 示例
    在反复尝试了几次后,终于完成了上述的几个目标,我们先贴一段示例代码:
    info表按id分表100个
    a model层

        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public static class Info {
            @PrimaryKey
            int id;
            String information;
            int userId;
        }
    

    b dao层

    @Repository
    public class InfoDAO implements ShardDAOBaseGet<Info>, ShardDAOBaseInsert<Info> {
        @Autowired
        @Qualifier("myDbDataSource")
        private DataSource myDataSource;
    
        @Override
        public Class<Info> getClazz() {
            return Info.class;
        }
    
        @Override
        public NamedParameterJdbcTemplate getReader() {
            return new NamedParameterJdbcTemplate(myDataSource);
        }
    
        @Override
        public NamedParameterJdbcTemplate getWriter() {
            return new NamedParameterJdbcTemplate(myDataSource);
        }
        
        public Info getInfoById(long shardId, long id) {
            return getByKey(shardId, "id", id);
        }
    
        public int insertInfo(long shardId, Info info) {
            return insert(shardId, info);
        }
    }
    

    这样一来,就清爽了很多。

    3 源码解析
    (1) DAOBase
    这个接口是整个框架的核心模块,它的代码如下:

    public interface DAOBase<T> {
        Map<Class, RowMapper> ROW_MAPPER_MAP = new ConcurrentHashMap<>();
        Map<Class, String> TABLE_NAME_MAP = new ConcurrentHashMap<>();
        Map<Class, String> PRIMARY_KEY_NAME_MAP = new ConcurrentHashMap<>();
    
        Class<T> getClazz();
    
        @SuppressWarnings("unchecked")
        default RowMapper<T> getRowMapper() {
            return ROW_MAPPER_MAP.computeIfAbsent(getClazz(), BeanPropertyRowMapper::new);
        }
    
        default String getTableName() {
            return TABLE_NAME_MAP.computeIfAbsent(getClazz(), (clazz) -> {
                String tableName = clazz.getSimpleName();
                tableName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, tableName);
    
                return tableName;
            });
        }
    
        default String getPrimaryKeyName() {
            return PRIMARY_KEY_NAME_MAP.computeIfAbsent(getClazz(), (clazz) ->
                    Arrays.stream(clazz.getDeclaredFields())
                    .filter(field ->
                            field.getDeclaredAnnotation(PrimaryKey.class) != null)
                    .findAny()
                    .get()
                    .getName());
        }
    
        default long getPrimaryKey(T model) {
            return (long) ReflectUtils.getField(model, getPrimaryKeyName());
        }
    }
    

    这里面一共有5个接口:
    a Class<T> getClazz() 这个接口描述了model的class
    b RowMapper<T> getRowMapper() 这个接口用于将数据库的一行数据映射为一个model
    c String getTableName() 这个接口描述需要访问的表名
    d String getPrimaryKeyName() 这个接口描述这个表的主键名(我们只支持单主键,并且只支持主键类型为long)
    e long getPrimaryKey(Object model) 这个接口描述的是通过model获取主键(主要在stream访问时使用)
    (2) DAOBaseGet
    这个接口继承自DAOBase,主要实现了select相关接口,挑几个比较典型的接口介绍下
    a <Key> T getByKey(String keyName, Key key)
    这个接口相当于

    select * from TABLE where key_name = key
    

    b <Key> Stream<T> listByKeyDesc(String keyName, Key key)
    这个接口相当于

    select * from TABLE where key_name = key ordery by primary_key desc
    

    c <Key extends Comparable> Stream<T> listByRangeDesc(String keyName, Range<Key> range)
    这个接口相当于

    select * from TABLE where key_name >(=) range.left and key_name <(=) range.right order by primary_key desc
    

    它的实现代码如下:

        default <Key extends Comparable> Stream<T> listByRangeDesc(String keyName, Range<Key> range) {
            MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
            final String dbPrimaryKeyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
                    getPrimaryKeyName());
            List<String> conditions = DAOUtils.buildRangeConditions(mapSqlParameterSource, keyName, range);
    
            return CursorIterator.<Long, T>newGenericBuilder().bufferSize(BUFFER_SIZE)
                    .start(Long.MAX_VALUE).cursorExtractor(this::getPrimaryKey)
                    .build((cursor, limit) -> getReader().query(
                            new SqlQueryBuilder()
                                    .select("*")
                                    .from(getTableName())
                                    .where(conditions)
                                    .and(format("%s<=:cursor", dbPrimaryKeyName))
                                    .orderBy(dbPrimaryKeyName)
                                    .desc()
                                    .limit(":limit")
                                    .build(),
                            mapSqlParameterSource.addValue("cursor", cursor)
                                    .addValue("limit", limit),
                            getRowMapper()))
                    .stream();
        }
    

    其中CursorIterator使用了迭代模式返回结果。
    (3) DAOBaseInsert
    这个接口主要用于insert or replace or update语句,还是挑几个接口介绍下
    a int insert(T model)
    这个语句等价于

    insert into TABLE(column1, column2...) values (value1, value2...);
    

    b long insertReturnKey(T model)
    这个语句会返回自增的主键指
    c int insertOnDuplicate(T model, String... conditions)
    这个语句等价于

    insert into TABLE(column1, column2...) values(value1, value2...)
        on duplicate key update conditions
    

    d <Key> int update(long primaryKey, String keyName, Key key)
    这个语句等价于

    update TABLE set key_name = key where primary_key = primaryKey
    

    贴一下update的代码实现:

        default int update(long primaryKey, List<Map.Entry<String, Object>> entryList, NamedParameterJdbcTemplate template) {
            if (template == null) {
                template = getWriter();
            }
    
            MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
            List<String> conditions = new ArrayList<>();
    
            entryList.forEach(entry -> {
                conditions.add(format(" %s=:%s ", CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey()),
                        entry.getKey()));
                mapSqlParameterSource.addValue(entry.getKey(), entry.getValue());
            });
    
            final String dbPrimaryKeyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
                    getPrimaryKeyName());
    
            return template.update(
                    new SqlUpdateBuilder()
                            .update(getTableName())
                            .set(conditions)
                            .where(format("%s=:%s", dbPrimaryKeyName, getPrimaryKeyName()))
                            .build(),
                    mapSqlParameterSource.addValue(getPrimaryKeyName(), primaryKey));
        }
    

    (4) 分表逻辑
    分表逻辑的实现基于对表的数量取模,比如user表有100个分表,分别为user_0, user_1 ... user_99,在实现时需要指定分表的id,在我们代码中叫shardId。比如user表按userId分为100张表,则我们可以使用ShardDAOBaseGet接口,并在每个get和list相关的接口中传入userId,框架会自动完成分表逻辑。

    4 使用方式
    (1) 首先定义一个数据库的数据源

        <!-- for example -->
        <bean id="myDbDataSource" class="org.apache.commons.dbcp.BasicDataSource"
              destroy-method="close" lazy-init="false">
            <property name="driverClassName" value="org.h2.Driver"></property>
            <property name="url" value="jdbc:h2:mem:my_db"></property>
            <property name="username" value="test"></property>
            <property name="password" value=""></property>
        </bean>
    

    :比较推荐的做法其实是把数据库的相关数据配置在zoomkeeper中,方便动态切换。
    (2) 根据表结构构建model层,比如,表的sql语句为

    create table info (id int, information varchar, user_id int);
    

    则对应的model代码为:

        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public static class Info {
            @PrimaryKey  //用于指定主键
            int id;
            String information;
            int userId;
        }
    

    : @PrimaryKey用于指定表的主键,必须有,否则在listByXXDesc方法中会有问题。
    同时注意表名和model的类名要一致,比如表名为user_info,则model类名应为UserInfo。字段名和数据库的字段也需要一致。(model使用驼峰,数据库使用下划线命名方式)
    (3) 构建DAO代码

    @Repository
    public class InfoDAO implements ShardDAOBaseGet<Info>, ShardDAOBaseInsert<Info> {
        @Autowired
        @Qualifier("myDbDataSource")
        private DataSource myDataSource;
    
        @Override
        public Class<Info> getClazz() {
            return Info.class;
        }
    
        @Override
        public NamedParameterJdbcTemplate getReader() {
            return new NamedParameterJdbcTemplate(myDataSource);
        }
    
        @Override
        public NamedParameterJdbcTemplate getWriter() {
            return new NamedParameterJdbcTemplate(myDataSource);
        }
        
        public Info getInfoById(long shardId, long id) {
            return getByKey(shardId, "id", id);
        }
    
        public int insertInfo(long shardId, Info info) {
            return insert(shardId, info);
        }
    }
    

    这样就可以方便的使用DAOBase里各种默认方法了。

    5 源码地址
    :本次升级属于兼容性升级,原来的light-dao相关的自动注入,注解模式仍可使用。
    当对数据库的使用比较轻量(不存在join表,group by等复杂操作)时,推荐使用新的DAOBase构建DAO层代码。如果确实有很多复杂的重sql逻辑,比如数据分析,数据导出等,还是建议使用原来的@Select sql注入的模式。
    新的代码逻辑主要集中在

    package com.littlersmall.lightdao.base
    

    github地址

    https://github.com/littlersmall/light-dao

    have fun

    相关文章

      网友评论

        本文标题:light-dao框架升级(基于接口的sql注入)

        本文链接:https://www.haomeiwen.com/subject/gbeaqftx.html