最近对嵌入式比较感兴趣,在学一些电路知识。在学习(还是玩?这是另一个声音)一切东西的时候,心底都有个混账声音在质问我,学这些究竟有什么用,你在逃避什么,我不知道如何反驳,只能说一句,去你妈的,如果急功近利,你屁都实现不了。
记录关于 Mybatis Plus 在复杂使用上的实践,以让生活变得更轻松。包括:
嵌套 Wrapper 查询方法的实际作用(文档语焉不详,网上没有找到仔细分析的文章),包括 and,or,nested,func
使用 Java 代码进行 SQL 片段抽象 ,以类型安全和防 SQL 的方式定义新的方法
自定义 SQL,以及多表查询 SQL(多 Wrapper 查询 ) 的 Mybatis Plus 解决方案,包括一对一,一对多(在“多”上有查询条件,但终究是去查“一”),以及查询其他字段
只研究 LabmdaWrapper ,同时不研究 groupBy 等方法,认为此种情况下自定义 SQL 更加合适。
连接词,以及嵌套 Wrapper 连接词指 and 和 or。
下面的每个方法都有带 condition 参数的版本,只有为 true 时该次调用才会真正生效,下面的描述都省略掉此种版本。
and
,or
,func
和nested
接受Consumer<Wrapper>
去以“子句”的形式去定义 SQL 片段,使用子句可以定义任意复杂的 SQL,在很多时候比 XML 还灵活。
or()
:修改当前连接词,下一条 语句拼接过来时,使用 or 而非 and 去连接(其后仍是 and)。MP 在拼接条件时,使用 and 是默认行为
or(Consumer<Wrapper> wrapper)
:构造 or 子句,即形如 or (...)
nested(Consumer<Wrapper> wrapper)
:构造一个子句,其中连接词取当前连接词 ,这就是说,.or(x -> {...}) === .or().nested(x -> {...})
and(Consumer<Wrapper> wrapper)
:构造 and 子句,即形如and (...)
;如果没有使用or()
的话,and
和nested
功能是相同的
func(Consumer<Wrapper> wrapper)
:不构造子句 ,子 Wrapper 就是自己,即形如 ...
(没有连接词,没有括号,扁平的 )
最佳实践:
尽量不要使用or()
(除非 SQL 极其简单),永远使用 or 和 and 子句,使用子句能让 SQL 结构更加清晰,其作用(无论是对程序员还是实际上)就像加括号一样。
不要使用func(wrapper)
,它的扁平结构意味着它中间如果使用了 or 可能会导致错误的结构,没有限制好它自己的抽象。
SQL 片段抽象 Mybatis Plus 使用 LambdaWrapper 时,最大的痛点之一就是无法进行 SQL 片段抽象(这同时也是 Mybatis 的痛点)。但我们能够利用Consumer<Wrapper>
去进行扩展 。
SQL 片段抽象是很重要的,考虑一些常见的问题:
in 语句最大只允许 1000 个元素的查询 ,为此在之前使用 Mybatis 的时候,出现过一些非常恶心但又不得不反复拷贝的代码:
<if test ="userIds!=null" > and (user_id in <trim suffixOverrides =" or user_id in ()" > <foreach collection ="userIds" item ="item" index ="index" open ="(" close =")" > <if test ="index != 0" > <choose > <when test ="index % 1000 == 999" > ) or user_id in (</when > <otherwise > ,</otherwise > </choose > </if > #{item} </foreach > </trim > )</if >
经常有查询特定日期片段内的数据的需求,有时候要精确到天,有时候要精确到秒,每次遇到前者都要在 SQL 中对入参进行 trunc 操作,或者 JdbcType 类型用 DATE,十分繁琐
无法使用 DBMS 特定的函数,如 INSTR,FIND_IN_SET
解决方案有两类:
定义工具函数进行抽象,这些工具函数接受 condition,查询列以及查询条件,然后返回Consumer<Wrapper>
,用户使用时使用nested
方法去引用这些工具类 。这个方法实现简单,但是无法直接 根据列的 getter 函数获取列名,所以无法直接对 SQL 函数进行抽象,但不是完全不可以。
集成 LambdaQueryWrapper,去定义新的方法。这个可以说是万能的,但是侵入性比较高
下面使用第一种方式,下面是一些示例,处理了上面的问题 1,2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 class WrapperUtil { private WrapperUtil () {} public static <T> Consumer<LambdaQueryWrapper<T>> betterIn (SFunction<T, ?> column, List<?> list) { return wrapper -> { Assert.notEmpty(list, "list cannot be empty" ); for (int i = 0 ; i < list.size(); i+=1000 ) { wrapper.or().in(column, list.subList(i, Math.min(list.size(), i + 1000 ))); } }; } static Date getDayStart (Date date) { if (date == null ) { return null ; } Calendar instance = Calendar.getInstance(); instance.setTime(date); instance.set(Calendar.HOUR_OF_DAY, 0 ); instance.set(Calendar.MINUTE, 0 ); instance.set(Calendar.SECOND, 0 ); instance.set(Calendar.MILLISECOND, 0 ); return instance.getTime(); } static Date getDayEnd (Date date) { if (date == null ) { return null ; } Calendar instance = Calendar.getInstance(); instance.setTime(date); instance.set(Calendar.HOUR_OF_DAY, 23 ); instance.set(Calendar.MINUTE, 59 ); instance.set(Calendar.SECOND, 59 ); instance.set(Calendar.MILLISECOND, 999 ); return instance.getTime(); } public static <T> Consumer<LambdaQueryWrapper<T>> datetimeBetween (SFunction<T, ?> column, Date startDate, Date endDate) { return wrapper -> { if (startDate != null || endDate != null ) { wrapper.ge(startDate != null , column, startDate); wrapper.le(endDate != null , column, endDate); } else { wrapper.apply("1=1" ); } }; } public static <T> Consumer<LambdaQueryWrapper<T>> dateBetween (SFunction<T, ?> column, Date startDate, Date endDate) { return datetimeBetween(column, getDayStart(startDate), getDayEnd(endDate)); } }
关于第 3 个问题,就需要玩一点真正的花活儿了——用反射拿到AbstractLambdaWrapper#getColumnCache
方法,它接受列函数并返回一个包含实际列名的对象。这个方法是 protected 的 ,所以如果实现 LambdaQueryWrapper 的子类的话能够直接使用这个函数。
下面同时使用apply
方法去插入函数调用,该方法第一个参数是 SQL 片段,但它的其他参数是通过#{}
的方式去注入的,所以不会有 SQL 注入风险。下面同时也演示了如何处理多 DBMS 兼容 问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private static final Object lock = new Object ();private static volatile Method getColumnCacheMethod = null ;private static <T> String getTableField (SFunction<T, ?> column, LambdaQueryWrapper<?> wrapper) { if (getColumnCacheMethod == null ) { synchronized (lock) { if (getColumnCacheMethod == null ) { try { Method method = AbstractLambdaWrapper.class.getDeclaredMethod("getColumnCache" , SFunction.class); method.setAccessible(true ); getColumnCacheMethod = method; } catch (Exception e) { throw new RuntimeException (e); } } } } ColumnCache invoke = null ; try { invoke = (ColumnCache) getColumnCacheMethod.invoke(wrapper, column); } catch (Exception e) { throw new RuntimeException (e); } return invoke.getColumn(); }public static <T> Consumer<LambdaQueryWrapper<T>> findInSet (SFunction<T, ?> column, Serializable value) { if (currentDB.equals("MySQL" )) { return wrapper -> { wrapper.apply(String.format("FIND_IN_SET({0}, %s) > 0" , getTableField(column, wrapper)), value); }; } else { return wrapper -> { wrapper.apply(String.format("INSTR(',' || %s || ',', {0}) > 0" , getTableField(column, wrapper)), value); }; } }
多表查询 很多时候需要自定义 SQL,如要连接查询,要查询表里没有的字段等,相关官方文档见 https://baomidou.com/guides/wrapper/#使用-wrapper-自定义-sql 。
简单来说的话,就是 wrapper 的<where>
部分能够通过 customSqlSegment
参数给它取出来,注意该部分代码包括 where 关键字 ,不需要另外写 where。
官方文档声称 Wrapper 的参数名必须为 ew,这是偏颇的,Wrapper 的参数名是可以定义的,而且也可以有多个 Wrapper 作为参数。
自定义 SQL 的话,就无法使用 Wrapper 的 orderBy,select 功能了,但这是可以忍受的。
关于重新定义 Wrapper 的参数名,Wrapper 提供了一个方法withParamAlias
去修改;它有一些问题:
必须和 SQL 方法上的参数名匹配,而这本不该由业务代码去关心 (我采取的方法是把实际的 SQL 方法以__起头,表示不应该调用它,同时在 mapper 中定义同名的 default 方法,在其中对 Wrapper 进行此项处理)
该方法只会修改顶层的 paramAlias,如果 Wrapper 使用了子句,则它会有嵌套的子 Wrapper
为解决第二个问题,定义一个函数去递归地设置 Wrapper 的 paramAlias:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static <T> T withParamAlias (T wrapper, String paramAlias) { if (!(wrapper instanceof AbstractWrapper)) { return wrapper; } AbstractWrapper<?, ?, ?> wrapper1 = (AbstractWrapper<?, ?, ?>) wrapper; try { wrapper1.setParamAlias(paramAlias); } catch (Exception ignored) { } MergeSegments expression = wrapper1.getExpression(); for (ISqlSegment iSqlSegment : expression.getNormal()) { withParamAlias(iSqlSegment, paramAlias); } return wrapper; }
然后关于多表查询,这里只讨论一种简单的情况,即我仍旧是查询某一张表的数据,但允许联其他表去查 。
要联其他表去查是容易的,问题是,如何避免重名字段 ?Wrapper 没提供任何方法去在查询字段前加上别名,为此,在 SQL 的设计上,我们就只能去控制 Wrapper 的作用域 。下面直接给出一个示例 SQL,考虑有学生表,班级表,分数表,其中学生和班级,学生和分数是关联的),我要根据班级和成绩查学生的信息,同时对这些学生,我想查询学生的班级名称,查询学生的化学课的最高分,这时候我就直接 tmd:
select * , (select class_name from class where class.id = st.class_id) , (select max (score) from score where score.id = st.id and score.class_id = (select class_id from class where class_name = '化学' limit 1 ))from (select * from student ${studentWrapper.customSqlSegment}) st< where > < if test= "!classWrapper.emptyOfWhere"> and class_id in (select id from class ${classWrapper.customSqlSegment}) < / if> < if test= "!scoreWrapper.emptyOfWhere"> and id in (select id from score ${classWrapper.customSqlSegment}) < / if> < / where >
这种SQL简直就是在雷普DBMS,但是它能用:)。