Besides Mybatis, what else can we use to access the database

Why do I hate Mybatis so much?

  • I hate writing SQL in XML. Every time I take over a project developed with Mybatis, I have to go one step further when I need to debug or modify SQL. I think it's silly to find the corresponding mapper file from the dao layer, although mybtais is now It can be developed based on full annotations, but the flexibility and comfort of SQL writing are greatly reduced
  • It is difficult to debug. There is no breakpoint debugging in the case of local startup. You need to turn on printing SQL to debug SQL through the console.
  • SQL is really difficult to write. This is my personal subjective feeling. Although Mybatis provides the function of dynamic SQL, it is still difficult to write. Various tags are used in XML. Adding a condition not only requires adding a condition to the xml , also need to jump to java code

Besides Mybatis, what else can we use

There is no way for the company's projects. Unification and standards overwhelm everything. Now Ali basically uses mybatis or older ibatis, and individuals can only obey the organization's arrangements. But there are actually more options when developing private projects. For example, I personally use Spring-JDBC when doing some small demos or small systems.

Spring-JDBC

Spring-JDBC is a very lightweight database access framework that basically only manages database connections (connection pool configuration), SQL execution and other functions, but it is very flexible and provides a data mapping extension interface similar to JPA. RowMapper, at least I feel that after a certain encapsulation, the experience of using Spring-JDBC is much better than that of mybatis, and using Spring-JDBC for web projects developed with Spring sounds very cool, right?

Practice based on Spring-JDBC

I personally encapsulate a second-party package based on Spring-JDBC, which integrates automatic paging, sub-database sub-table, SQL assembly and other functions. Next, I will give you a basic introduction.

SQL Assembly Tool

In fact, many people think that a big advantage of Mybatis is that it can write more flexible SQL, but this advantage can only be established when compared with JPA frameworks such as Spring Data JPA or hibernate, no matter how flexible you are in XML, it is better to directly Flexibility to write SQL in the code. But writing SQL, flexibility is only one aspect, and the more important thing is how to ensure readability. Mybatis actually takes both into account. After all, you write a lot of string splicing in the code, and carbon-based creatures are basically all I don't understand very well, I developed a tool class SqlBuilderfor writing SQL in the code

public final class SqlBuilder {
    private StringBuilder sqlBuilder;

    private LinkedList<Object> 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;
    }
}

In fact, the code is very simple. Two member variables are sqlBuilderused to save the user's SQL and the paramsuser's parameters. At the same time, common SQL splicing tool methods are provided. For example, the joinmethod will splicing a query condition when the incoming parameter is not empty. In SQL, joinInan in query condition will be spliced, and the following is an example of use

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());

Pagination plugin

A core point of implementing a paging plug-in is that it needs to be extensible, that is, it can be adapted for different database types (at least the paging statements of oracle and mysql are different). I mainly implement it based on the interface and factory mode. , the interface is as follows

/**
 * 分页插件
 */
public interface PaginationSupport {
    /**
     * 用于将指定SQL加工为带分页的SQL
     * @param sql
     * @param page
     * @param <T>
     * @return
     */
    public <T> String getPaginationSql(String sql, Page<T> page);

    /**
     * 获取计数SQL
     * @param sql
     * @return
     */
    public String getCountSql(String sql);

    /**
     * 当前SQL插件是否支持指定数据库类型
     * @param dbType
     * @return
     */
    public boolean support(String dbType);
}

The factory class is as follows

public class PaginationSupportFactory {
    private static final Set<PaginationSupport> 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));
    }
}

Finally, we provide a default implementation, Mysql's paging plug-in (the code was written in 2019, and I hadn't entered Ali yet, so there are actually some violations of Ali's code regulations in the code. Can anyone find it?

public class MysqlPaginationSupport implements PaginationSupport {
    @Override
    public <T> String getPaginationSql(String sql, Page<T> 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");
    }
}

Basic query layer

As mentioned earlier, Spring-JDBC is a very lightweight framework that provides few functions. In order to make database development more comfortable, I made a basic query class that provides some JPA-like data mapping capabilities.

@Validated
public abstract class BaseDao{

    @Autowired
    protected JdbcTemplate jdbcTemplate;

    private String dbType;

    /**
     * 分页查询
     * @param page
     * @param modelClass
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T> Page<T> queryForPaginationBean(@NotNull Page<T> page, @NotNull Class<T> 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<T> data = this.jdbcTemplate.query(paginationSupport.getPaginationSql(sql, page), new BeanPropertyRowMapper<>(modelClass), args);
        page.setData(data);
        return page;
    }

    /**
     * 分页查询
     * @param page
     * @param modelClass
     * @param sqlBuilder
     * @param <T>
     * @return
     */
    public <T> Page<T> queryForPaginationBean(@NotNull Page<T> page, @NotNull Class<T> modelClass, @NotNull SqlBuilder sqlBuilder){
        return queryForPaginationBean(page, modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray());
    }

    /**
     * 快速查询全部
     * @param modelClass
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T> List<T> queryForAllBean(@NotNull Class<T> modelClass, @NotBlank String sql, @NotNull Object[] args){
        return this.jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(modelClass), args);
    }

    /**
     * 快速查询全部
     * @param modelClass
     * @param sqlBuilder
     * @param <T>
     * @return
     */
    public <T> List<T> queryForAllBean(@NotNull Class<T> modelClass, @NotNull SqlBuilder sqlBuilder){
        return this.queryForAllBean(modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray());
    }

    /**
     * 快速查询首个Bean
     * @param modelClass
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T> Optional<T> findFirstBean(@NotNull Class<T> 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 <T> Optional<T> findFirstBean(Class<T> modelClass, SqlBuilder sqlBuilder){
        return findFirstBean(modelClass, sqlBuilder.getSql(), sqlBuilder.getParamArray());
    }

    /**
     * 快速分页查询Map
     * @param page
     * @param sql
     * @param args
     * @return
     */
    public Page<Map<String, Object>> queryForPaginationMap(@NotNull Page<Map<String, Object>> 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<Map<String, Object>> data = this.jdbcTemplate.queryForList(paginationSupport.getPaginationSql(sql, page), args);
        page.setData(data);
        return page;
    }

    /**
     * 根据SqlBuilder快速分页查询接口
     * @param page
     * @param sqlBuilder
     * @return
     */
    public Page<Map<String, Object>> queryForPaginationMap(@NotNull Page<Map<String, Object>> 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 <T>
     * @return
     */
    public <T> int updateBean(@NotBlank String column, @NotNull T bean, @NotBlank String table){
        Map<String, Object> parameterMap = BeanUtils.transBeanToMapWithUnderScore(bean);
        Object selectValue = parameterMap.remove(column);

        List<Object> 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 <T>
     * @return
     */
    public <T> 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 <T>
     */
    public <T> void saveBean(T bean, String table){
        Map<String, Object> parameterMap = BeanUtils.transBeanToMapWithUnderScore(bean);
        String sql = this.getSaveSql(parameterMap, table);
        this.jdbcTemplate.update(sql, parameterMap.values().toArray());
    }

    /**
     * 批量保存javaBean对象,使用NoConvertField标注的字段不会保存
     * @param beans
     * @param table
     * @param <T>
     */
    public <T> void saveBeanList(List<T> beans, String table){
        if(beans.size() <= 0){
            return;
        }
        String sql = null;
        List<Object[]> batchArgs = new LinkedList<>();
        for(T bean : beans) {
            Map<String, Object> 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 <T>
     * @return
     */
    public <T> Optional<T> findById(Integer id, Class<T> modelClass, String table){
        return findBy("id", modelClass, table, id);
    }

    /**
     * 根据数据库表指定的一个字段进行单条记录查询
     * @param key
     * @param modelClass
     * @param table
     * @param value
     * @param <T>
     * @return
     */
    public <T> Optional<T> findBy(String key, Class<T> 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<String>) connection ->
                    connection.getMetaData().getDatabaseProductName());
        }
        return dbType;
    }

    /**
     * 获取更新bean的SQL
     * @param beanPropertyMap
     * @param table
     * @param column
     * @return
     */
    private String getUpdateSql(Map<String, Object> 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<String, Object> beanPropertyMap, String table){
        return "INSERT INTO " +
                table +
                "(" +
                StringUtil.join(new ArrayList<>(beanPropertyMap.keySet()), ",") +
                ")" +
                "VALUES" +
                "(" +
                StringUtil.getEmptyParams(beanPropertyMap.size()) +
                ")";
    }
}

A very important point is that we need to get the database type of the current connection when doing paging query (different database types have different paging statements). I used DataSourcethe getConnectionmethod directly at first, but found that every time I do a paging query A connection will be established. After several queries, the database connection times out. I carefully looked at the document and found that this method is not to obtain an existing database connection but to open a new connection. The problem is solved after modification.

In fact, up to this point, I think it has surpassed Mybatis in terms of ease of use. Based on this encapsulated framework, we can not only achieve rapid JavaBean-based development similar to the JPA framework, but also develop based on flexible SQL. But there is one point that is not elegant enough, that is, we need to pass the table name when implementing JPA-like operations, so I added an annotation and an extended query class to achieve table cohesion

@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;
}

As shown in the figure, DaoMapping is an annotation used to identify database table mapping. It has several functions, which identify the data table to be operated by the current Dao, and implement sub-database and sub-table (this function will be discussed in the next issue). In the extended query class, We will get the annotations on the current class and inject the table name to achieve a more convenient query

@Validated
public abstract class BaseMappingDao extends BaseDao{

    /**
     * 快速根据ID查询
     * @param id
     * @param modelClass 返回值类型
     * @param <T>
     * @return
     */
    public <T> Optional<T> findById(@NotNull Integer id, @NotNull Class<T> modelClass){
        return this.findById(id, modelClass, this.getTable());
    }

    /**
     * 快速根据单个字段查询
     * @param key 字段名称
     * @param value 字段值
     * @param modelClass 返回值类型
     * @param <T>
     * @return
     */
    public <T> Optional<T> findBy(@NotBlank String key, @NotNull Object value, @NotNull Class<T> modelClass){
        return this.findBy(key, modelClass, this.getTable(), value);
    }

    /**
     * 快速根据单个字段更新bean
     * @param column
     * @param bean
     * @param <T>
     * @return
     */
    public <T> int updateBean(@NotBlank String column, @ NotNull T bean){
        return this.updateBean(column, bean, this.getTable());
    }

    /**
     * 快速根据ID更新数据库
     * @param bean 必须包含id字段
     * @param <T>
     * @return
     */
    public <T> int updateById(@NotNull T bean){
        return this.updateById(bean, this.getTable());
    }

    public <T> void saveBean(@NotNull T bean){
        this.saveBean(bean, getTable());
    }

    public <T> void saveBeanList(@NotEmpty List<T> 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<DaoMapping> getThisDaoMapping(){
        return Optional.ofNullable(this.getClass().getAnnotation(DaoMapping.class));
    }

    public String getTable(){
        return getThisDaoMapping().map(DaoMapping::logicTable).orElseThrow(() -> new UnsupportedOperationException("缺少DaoMapping注解的类不支持该类查询"));
    }
}

example

Take the actual code of our project when participating in Baiji as an example

@Component
@DaoMapping(logicTable = "tb_book_flow")
public class BookDao extends BaseMappingDao {

    public List<Book> findAllBook(){
        SqlBuilder sqlBuilder = SqlBuilder.init("select * from tb_book_flow");
        return queryForAllBean(Book.class, sqlBuilder);
    }

    public Page<Book> queryForPage(BookQueryCondition condition, Page<Book> 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);
    }
}

Guess you like

Origin blog.csdn.net/qq_35488769/article/details/121029016