数据库|除了Mybatis,我们还能用什么访问数据库


目录

  • 为什么我很讨厌Mybatis?
  • 除了Mybatis,我们还能用什么
  • Spring-JDBC
  • 基于Spring-JDBC的实践
      • SQL组装工具
      • 分页插件
      • 基础查询层
      • 实例

为什么我很讨厌Mybatis?
  • 我很讨厌在XML里写SQL,每次接手一个用Mybatis开发的项目,需要调试或者修改SQL的时候都要多走一步,从dao层找对应的mapper文件,我觉得挺傻的,虽然现在mybtais可以基于全注解来开发,但是灵活性和SQL编写的舒适性都有很大程度下降
  • 很难调试,本地启动的情况下没法断点调试,需要开启打印SQL通过控制台来调试SQL
  • SQL真的很难写,这条是我个人的主观感受,虽然Mybatis提供了动态SQL的功能,但还是很难写,在XML里面要用到各种标签,加一个条件不光要在xml里面加,还需要跳到java代码
除了Mybatis,我们还能用什么 公司的项目没办法,统一和标准压过一切,现在阿里基本上用的都是mybatis或者更老的ibatis,个人只能服从组织安排。但在开发私人项目时其实可以有更多的选择,比如我个人做一些小demo或者小系统时都会用Spring-JDBC。
Spring-JDBC Spring-JDBC是一个非常轻量级的数据库访问框架,基本上只有管理数据库连接(连接池配置),SQL执行等功能,但是它本身又非常灵活,提供了可以实现类似于JPA的数据映射扩展接口RowMapper,至少就我感觉,在 经过一定的封装之后,Spring-JDBC的使用体验比mybatis好很多,而且用Spring开发的web项目使用Spring-JDBC,听起来就很爽对吧
基于Spring-JDBC的实践 我个人是基于Spring-JDBC封装了一个二方包,集成了自动分页,分库分表,SQL组装等功能,接下来给大家做一个基本的介绍
SQL组装工具
其实很多人认为Mybatis的一个大优势就是可以写比较灵活的SQL,但这个优点其实只能在和SpringDataJPA或者hibernate这种JPA框架对比时才能成立,无论你在XML里面无论有多灵活,也不如直接在代码里写SQL来的灵活。但是写SQL,但是灵活只是一方面,更重要的是如何保证可读性,mybatis其实是兼顾了两者的,毕竟你在代码里哗哗写一大堆字符串拼接,碳基生物基本上都不太能看懂,我是开发了一个工具类SqlBuilder用于在代码中书写SQL
public final class SqlBuilder { private StringBuilder sqlBuilder; private LinkedList params; private SqlBuilder(String initSql) { this.sqlBuilder = new StringBuilder(initSql); this.params = new LinkedList<>(); }public static SqlBuilder init(String str){ return new SqlBuilder(str); }public String getSql(){ return sqlBuilder.toString(); }public Object[] getParamArray(){ return params.toArray(); }public SqlBuilder joinDirect(String sql, Object param){ sqlBuilder.append(" ").append(sql); params.add(param); return this; }public SqlBuilder joinDirect(String sql){ sqlBuilder.append(" ").append(sql); return this; }public SqlBuilder join(String sql, Object param){ if(param != null){ sqlBuilder.append(" ").append(sql); params.add(param); } return this; }public SqlBuilder joinLikeBefore(String sql, Object param){ if(param != null){ sqlBuilder.append(" ").append(sql).append(" like? "); params.add("%" + param); } return this; }public SqlBuilder joinLikeAfter(String sql, Object param){ if(param != null){ sqlBuilder.append(" ").append(sql).append(" like? "); params.add(param + "%"); } return this; } public SqlBuilder joinLikeAround(String sql, Object param){ if(param != null){ sqlBuilder.append(" ").append(sql).append(" like? "); params.add("%" + param + "%"); } return this; }public SqlBuilder joinIn(String sql, Object[] paramArray){ if(paramArray.length > 0){ sqlBuilder.append(" ").append(sql).append(" in(").append(StringUtil.getEmptyParams(paramArray.length)).append(")"); params.addAll(Arrays.stream(paramArray).collect(Collectors.toList())); } return this; }public SqlBuilder joinIn(String sql, Collection paramArray){ if(paramArray.size() > 0){ sqlBuilder.append(" ").append(sql).append(" in(").append(StringUtil.getEmptyParams(paramArray.size())).append(")"); params.addAll(paramArray); } return this; } }
【数据库|除了Mybatis,我们还能用什么访问数据库】其实代码非常简单,两个成员变量,sqlBuilder用于保存用户的SQL,params用于保存用户的参数,同时提供了常用的SQL拼接工具方法,比如join方法会在传入参数不为空时拼接一个查询条件到SQL中,joinIn会拼接一个in查询条件,下面是一个使用例子
SqlBuilder sqlBuilder = SqlBuilder.init("select * from tb_book_flow"); sqlBuilder.joinDirect("where 1=1") .join("and id=?", condition.getId()) .join("and book_name=?", condition.getBookName()) .join("and status", condition.getStatus());

分页插件
实现分页插件一个核心的点是需要可扩展,即针对不同的的数据库类型要能做到适配(至少oracle和mysql的分页语句是不同的),我这里主要是基于接口和工厂模式来实现的,接口如下
/** * 分页插件 */ public interface PaginationSupport { /** * 用于将指定SQL加工为带分页的SQL * @param sql * @param page * @param * @return */ public String getPaginationSql(String sql, Page page); /** * 获取计数SQL * @param sql * @return */ public String getCountSql(String sql); /** * 当前SQL插件是否支持指定数据库类型 * @param dbType * @return */ public boolean support(String dbType); }

工厂类如下
public class PaginationSupportFactory { private static final Set paginationSupports = new HashSet<>(); static { addPaginationSupport(new MysqlPaginationSupport()); }public static void addPaginationSupport(PaginationSupport paginationSupport){ paginationSupports.add(paginationSupport); }public static PaginationSupport getSuitableSupport(String dbType){ return paginationSupports.stream() .filter(paginationSupport -> paginationSupport.support(dbType)) .findAny() .orElseThrow(() -> new IllegalArgumentException("不支持的数据库类型:" + dbType)); } }

最后我们提供一个默认实现,Mysql的分页插件(代码是19年写的,当时我还没进入阿里,所以代码中其实存在一些违反阿里代码规约的地方,有人能找出来吗?
public class MysqlPaginationSupport implements PaginationSupport { @Override public String getPaginationSql(String sql, Page page) { long startIndex = (page.getPageNo() - 1) * page.getPageSize(); long endIndex = startIndex + page.getPageSize(); StringBuilder sqlBuilder = new StringBuilder(sql); if(!StringUtil.isEmpty(page.getSortBy())){ sqlBuilder.append(" ORDER BY ").append(page.getSortBy()).append(page.getRank()); } sqlBuilder.append(" limit ").append(startIndex).append(",").append(endIndex); return sqlBuilder.toString().toLowerCase(); }@Override public String getCountSql(String sql) { return ("SELECT COUNT(*) FROM (" + sql + ") A").toLowerCase(); }@Override public boolean support(String dbType) { return dbType.equalsIgnoreCase("MYSQL"); } }

基础查询层
前面说过,Spring-JDBC是一个非常轻量级的框架,本身提供的功能很少,为了能更舒服的做数据库开发,我做了一个基础的查询类,提供了一些类似JPA的数据映射能力
@Validated public abstract class BaseDao{@Autowired protected JdbcTemplate jdbcTemplate; private String dbType; /** * 分页查询 * @param page * @param modelClass * @param sql * @param args * @param * @return */ public Page queryForPaginationBean(@NotNull Page page, @NotNull Class modelClass, @NotBlank String sql, @NotNull Object[] args){ PaginationSupport paginationSupport = null; try { paginationSupport = getSuitablePaginationSupport(); } catch (SQLException e) { throw new UnsupportedOperationException(e); } int total = this.count(paginationSupport.getCountSql(sql), args); page.setTotal(total); List data = https://www.it610.com/article/this.jdbcTemplate.query(paginationSupport.getPaginationSql(sql, page), new BeanPropertyRowMapper<>(modelClass), args); page.setData(data); return page; }/** * 分页查询 * @param page * @param modelClass * @param sqlBuilder * @param * @return */ public Page queryForPaginationBean(@NotNull Page page, @NotNull Class modelClass, @NotNull SqlBuilder sqlBuilder){ return queryForPaginationBean(page, modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray()); }/** * 快速查询全部 * @param modelClass * @param sql * @param args * @param * @return */ public List queryForAllBean(@NotNull Class modelClass, @NotBlank String sql, @NotNull Object[] args){ return this.jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(modelClass), args); }/** * 快速查询全部 * @param modelClass * @param sqlBuilder * @param * @return */ public List queryForAllBean(@NotNull Class modelClass, @NotNull SqlBuilder sqlBuilder){ return this.queryForAllBean(modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray()); }/** * 快速查询首个Bean * @param modelClass * @param sql * @param args * @param * @return */ public Optional findFirstBean(@NotNull Class modelClass, @NotBlank String sql, @NotNull Object[] args){ T t; try { t = this.jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(modelClass), args); }catch (EmptyResultDataAccessException e){ t = null; } return Optional.ofNullable(t); } public Optional findFirstBean(Class modelClass, SqlBuilder sqlBuilder){ return findFirstBean(modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray()); }/** * 快速分页查询Map * @param page * @param sql * @param args * @return */ public Page queryForPaginationMap(@NotNull Page page, @NotBlank String sql, @NotNull Object[] args) { PaginationSupport paginationSupport = null; try { paginationSupport = getSuitablePaginationSupport(); } catch (SQLException e) { throw new UnsupportedOperationException(e); } int total = this.count(paginationSupport.getCountSql(sql), args); page.setTotal(total); List data = https://www.it610.com/article/this.jdbcTemplate.queryForList(paginationSupport.getPaginationSql(sql, page), args); page.setData(data); return page; }/** * 根据SqlBuilder快速分页查询接口 * @param page * @param sqlBuilder * @return */ public Page queryForPaginationMap(@NotNull Page page, @NotNull SqlBuilder sqlBuilder){ return queryForPaginationMap(page, sqlBuilder.getSql(), sqlBuilder.getParamArray()); }/** * 根据SqlBuilder快速更新 * @param sqlBuilder * @return */ public int update(@NotNull SqlBuilder sqlBuilder){ return this.update(sqlBuilder.getSql(), sqlBuilder.getParamArray()); }/** * 根据SQL进行更新 * @param sql * @param args * @return */ public int update(@NotBlank String sql, @NotNull Object[] args){ return jdbcTemplate.update(sql, args); }/** * 快速更新javabean对象 * @param column 数据库字段名称 * @param bean * @param table * @param * @return */ public int updateBean(@NotBlank String column, @NotNull T bean, @NotBlank String table){ Map parameterMap = BeanUtils.transBeanToMapWithUnderScore(bean); Object selectValue = https://www.it610.com/article/parameterMap.remove(column); List params = new ArrayList<>(parameterMap.values()); params.add(selectValue); String sql = getUpdateSql(parameterMap, table, column); return this.jdbcTemplate.update(sql, params.toArray()); }/** * 快速根据ID进行修改 * @param bean * @param table * @param * @return */ public int updateById(@NotNull T bean, String table){ return updateBean("id", bean, table); }/** * 快速根据字段删除 * @param column * @param value * @param table * @return */ public int remove(@NotBlank String column, @NotNull Object value, @NotBlank String table){ String sql = "delete from " + table + " where " + column + " =?"; return this.jdbcTemplate.update(sql, value); }/** * 快速根据字段批量删除 * @param column * @param valueArray * @param table * @return */ public int remove(@NotBlank String column, @NotEmpty Object[] valueArray, @NotBlank String table){ String sql = "delete from " + table + " where " + column + " in(" + StringUtil.getEmptyParams(valueArray.length) + ")"; return this.jdbcTemplate.update(sql, valueArray); }/** * 快速根据ID删除 * @param id * @param table * @return */ public int removeById(@NotNull Object id, @NotBlank String table){ return remove("id", id, table); }/** * 快速根据ID批量删除 * @param array * @param table * @return */ public int removeByIdArray(@NotEmpty Object[] array, @NotBlank String table){ return remove("id", array, table); } /** * 保存单个对象 * @param bean * @param table * @param */ public void saveBean(T bean, String table){ Map parameterMap = BeanUtils.transBeanToMapWithUnderScore(bean); String sql = this.getSaveSql(parameterMap, table); this.jdbcTemplate.update(sql, parameterMap.values().toArray()); }/** * 批量保存javaBean对象,使用NoConvertField标注的字段不会保存 * @param beans * @param table * @param */ public void saveBeanList(List beans, String table){ if(beans.size() <= 0){ return; } String sql = null; List batchArgs = new LinkedList<>(); for(T bean : beans) { Map beanPropertyMap = BeanUtils.transBeanToMapWithUnderScore(bean); if (sql == null) { sql = getSaveSql(beanPropertyMap, table); } batchArgs.add(beanPropertyMap.values().toArray()); } this.jdbcTemplate.batchUpdate(sql, batchArgs); }/** * 快速根据主健ID(id)进行单条记录查询 * @param id * @param modelClass * @param table * @param * @return */ public Optional findById(Integer id, Class modelClass, String table){ return findBy("id", modelClass, table, id); }/** * 根据数据库表指定的一个字段进行单条记录查询 * @param key * @param modelClass * @param table * @param value * @param * @return */ public Optional findBy(String key, Class modelClass, String table, Object value){ T t; try { t = this.jdbcTemplate.queryForObject("select * from " + table + " where " + key + "=?", BeanPropertyRowMapper.newInstance(modelClass), value); }catch (EmptyResultDataAccessException e){ t = null; } return Optional.ofNullable(t); }/** * 根据占位符sql和入参进行计数 * @param sql * @param args * @return */ public Integer count(String sql, Object[] args){ return this.jdbcTemplate.queryForObject(sql, Integer.class, args); }/** * 根据SQLBuilder进行计数 * @param sqlBuilder * @return */ public Integer count(SqlBuilder sqlBuilder){ AssertUtil.assertNotNull(sqlBuilder, "count sqlBuilder cannot be null"); return this.jdbcTemplate.queryForObject(sqlBuilder.getSql(), Integer.class, sqlBuilder.getParamArray()); }/** * 简单执行SQL并计数 * @param sql * @return */ public Integer count(String sql){ return this.jdbcTemplate.queryForObject(sql, Integer.class); }/** * 获取当前适用的分页插件 * @return * @throws SQLException */ private PaginationSupport getSuitablePaginationSupport() throws SQLException { try { return PaginationSupportFactory.getSuitableSupport(this.getCurrentDbType()); } catch (SQLException e) { throw new SQLException("数据库分页查询失败,无法获取当前数据库类型:" + jdbcTemplate.getDataSource(), e); } }/** * 获取当前数据库类型 * @return * @throws SQLException */ private String getCurrentDbType() throws SQLException { if(StringUtil.isNotEmpty(dbType)){ return dbType; } synchronized (this){ dbType = this.jdbcTemplate.execute((ConnectionCallback) connection -> connection.getMetaData().getDatabaseProductName()); } return dbType; }/** * 获取更新bean的SQL * @param beanPropertyMap * @param table * @param column * @return */ private String getUpdateSql(Map beanPropertyMap, String table, String column){ AssertUtil.assertTrue(beanPropertyMap.containsKey(column), String.format("column:%s not exist in bean", column)); StringBuilder updateSqlBuilder = new StringBuilder("UPDATE ").append(table); boolean isFirstParam = true; for(String key : beanPropertyMap.keySet()){ if(key.equals(column)){ continue; } if (isFirstParam) { updateSqlBuilder.append(" SET "); isFirstParam = false; } else { updateSqlBuilder.append(","); } updateSqlBuilder.append(key).append("=").append("?"); } updateSqlBuilder.append(" WHERE ").append(column).append("=?"); return updateSqlBuilder.toString(); }/** * 生成保存bean的SQL * @param beanPropertyMap * @param table * @return */ private String getSaveSql(Map beanPropertyMap, String table){ return "INSERT INTO " + table + "(" + StringUtil.join(new ArrayList<>(beanPropertyMap.keySet()), ",") + ")" + "VALUES" + "(" + StringUtil.getEmptyParams(beanPropertyMap.size()) + ")"; } }
有一个非常重要的点是在做分页查询时我们需要获取当前连接的数据库类型(不同的数据库类型分页语句是不同的),我一开始是直接调的DataSourcegetConnection方法,结果发现每次做一次分页查询都会建立一个连接,查询几次之后数据库连接都超时了,我仔细看了看文档才发现这个方法不是获取已有的数据库连接而是开启一个新连接,修改之后问题才解决
其实到这一步为止,从易用性上讲我认为已经超过Mybatis了,基于这套封装的框架,我们既可以实现类似于JPA框架一样的快速基于JavaBean开发,也可以基于灵活的SQL进行开发,但有个点其实不够优雅,就是在实现类似JPA的操作时候我们需要传表名,所以我加了一个注解和一个扩展查询类,用于实现表的内聚
@Retention(RetentionPolicy.RUNTIME) public @interface DaoMapping { //逻辑表 String logicTable(); //是否开启分库分表 boolean sharding() default false; //用户分库分表的字段 String shardingColumn() default "id"; //可映射的表,若为default,映射到逻辑表 String actualTable() default "default"; //可映射的数据源,若为default,映射到主数据源 String actualDataSource() default "default"; //分表算法 Class tablePreciseShardingAlgorithm() default ModShardingAlgorithm.class; //分表算法 Class tableRangeShardingAlgorithm() default ModShardingAlgorithm.class; //分库算法 Class dbPreciseShardingAlgorithm() default DefaultShardingAlgorithm.class; //分库算法 Class dbRangeShardingAlgorithm() default DefaultShardingAlgorithm.class; }

如图,DaoMapping是一个用于标识数据库表映射的注解,它有几个功能,标识当前Dao要操作的数据表,实现分库分表(这个功能下期有时间再说),在扩展查询类中,我们会获取当前类上的注解并注入表名,实现更方便的查询
@Validated public abstract class BaseMappingDao extends BaseDao{/** * 快速根据ID查询 * @param id * @param modelClass 返回值类型 * @param * @return */ public Optional findById(@NotNull Integer id, @NotNull Class modelClass){ return this.findById(id, modelClass, this.getTable()); }/** * 快速根据单个字段查询 * @param key 字段名称 * @param value 字段值 * @param modelClass 返回值类型 * @param * @return */ public Optional findBy(@NotBlank String key, @NotNull Object value, @NotNull Class modelClass){ return this.findBy(key, modelClass, this.getTable(), value); }/** * 快速根据单个字段更新bean * @param column * @param bean * @param * @return */ public int updateBean(@NotBlank String column, @ NotNull T bean){ return this.updateBean(column, bean, this.getTable()); }/** * 快速根据ID更新数据库 * @param bean 必须包含id字段 * @param * @return */ public int updateById(@NotNull T bean){ return this.updateById(bean, this.getTable()); }public void saveBean(@NotNull T bean){ this.saveBean(bean, getTable()); }public void saveBeanList(@NotEmpty List beanList){ this.saveBeanList(beanList, getTable()); }public void removeBy(@NotBlank String column, @NotNull Object value){ this.remove(column, value, this.getTable()); }public void removeById(@NotNull Object value){ this.removeBy("id", value); }public Optional getThisDaoMapping(){ return Optional.ofNullable(this.getClass().getAnnotation(DaoMapping.class)); }public String getTable(){ return getThisDaoMapping().map(DaoMapping::logicTable).orElseThrow(() -> new UnsupportedOperationException("缺少DaoMapping注解的类不支持该类查询")); } }

实例
用参加百技时我们项目实战的代码举个例子
@Component @DaoMapping(logicTable = "tb_book_flow") public class BookDao extends BaseMappingDao {public List findAllBook(){ SqlBuilder sqlBuilder = SqlBuilder.init("select * from tb_book_flow"); return queryForAllBean(Book.class, sqlBuilder); }public Page queryForPage(BookQueryCondition condition, Page page){ SqlBuilder sqlBuilder = SqlBuilder.init("select * from tb_book_flow"); sqlBuilder.joinDirect("where 1=1") .join("and id=?", condition.getId()) .join("and book_name=?", condition.getBookName()) .join("and status=?", condition.getStatus()); return queryForPaginationBean(page, Book.class, sqlBuilder); }public void insertBook(Book book){ saveBean(book); }public void updateBook(Book book) { updateBook(book); } }

    推荐阅读