使用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框架。

由於小編技術能力有限,文中難免有紕漏之處,還望指正,謝謝合作!

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