使用Spring-JDBC自定义一款简单ORM
Posted YQS_Love
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用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框架。
由于小编技术能力有限,文中难免有纰漏之处,还望指正,谢谢合作!
以上是关于使用Spring-JDBC自定义一款简单ORM的主要内容,如果未能解决你的问题,请参考以下文章