一 概述
目前市面上有很多主流的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框架。
由於小編技術能力有限,文中難免有紕漏之處,還望指正,謝謝合作!