使用Spring-JDBC自定义一款简单ORM

一 概述

目前市面上有很多主流的ORM框架,像Mybatis、Hibernate等,它们都拥有强大的功能,而且还是免费的,因此,受到很多公司和个人的青睐。这些ORM框架确实很强大,大到我做一个小小的博客网站,都感觉是杀鸡用了牛刀。很多时候,一些小项目或者个人网站,其实是不需要那么多的功能。相反,强大意味着复杂,自身技术能力不够强的情况下,很难改动。本人是不太喜欢使用像Mybatis、Hibernate这样的ORM框架的,因为我特别讨厌编写XML文件,要学很多语法和标签,特别是在编写动态SQL和维护别人写的SQL时,我的内心是崩溃的。为此,我做了一些小小的研究,自己写了一套基于Spring-JDBC的ORM工具,并在公司的小项目中使用起来,不仅加快了项目的开发进度,代码量也变得少了很多。因此,我把我所知道的分享给大家,希望能给你带来一定的收获。

二 编写一个简单的ORM工具

本篇博客所描述的ORM不能叫做框架,因为它很简单,很小,根本做不到框架的一些特性,而且是基于Spring-JDBC来实现的,说简单点就是对各种工具做一种整合,以实现自己想要的功能。
工具中我将使用spring NamedParameterJdbcTemplate类来做Java bean 与数据库表的映射,使用过 NamedParameterJdbcTemplate的同学肯定都知道,这个类已经很符合我们的使用了,感觉没有必要再做一层封装了,其实不然,下面,我们就一起来看看不对NamedParameterJdbcTemplate有哪些不足。(如果有不会使用NamedParameterJdbcTemplate的,可先简单了解下在看本篇博客)

1)NamedParameterJdbcTemplate对于业务开发还有哪些不足

  • 插入一条数据会不断写逻辑类似的代码

请看以下例子,你可能觉得这已经不可能在优化了,其实不然。如果我现在换了一个张数据库表,比如订单表,那么插入订单的时候,这样子的代码是不是似曾相识,唯一不同的就是插入的字段名不一样。

// OrmToolTestPo.java
/**
 * @Author: Yh
 * @Date: 2019-07-19
 * @Time: 15:12
 * Copyright © Bell All Rights Reserved.
 */
@Data
public class OrmToolTestPo {

    private Integer id;

    private String name;

    private String desc;

}
// OrmToolTestDao.java
/**
 * @Author: Yh
 * @Date: 2019-07-19
 * @Time: 15:12
 * Copyright © Bell All Rights Reserved.
 */
@Repository
public class OrmToolTestDao {
    @Resource
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    public long insert(OrmToolTestPo ormToolTestPo) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        SQL sql = new SQL();
        sql.INSERT_INTO("bell_orm_test")
                .INTO_COLUMNS("name,desc")
                .INTO_VALUES(":name", ":desc");

        namedParameterJdbcTemplate.update(sql.toString(),
                new BeanPropertySqlParameterSource(ormToolTestPo), keyHolder);
        return Optional.ofNullable(keyHolder.getKey()).map(Number::intValue).orElse(0);
    }
}
  • 获取数据时同样会有冗余的代码

你会发现,如果通过主键ID去查找数据时,同样会出现冗余的代码。

    public OrmToolTestPo getById(Integer id) {
        String sql = "select * from um.adnest_landing_page_haoke where id = :id";
        return namedParameterJdbcTemplate.queryForObject(sql,
                new MapSqlParameterSource("id", id),
                new BeanPropertyRowMapper<>(OrmToolTestPo.class));
    }
  • 全量更新字段时代码会有冗余
public int update(long id, OrmToolTestPo ormToolTestPo) {
        String sql = "update bell_orm_test " +
                "set name = :name, desc = :desc where id =:id";
        return namedParameterJdbcTemplate.update(sql,
                new MapSqlParameterSource()
                        .addValue("name", ormToolTestPo.getName())
                        .addValue("desc", ormToolTestPo.getDesc())
                        .addValue("id", id));
}
  • 其它问题

其它问题还有很多。比如:

(a) 实现自己的ORM框架时,如果要忽略某个字段不被数据库识别,这种特性就需要自行实现封装Bean,在封装的过程中使用自定义注解过滤掉不需要映射的字段。(不在本博客范畴,如果有朋友想了解,可以一起探讨)

(b) 给特定列取别名;

© 自动使用bean创建数据库表;

(d) 数据缓存;

像以上问题,仅仅依靠NamedParameterJdbcTemplate是无法实现的,这个需要更深一步去封装,这种类型的封装过于复杂,本博客讲解不清楚,如果有想了解的朋友,可以私我一起探讨。

2)解决NamedParameterJdbcTemplate的短板

如何解决上面所遇到的问题,使用反射的手段对NamedParameterJdbcTemplate做一个简单的封装,以减少编程的工作量。接下来,我们一起来封装一把。

  • 自定义注解映射数据库表与Java bean的对应关系

首先,自定义一个注解,用来标注数据库表与之对应的实体类,便于将数据库表中的数据封装成Java Bean。代码如下

/**
 * @Author: Yh
 * @Date: 2019-07-10
 * @Time: 14:49
 * Copyright ©  All Rights Reserved.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MySqlTable {

    /**
     * 表名
     */
    String value();
}
  • 抽取Dao层操作数据库的共有方法为接口

对一些经常需要用到的操作数据库的方法抽取接口进行约束,规范具体实现类;由于我经常使用求个数,但又不想写MapSqlParameterSource参数映射,特别是参数很少的时候,所以我抽取了几个占位符为“?”的方法,方便使用,它与NamedParameterJdbcTemplate的":"占位符是不一致的,使用时需要注意。

/**
 * @Author: Yh
 * @Date: 2019-07-10
 * @Time: 14:25
 * Copyright © Bell All Rights Reserved.
 */
public interface IAbstractDaoSupport<T> {

    /**
     * insert po
     * @param t 实体类
     * @return true 插入成功
     */
    Boolean insert(T t);
    
    /**
     * update
     * @param sql sql
     * @param params params
     * @return true 更新成功
     */
    Boolean update(String sql, MapSqlParameterSource params);

    /**
     * batch insert po
     * @param tList
     * @return 每个实体处理的结果
     */
    int[] batchInsert(List<T> tList);

    /**
     * delete po
     * @param id 为兼容数据 所有Integer类将其转化为Long 主键底层一律将其识别为数字
     * @return true 删除成功
     */
    Boolean delete(Long id);

    /**
     * select po by id
     * @param id 主键ID 为兼容数据 所有Integer类将其转化为Long 主键底层一律将其识别为数字
     * @return maybe return null
     */
    T select(Long id);

    /**
     * 查询单个字段
     * @param sql sql
     * @param paramMap paramMap
     * @param otherClazz 返回字段类型(Integer、String)
     * @return maybe return null
     */
    <F> F select(String sql, MapSqlParameterSource paramMap, Class<F> otherClazz);

    /**
     * select po by idList
     * @param idList 主键idList 为兼容数据 所有Integer类将其转化为Long 主键底层一律将其识别为数字
     * @return maybe return null
     */
    List<T> select(Collection<Long> idList);

    /**
     * 根据sql查找对象 仅返回一个 因此查找条件的结果最好是一个
     *
     * @param sql sql
     * @param parameterSources params
     * @return maybe return null
     */
    T select(String sql, MapSqlParameterSource parameterSources);

    /**
     * query list by sql
     * @param sql sql
     * @param parameterSources params
     */
    List<T> queryList(String sql, MapSqlParameterSource parameterSources);

    /**
     * query list by sql (查询单个字段的list)
     * @param sql sql
     * @param parameterSources params
     * @param otherClazz 要返回的class(可选值Integer、String)
     */
    <F> List<F> queryList(String sql, MapSqlParameterSource parameterSources, Class<F> otherClazz);

    /**
     * where指定条件下数据是否存在
     *
     * @param where 条件,值使用?代替, eg : status = ? AND name = ?
     * @param args 参数列表,按照?顺序匹配
     * @return true 存在
     */
    Boolean existByWhere(String where, Object... args);

    /**
     * 根据sql求记录个数
     *
     * @param sql sql
     * @param parameterSources params
     * @return 记录个数
     */
    Integer count(String sql, MapSqlParameterSource parameterSources);

    /**
     * 根据where求记录个数
     *
     * @param where 条件,值使用?代替, eg : status = ? AND name = ?
     * @param args 参数列表,按照?顺序匹配
     * @return 记录个数
     */
    Integer countByWhere(String where, Object... args);
}

  • 抽取抽象Dao层公共特性并实现 IAbstractDaoSupport 接口

此处,我们就要在这个抽象Dao里完成Bean的封装,结果解析等一系列操作。

/**
 * @Author: Yh
 * @Date: 2019-07-10
 * @Time: 14:25
 * Copyright © Bell All Rights Reserved.
 */
public abstract class AbstractDaoSupport<T> implements IAbstractDaoSupport<T>, InitializingBean {

    @Resource
    private NamedParameterJdbcTemplate template;

    private Class clazz;

    private String tableName;

    @Override
    public void afterPropertiesSet() {
        this.clazz = getCurrentClass();
        this.tableName = getPoTableName();
    }

    @Override
    public Boolean insert(T o) {
        String sql = buildInsertSql();
        return template.update(sql, new BeanPropertySqlParameterSource(o)) > 0;
    }

    @Override
    public Boolean insertEscapeNull(T t) {
        String sql = buildInsertSql(t);
        return template.update(sql, new BeanPropertySqlParameterSource(t)) > 0;
    }

    @Override
    public Boolean updateEscapeNull(T t) {
        String sql = buildUpdateSql(t);
        return template.update(sql, new BeanPropertySqlParameterSource(t)) > 0;
    }

    @Override
    public Boolean update(String sql, MapSqlParameterSource params) {
        return template.update(sql, params) > 0;
    }

    @Override
    public int[] batchInsert(List<T> tList) {
        String sql = buildInsertSql();
        SqlParameterSource[] paramMapArray = tList.stream()
                .map(BeanPropertySqlParameterSource::new).collect(Collectors.toList())
                .toArray(new SqlParameterSource[tList.size()]);
        return template.batchUpdate(sql, paramMapArray);
    }

    @Override
    public Boolean delete(Long id) {
        SQL sql = new SQL();
        sql.DELETE_FROM(tableName)
                .WHERE("id = :id");
        return template.update(sql.toString(), new MapSqlParameterSource("id", id)) > 0;
    }

    @Override
    public T select(Long id) {
        SQL sql = new SQL();
        sql.SELECT("*").FROM(tableName).WHERE("id = :id");

        try {
            return (T) template.queryForObject(sql.toString(),
                    new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(this.clazz));
        } catch (EmptyResultDataAccessException ignored) {
        }
        return null;
    }

    @Override
    public <F> F select(String sql, MapSqlParameterSource paramMap, Class<F> otherClazz) {
        try {
            return template.queryForObject(sql, paramMap, otherClazz);
        } catch (EmptyResultDataAccessException ignored) {

        }
        return null;
    }

    @Override
    public List<T> select(Collection<Long> idList) {
        if (CollectionUtils.isEmpty(idList)) {
            return Collections.emptyList();
        }
        SQL sql = new SQL();
        sql.SELECT("*").FROM(tableName).WHERE("id in (:idList)");
        return template.query(sql.toString(),
                new MapSqlParameterSource("idList", idList),
                new BeanPropertyRowMapper<>(this.clazz));
    }

    @Override
    public T select(String sql, MapSqlParameterSource parameterSources) {
        try {
            return (T) template.queryForObject(sql, parameterSources, new BeanPropertyRowMapper<>(this.clazz));
        } catch (EmptyResultDataAccessException e) {
            log.error(e.getMessage());
        }

        return null;
    }

    @Override
    public List<T> queryList(String sql, MapSqlParameterSource parameterSources) {
        return template.query(sql, parameterSources, new BeanPropertyRowMapper<>(this.clazz));
    }

    @Override
    public <F> List<F> queryList(String sql, MapSqlParameterSource parameterSources, Class<F> otherClazz) {
        return template.queryForList(sql, parameterSources, otherClazz);
    }
    
    @Override
    public Boolean existByWhere(String where, Object... args) {
        return countByWhere(where, args) > 0;
    }

    @Override
    public Integer count(String sql, MapSqlParameterSource parameterSources) {
        try {
            return template.queryForObject(sql, parameterSources, Integer.class);
        } catch (EmptyResultDataAccessException ignored) {
        }
        return 0;
    }

    @Override
    public Integer countByWhere(String where, Object... args) {
        SQL sql = new SQL();
        sql.SELECT("count(*)").FROM(tableName).WHERE(where);
        JdbcTemplate jdbcTemplate = this.template.getJdbcTemplate();
        return jdbcTemplate.query(sql.toString(), rs -> rs.next() ? rs.getInt(1) : 0, args);
    }

    /**
     * 返回表名
     */
    protected String tableName() {
        return this.tableName;
    }

    protected NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
        return this.template;
    }

    private Class getCurrentClass() {
        Class childClazz = this.getClass();
        ParameterizedType genericSuperclass = (ParameterizedType) childClazz.getGenericSuperclass();
        return (Class) genericSuperclass.getActualTypeArguments()[0];
    }

    private String getPoTableName() {
        // 取得类头注解
        MySqlTable mySqlTable = (MySqlTable) clazz.getAnnotation(MySqlTable.class);
        if (Objects.isNull(mySqlTable)) {
            throw new DaoException("找不实体类中的@MySqlTable注解 :(");
        }

        String name = mySqlTable.value();
        if (StringUtils.isBlank(name)) {
            throw new DaoException("实体类没有与之映射的表名 :(");
        }

        return name;
    }

    private String buildInsertSql() {
        SQL sql = new SQL();
        sql.INSERT_INTO(tableName);

        Field[] declaredFields = this.clazz.getDeclaredFields();
        for (Field field : declaredFields) {

            String fieldName = field.getName();
            if ("id".equals(fieldName)) {
                continue;
            }

            // 转为下划线
            String lowerFiledName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName);
            sql.INTO_COLUMNS(lowerFiledName)
                    .INTO_VALUES(":" + fieldName);
        }

        return sql.toString();
    }
}
  • 使用示例

创建OrmToolTestDao继承至AbstractDaoSupport,并在泛型中加入实体类。同时需要在实体类头上标注我们创建的注解。代码如下:

/**
 * @Author: Yh
 * @Date: 2019-07-19
 * @Time: 15:12
 * Copyright © Bell All Rights Reserved.
 */
@Data
@MySqlTable("bell_orm_test")
public class OrmToolTestPo {

    private Integer id;

    private String name;

    private String desc;

}

以下是dao的使用示例,此处我使用了MyBatis的动态SQL构建器,以减少SQL写错的概率,同时代码也更加优雅了,有么有(哈哈哈~~~~)。

/**
 * @Author: Yh
 * @Date: 2019-07-19
 * @Time: 15:12
 * Copyright © Bell All Rights Reserved.
 */
@Repository
public class OrmToolTestDao extends AbstractDaoSupport<OrmToolTestPo> {

    public Boolean insert(OrmToolTestPo ormToolTestPo) {
        return super.insert(ormToolTestPo);
    }

    public OrmToolTestPo getById(Integer id) {
        return super.select(id.longValue());
    }

    public Boolean update(OrmToolTestPo ormToolTestPo) {
        return super.updateEscapeNull(ormToolTestPo);
    }

    /**
     * ? 占位符取个数
     */
    public Integer countByName(String name) {
        return super.countByWhere("name = ?", name);
    }

    /**
     * 动态sql拼接 需要注意参数全部为空的情况
     */
    public List<OrmToolTestPo> getList(Integer id, String name, String desc) {
        MapSqlParameterSource params = new MapSqlParameterSource();
        SQL sql = new SQL();
        sql.SELECT("*")
                .FROM(tableName());

        if (Objects.nonNull(id)) {
            sql.WHERE("id = :id");
            params.addValue("id", id);
        }

        if (StringUtils.isNotBlank(name)) {
            sql.WHERE("name like :name");
            params.addValue("name", "%" + name + "%");
        }

        if (StringUtils.isNotBlank(desc)) {
            sql.WHERE("desc like :desc");
            params.addValue("desc", "%" + desc + "%");
        }
        return super.queryList(sql.toString(), params);
    }
}

三 总结

至此,我们完成了对NamedParameterJdbcTemplate的简单的封装,给平常的开发带来了很大的便利,同时使用了Mybatis的动态SQL构建器,不仅让写SQL的错误大大降低,同时也去除了像Mybatis、Hibernate编写繁琐的xml文件。在本篇博客中,由于篇幅有限,仅仅实现了最简单的封装,如果想要将上诉的短板中的所有特性都实现,当前环境下是讲述不清的,你可以私下自行尝试实现下,说不定你也能写出一个优秀的ORM框架。

由于小编技术能力有限,文中难免有纰漏之处,还望指正,谢谢合作!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章