前言
几乎每个系统都需要单表的基础操作(即增删改查分页查询等),如果不使用通用的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