公司的项目没办法,统一和标准压过一切,现在阿里基本上用的都是mybatis或者更老的ibatis,个人只能服从组织安排。但在开发私人项目时其实可以有更多的选择,比如我个人做一些小demo或者小系统时都会用Spring-JDBC。
Spring-JDBC是一个非常轻量级的数据库访问框架,基本上只有管理数据库连接(连接池配置),SQL执行等功能,但是它本身又非常灵活,提供了可以实现类似于JPA的数据映射扩展接口RowMapper
,至少就我感觉,在 经过一定的封装之后,Spring-JDBC的使用体验比mybatis好很多,而且用Spring开发的web项目使用Spring-JDBC,听起来就很爽对吧
我个人是基于Spring-JDBC封装了一个二方包,集成了自动分页,分库分表,SQL组装等功能,接下来给大家做一个基本的介绍
其实很多人认为Mybatis的一个大优势就是可以写比较灵活的SQL,但这个优点其实只能在和SpringDataJPA或者hibernate这种JPA框架对比时才能成立,无论你在XML里面无论有多灵活,也不如直接在代码里写SQL来的灵活。但是写SQL,但是灵活只是一方面,更重要的是如何保证可读性,mybatis其实是兼顾了两者的,毕竟你在代码里哗哗写一大堆字符串拼接,碳基生物基本上都不太能看懂,我是开发了一个工具类SqlBuilder
用于在代码中书写SQL
public final class SqlBuilder {
private StringBuilder sqlBuilder;
private LinkedList
其实代码非常简单,两个成员变量,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 = 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
有一个非常重要的点是在做分页查询时我们需要获取当前连接的数据库类型(不同的数据库类型分页语句是不同的),我一开始是直接调的DataSource
的getConnection
方法,结果发现每次做一次分页查询都会建立一个连接,查询几次之后数据库连接都超时了,我仔细看了看文档才发现这个方法不是获取已有的数据库连接而是开启一个新连接,修改之后问题才解决
其实到这一步为止,从易用性上讲我认为已经超过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 extends PreciseShardingAlgorithm> tablePreciseShardingAlgorithm() default ModShardingAlgorithm.class;
//分表算法
Class extends RangeShardingAlgorithm> tableRangeShardingAlgorithm() default ModShardingAlgorithm.class;
//分库算法
Class extends PreciseShardingAlgorithm> dbPreciseShardingAlgorithm() default DefaultShardingAlgorithm.class;
//分库算法
Class extends RangeShardingAlgorithm> 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);
}
}