前言

几乎每个系统都需要单表的基础操作(即增删改查分页查询等),如果不使用通用的Mapper则需要每个mapper中都需要实现对应的重复方法,虽然mybatis逆向生成工具会生成对应的.xml文件。里面已经含有一些通用的方法,但是每个实体对应一个.xml文件太复杂。而mybatis也支持注解方式实现sql,使用注解方式实现sql方式,个人感觉更简洁,也符合减少配置文件的趋势。如springboot都在简化配置文件。

通用Mapper

通用Mapper就是为了解决单表增删改查,基于Mybatis Provider机制实现。开发人员不需要编写SQL,不需要在DAO中增加方法,不需要引入其他多余的第三方框架。只要写好实体类,就能支持相应的增删改查方法。

mybatis注解方式实现sql编写

/**
*注解方式实现sql
*/
public interface UserMapper{
	@Select("SELECT id, name FROM tb_user WHERE user_id=#{userId}")
	User selectById(Integer userId);
}

以上就是通过mybatis注解方式实现sql语句调用,是不是看上去就显得更简洁。

BaseMapper代码如下:

/**
 * meilin.huang
 * @param <Entity>
 */
public interface BaseMapper<Entity> {

    /**
     * 插入新对象,并返回主键id值
     * @param entity 实体对象
     * @return
     */
    @InsertProvider(type = InsertSqlProvider.class, method = "sql")
    @Options(useGeneratedKeys = true)
    Integer insert(Entity entity);

//    @InsertProvider(type = BatchInsertSqlProvider.class, method = "sql")
//    @Options(useGeneratedKeys = true)
//    Integer batchInsert(List<Entity> entities);

    /**
     * 根据主键id更新实体,若实体field为null,则对应数据库的字段也更新为null
     * @param entity
     * @return
     */
    @UpdateProvider(type = UpdateSqlProvider.class, method = "sql")
    Integer updateByPrimaryKey(Entity entity);

    /**
     * 根据主键id更新实体,若实体field为null,则对应数据库的字段不更新
     * @param entity
     * @return
     */
    @UpdateProvider(type = UpdateSelectiveSqlProvider.class, method = "sql")
    Integer updateByPrimaryKeySelective(Entity entity);

    @DeleteProvider(type = DeleteSqlProvider.class, method = "sql")
    Integer deleteByPrimaryKey(Integer id);

    @DeleteProvider(type = DeleteByCriteriaSqlProvider.class, method = "sql")
    Integer deleteByCriteria(Entity criteria);

    @SelectProvider(type = SelectOneSqlProvider.class, method = "sql")
    Entity selectByPrimaryKey(Integer id);

    @SelectProvider(type = SelectAllSqlProvider.class, method = "sql")
    List<Entity> selectAll(String orderBy);

    @SelectProvider(type = SelectByCriteriaSqlProvider.class, method = "sql")
    List<Entity> selectByCriteria(Entity criteria);

    /**
     * 根据条件查询单个数据
     *
     * @param criteria
     * @return
     */
    @SelectProvider(type = SelectByCriteriaSqlProvider.class, method = "sql")
    Entity selectOneByCriteria(Entity criteria);

    @SelectProvider(type = CountSqlProvider.class, method = "sql")
    Long count();

    @SelectProvider(type = CountByCriteriaSqlProvider.class, method = "sql")
    Long countByCriteria(Entity criteria);


    class InsertSqlProvider extends SqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .INSERT_INTO(table.getTableName())
                    .INTO_COLUMNS(table.getColumns())
                    .INTO_VALUES(Stream.of(table.getFields()).map(this::bindParameter).toArray(String[]::new))
                    .toString();

        }
    }

    class BatchInsertSqlProvider extends SqlProviderSupport {
        public String sql(Object entities, ProviderContext context) {
            TableInfo table = tableInfo(context);

            int size = ((List)((Map)entities).get("list")).size();
            String value = "(" + String.join(",", Stream.of(table.getFields()).map(this::bindParameter).toArray(String[]::new)) + ")";
            String[] values = new String[size];
            Arrays.fill(values, value);

            SQL sql = new SQL()
                    .INSERT_INTO(table.getTableName())
                    .INTO_COLUMNS(table.getColumns());
            StringBuilder sqlBuilder =  new StringBuilder(sql.toString());
            sqlBuilder.append(" VALUES ");
            sqlBuilder.append(String.join(",", values));
            return sqlBuilder.toString();
        }
    }

    class UpdateSqlProvider extends SqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .UPDATE(table.getTableName())
                    .SET(Stream.of(table.getFields())
                            .filter(field -> !table.getPrimaryKeyColumn().equals(columnName(field)))
                            .map(field -> columnName(field) + " = " + bindParameter(field)).toArray(String[]::new))
                    .WHERE(table.getPrimaryKeyColumn() + " = #{id}")
                    .toString();
        }
    }

    class UpdateSelectiveSqlProvider extends SqlProviderSupport {
        public String sql(Object entity, ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .UPDATE(table.getTableName())
                    .SET(Stream.of(table.getFields())
                            .filter(field -> value(entity, field) != null && !table.getPrimaryKeyColumn().equals(columnName(field)))
                            .map(field -> columnName(field) + " = " + bindParameter(field)).toArray(String[]::new))
                    .WHERE(table.getPrimaryKeyColumn() + " = #{id}")
                    .toString();
        }
    }

    class DeleteSqlProvider extends SqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .DELETE_FROM(table.getTableName())
                    .WHERE(table.getPrimaryKeyColumn() + " = #{id}")
                    .toString();
        }
    }

    class DeleteByCriteriaSqlProvider extends SqlProviderSupport {
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .DELETE_FROM(table.getTableName())
                    .WHERE(Stream.of(table.getFields())
                            .filter(field -> value(criteria, field) != null)
                            .map(field -> columnName(field) + " = " + bindParameter(field))
                            .toArray(String[]::new))
                    .toString();
        }
    }

    class SelectOneSqlProvider extends SqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .SELECT(table.getSelectColumns())
                    .FROM(table.getTableName())
                    .WHERE(table.getPrimaryKeyColumn() + " = #{id}")
                    .toString();
        }
    }

    class SelectAllSqlProvider extends SqlProviderSupport {
        public String sql(String orderBy, ProviderContext context) {
            TableInfo table = tableInfo(context);
            SQL sql = new SQL()
                    .SELECT(table.getSelectColumns())
                    .FROM(table.getTableName());
            if (StringUtils.isEmpty(orderBy)) {
                orderBy = table.getPrimaryKeyColumn() + " DESC";
            }
            return sql.ORDER_BY(orderBy).toString();
        }
    }

    class SelectByCriteriaSqlProvider extends SqlProviderSupport {
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT(table.getSelectColumns())
                    .FROM(table.getTableName())
                    .WHERE(Stream.of(table.getFields())
                            .filter(field -> value(criteria, field) != null)
                            .map(field -> columnName(field) + " = " + bindParameter(field))
                            .toArray(String[]::new)).ORDER_BY(table.getPrimaryKeyColumn() + " DESC").toString();
        }
    }

    class CountByCriteriaSqlProvider extends SqlProviderSupport {
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT("COUNT(*)")
                    .FROM(table.getTableName())
                    .WHERE(Stream.of(table.getFields())
                            .filter(field -> value(criteria, field) != null)
                            .map(field -> columnName(field) + " = " + bindParameter(field)).toArray(String[]::new))
                    .toString();
        }
    }

    class CountSqlProvider extends SqlProviderSupport {
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT("COUNT(*)")
                    .FROM(table.getTableName())
                    .toString();
        }
    }


    abstract class SqlProviderSupport {

        /**
         * 表前缀
         */
        private static final String TABLE_PREFIX = "tb_";

        /**
         * 主键名
         */
        private static final String DEFAULT_PRIMARY_KEY = "id";

        /**
         * key:interface class   value:tableInfo
         */
        private static Map<Class, TableInfo> tableCache = new ConcurrentHashMap<>(256);


        /**
         * 获取表信息结构
         * @param context
         * @return
         */
        protected TableInfo tableInfo(ProviderContext context) {
            TableInfo info = tableCache.get(context.getMapperType());
            if (info != null) {
                return info;
            }

            Class<?> entityClass = entityType(context);
            //获取不含有@NoColumn注解的fields
            Field[] fields = excludeNoColumnField(ReflectionUtils.getFields(entityClass));
            info = TableInfo.entityClass(entityClass)
                    .fields(fields)
                    .tableName(tableName(entityClass))
                    .primaryKeyColumn(primaryKeyColumn(fields))
                    .columns(columns(fields))
                    .selectColumns(selectColumns(fields))
                    .build();

            tableCache.put(context.getMapperType(), info);
            return info;
        }

        /**
         * 获取BaseMapper接口中的泛型类型
         * @param context
         * @return
         */
        protected Class<?> entityType(ProviderContext context) {
            return Stream.of(context.getMapperType().getGenericInterfaces())
                    .filter(ParameterizedType.class::isInstance)
                    .map(ParameterizedType.class::cast)
                    .filter(type -> type.getRawType() == BaseMapper.class)
                    .findFirst()
                    .map(type -> type.getActualTypeArguments()[0])
                    .filter(Class.class::isInstance).map(Class.class::cast)
                    .orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型类 " + context.getMapperType().getName() + "."));
        }


        protected String tableName(Class<?> entityType) {
            return TABLE_PREFIX + StringUtils.camel2Underscore(entityType.getSimpleName());
        }

        /**
         * 过滤含有@NoColumn注解的field
         * @param totalField  entityClass所有的字段
         * @return   不包含@NoColumn注解的fields
         */
        protected Field[] excludeNoColumnField(Field[] totalField) {
            return Stream.of(totalField)
                    //过滤含有@NoColumn注解的field
                    .filter(field -> !field.isAnnotationPresent(NoColumn.class))
                    .toArray(Field[]::new);
        }
        /**
         * 获取查询对应的字段 (不包含pojo中含有@NoColumn主键的属性)
         *
         * @param fields p
         * @return
         */
        protected String[] selectColumns(Field[] fields) {
            return Stream.of(fields).map(this::selectColumnName).toArray(String[]::new);
        }

        /**
         * 获取所有pojo所有属性对应的数据库字段 (不包含pojo中含有@NoColumn主键的属性)
         *
         * @param fields entityClass所有fields
         * @return
         */
        protected String[] columns(Field[] fields) {
            return Stream.of(fields).map(this::columnName).toArray(String[]::new);
        }

        /**
         * 如果fields中含有@Primary的字段,则返回该字段名为主键,否则默认'id'为主键名
         * @param fields entityClass所有fields
         * @return  主键column(驼峰转为下划线)
         */
        protected String primaryKeyColumn(Field[] fields) {
            return Stream.of(fields).filter(field -> field.isAnnotationPresent(Primary.class))
                    .findFirst()    //返回第一个primaryKey的field
                    .map(this::columnName)
                    .orElse(DEFAULT_PRIMARY_KEY);
        }

        /**
         * 获取单个属性对应的数据库字段(带有下划线字段将其转换为"字段 AS pojo属性名"形式)
         *
         * @param field
         * @return
         */
        protected String selectColumnName(Field field) {
            String camel = StringUtils.camel2Underscore(field.getName());
            return camel.contains("_") ? camel + " AS " + field.getName() : camel;
        }

        /**
         * 获取单个属性对应的数据库字段
         *
         * @param field  entityClass中的field
         * @return
         */
        protected String columnName(Field field) {
            return StringUtils.camel2Underscore(field.getName());
        }

        protected String bindParameter(Field field) {
            return "#{" + field.getName() + "}";
        }

        protected Object value(Object bean, Field field) {
            try {
                field.setAccessible(true);
                return field.get(bean);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e);
            } finally {
                field.setAccessible(false);
            }
        }
    }
}

以上就是BaseMapper的主要代码,就可以轻松实现一些通用的mapper方法。以上代码中还有些其他对象(如TableInfo以及@NoColume注解等),由于文章篇幅已经太多了。如果有需要的可以在个人的项目中查看,并copy。
个人项目之BaseMapper目录链接:https://gitee.com/objs/mayfly/tree/master/mayfly-dao/src/main/java/mayfly/dao/base

使用方法

/**
*声明一个接口,继承BaseMapper接口,并将实体类传入BaseMapper的泛型参数中
*/
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {

}

也可以使用@NoColume注解过滤非数据库表中的字段,加了该注解之后通用查询时候就不会查该属性,主要用户复合对象如:

public class User{
	@NoColume
	private Product product;
}

接下来就可以在service中注入对应Mapper就可以实现通用方法调用了。

总结

有了通用Mapper可以大幅减轻重复的工作量。个人项目中也有一些通用Service等通用功能,如感兴趣可前往查看,并使用,当然可能也存在BUG,欢迎大佬多多指导!
BaseService代码链接:https://gitee.com/objs/mayfly/tree/master/mayfly-sys/src/main/java/mayfly/sys/service/base