mybatisplus在设计上略有欠缺,导致整个框架有些笨重,因此决定抽出精华部分自己写
MyBatis更加强大的动态SQL
为什么不用MyBatis-Plus
MP(mybatis-plus)是一个很便捷的框架,功能也很多,在大部分人眼中是一个超级好用的框架,事实上确实如此,只要不去在意其设计上的缺陷,但我偏偏就是有强迫症,就对MP的设计抱有疑问。
MP本身不支持多表联查
MP最吸引人的东西就是
QueryWrapper
,以及通过实体类的注解来自动的插入、更新数据,对于插入和更新没有问题,但是查询却只能在实体类上注解的TableName
一张表上进行查询,让人有些遗憾。MP绑定的
IService
破坏了三层架构这就是我最疑惑的地方,在MP中只要不进行多表联查的话是不需要写Mapper的,因此对于条件查询使用的Wrapper是在Service层中进行的,这就必须涉及到在Service中写数据库的字段名,匹配哪些字段来进行查询——这个功能本身是由持久层完成的,不应该放在业务层中,如果数据库字段名出现更改,那么业务层中使用的方法,由于相同的查询可能写多次而导致修改的工作量变大,事实上对于一次查询的所有条件都包含在
DTO
内,因此只需要根据DTO内的值按规则进行查询即可,而规则对于一个DTO来说只有一个,也只需要编写一次。
以上,可能由于我的无知造成误解
实现动态SQLBuilder
可以依赖于Mybatis自带的SQL
进行二次封装。因此实现的Builder是用于Mybatis注解配置SQL的场景,在@XXXXProvider
中使用。对框架没有任何本质增强,仅仅是简化动态生成SQL的代码量。
自定义注解
注解 | 作用 |
---|---|
@UpdateField | 重命名实体对应表的名字,允许更新/插入 |
@PrimaryField | 标注实体对应表的主键,可重命名 |
/**
* 带有此注解的 Field 即允许更新(包括插入),与数据库不同名可指定value值,默认同名不用赋值
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UpdateField {
String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrimaryField {
String value() default "";
/*是否自增*/
boolean auto() default true;
}
InsertSQL
对于插入语句的复杂场景无非就是批量插入,因此实现一个批量插入语句的构造器,依赖于自定义注解来保持规则。
public class InsertSQL extends BaseUpInSQL<InsertSQL> {
private final List<?> data;
public InsertSQL(List<?> batchData, String tableName) {
this.data = batchData;
this.tableName = tableName;
}
private String[] getIntoValueArray(String[] nameArray, int index) {
String[] intoValueNames = new String[nameArray.length];
for (int i = 0; i<intoValueNames.length; i++) {
String cur = nameArray[i];
if (cur.matches(".*\\$")) {
nameArray[i] = Str.removeEnd(cur);
//mybatis内部根据是否有L转化成长整形 此处无安全性风险可直接添加
intoValueNames[i] = String.valueOf(SqlBuilderConfig.snowFlake.generate());
} else {
intoValueNames[i] = Str.f("#{list[{}].{}}",index, cur);
}
}
return intoValueNames;
}
private String getFieldColumn(Field field, Annotation annotation) {
String alias = field.getName();
if (annotation instanceof UpdateField) {
UpdateField anno = (UpdateField) annotation;
alias = anno.value().isEmpty() ? alias : anno.value();
}
else if (annotation instanceof PrimaryField) {
PrimaryField anno = (PrimaryField) annotation;
alias = anno.value().isEmpty() ? alias : anno.value();
}
if(isCamelToUnderscore()) alias = Str.toUnderscore(alias);
return alias;
}
@Override
public String toString() {
INSERT_INTO(tableName);
if (data.size() == 0) return null;
Class<?> dataClass = data.get(0).getClass();
Field[] fields = dataClass.getDeclaredFields();
List<String> fieldNames = new ArrayList<>(fields.length);
boolean hasFoundId = false;
for (Field field : fields) {
//找主键 如果主键自增则跳过不插入主键
if (!hasFoundId) {
PrimaryField anno = field.getDeclaredAnnotation(PrimaryField.class);
if (anno != null) {
hasFoundId = true;
if (anno.snowFlake()) {
fieldNames.add(getFieldColumn(field,anno) + "$");
continue;
} else if (anno.auto()) {
continue;
} else {
fieldNames.add(getFieldColumn(field,anno));
continue;
}
}
}
//2 判断是否允许插入
UpdateField upAnno = field.getDeclaredAnnotation(UpdateField.class);
if (upAnno != null) {
fieldNames.add(getFieldColumn(field, upAnno));
}
}
String[] names = fieldNames.toArray(Cl.emptyArr(String.class));
for (int i = 0; i < data.size(); i++) {
INTO_VALUES(getIntoValueArray(names, i));
}
//雪花算法检测规则可能造成对 ID 列名的一些修改 需要处理后在INTO_COLUMNS 看起来不太优雅 但是性能比清晰的划分功能更高
INTO_COLUMNS(names);
return super.toString();
}
}
UpdateSQL
更新语句主要在平时写的过程中主要时重复了“判空更新”的步骤,使用构造器简化代码。
/**
* 默认使用 {@link PrimaryField} 注解的属性作为 where 条件 <br/>
* 指定 <b>keyFieldName</b> 则使用该值作为 where 条件 <br/>
* 也可使用方法 <b>where()</b> 指定自己编写的 where 子句
*/
@Slf4j
public class UpdateSQL extends SQL {
private final Object data;
private final String tableName;
private String keyFieldName;
private String whereSql;
public UpdateSQL(Object data, String tableName) {
this.data = data;
this.tableName = tableName;
}
public UpdateSQL(Object data, String tableName, String keyFieldName) {
this.data = data;
this.tableName = tableName;
this.keyFieldName = keyFieldName;
}
public UpdateSQL where(String whereSql) {
this.whereSql = whereSql;
return this;
}
@Override
public String toString() {
UPDATE(tableName);
Field[] fields = data.getClass().getDeclaredFields();
String name = "";
String idFieldName = "";
String tableId = "";
String whereBy = "";
boolean hasFoundId = false;
try {
for (Field field : fields) {
name = field.getName();
//1 尝试找主键 主键不能更新
if (!hasFoundId) {
PrimaryField pAnno = field.getDeclaredAnnotation(PrimaryField.class);
if (pAnno != null) {
hasFoundId = true;
idFieldName = name;
tableId = pAnno.value().isEmpty() ? name : pAnno.value();
continue;
}
}
UpdateField annotation = field.getDeclaredAnnotation(UpdateField.class);
//2 判断是否使用自定义的 where 条件
if (!Str.empty(keyFieldName) && keyFieldName.equals(name)) {
whereBy = annotation == null || annotation.value().isEmpty() ? name : annotation.value();
}
//3 判断是否需要更新
if (annotation == null) continue;
field.setAccessible(true);
String alias = annotation.value().isEmpty() ? name : annotation.value();
if (field.get(data) != null) {
SET(Str.f("{}=#{{}}", name, alias));
}
}
if(Str.empty(whereSql)) {
if (Str.empty(keyFieldName) || Str.empty(whereBy)) {
keyFieldName = idFieldName;
whereBy = tableId;
}
WHERE(Str.f("{}=#{{}}", whereBy, keyFieldName));
} else {
WHERE(whereSql);
}
} catch (IllegalAccessException e) {
log.error("update sql build failed, couldn't access {} of {}", name, data.getClass().getSimpleName());
return null;
}
return super.toString();
}
}
QuerySQL
实现只写“字段名”来构造查询Sql,仅仅只填写字段然后获得语句,内部还是使用mybatis的SQL
,判断DTO字段对应值是否为空来决定是否应用where语句,使用者无需进行判断。
@SuppressWarnings("unchecked")
public abstract class BaseQuerySQL<T> {
private final Object dto;
private final SQL sql;
public BaseQuerySQL(Object dto, String tableName) {
this.dto = dto;
this.sql = new SQL();
sql.FROM(tableName);
}
public T join(String... joinSql) {
sql.JOIN(joinSql);
return (T)this;
}
public T select(String... columns) {
sql.SELECT(columns);
return (T)this;
}
public T group(String... columns) {
sql.GROUP_BY(columns);
return (T)this;
}
public T having(String havingSql) {
sql.HAVING(havingSql);
return (T)this;
}
private void where(String whereSql, String column, String fieldName) {
if (fieldName == null) fieldName = column;
if (isNotNULL(fieldName)) {
sql.WHERE(Str.f(whereSql, column, fieldName));
}
}
protected T unsafeWhere(String whereSql) {
sql.WHERE(whereSql);
return (T)this;
}
//通过此方法复合一组条件实现 xxx AND (xx OR xx) 这种逻辑表达式
public T conditions(Condition... conditions) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(");
boolean isFst = true;
for (Condition condition : conditions) {
if (isNotNULL(condition.getFieldName())) {
if (isFst) {
condition.removeLogic();
isFst = false;
}
stringBuilder.append(" ").append(condition.toString());
}
}
stringBuilder.append(")");
String s = stringBuilder.toString();
if (s.equals("()")) {
return (T) this;
}
return unsafeWhere(s);
}
private boolean isNotNULL(String fieldName) {
try {
Field field = dto.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(dto) != null;
} catch (Exception e) {
return false;
}
}
public T eq(String column, String fieldName) {
where("{} = #{{}}", column, fieldName);
return (T)this;
}
public T neq(String column, String fieldName) {
where("{} != #{{}}", column, fieldName);
return (T)this;
}
public T le(String column, String fieldName) {
where("{} <= #{{}}", column, fieldName);
return (T)this;
}
public T lt(String column, String fieldName) {
where("{} < #{{}}", column, fieldName);
return (T)this;
}
public T ge(String column, String fieldName) {
where("{} >= #{{}}", column, fieldName);
return (T)this;
}
public T gt(String column, String fieldName) {
where("{} > #{{}}", column, fieldName);
return (T)this;
}
public T or() {
sql.OR();
return (T)this;
}
public T like(String column, String fieldName, LikeType type) {
switch (type) {
case L: fieldName = "%" + fieldName;
case R: fieldName = fieldName + "%";
case LR: fieldName = "%" + fieldName + "%";
}
where("{} like #{{}}", column, fieldName);
return (T)this;
}
public T regexp(String column, String fieldName) {
where("{} regexp #{{}}", column, fieldName);
return (T)this;
}
@Override
public String toString() {
return sql.toString();
}
}