mybatis-mapper / mapper

MyBatis Mapper
https://mapper.mybatis.io
Apache License 2.0
325 stars 47 forks source link

Example能否支持类似or(...)的and(...)方法? #53

Closed abel533 closed 1 year ago

abel533 commented 1 year ago

@ydq QQ群里提的需求,方便加群看看吗?

群号: 277256950

大佬 请问是否支持 2% 9YQB~4O}4QD`SS(Y2YAG

将图中or改为and的写法

4YB%EDM)636OFTRCCZ~D(MO

想要上图效果 使用or E)Y$AKL{%OHN$9V67%S KQH

sql是 M`}BQ27Z4`NW_A0)ZA9 N31

我把这里的or 改为and就满足我的需求了 5BUF`X`Z2V9L$Q YVPMDL7U

个人感觉and的场景应该是多些的 大佬下个版本or 能否加个这样的需求

ydq commented 1 year ago

之前做 or(...) 的时候,尝试了下,本来想使用 <sql><include> 两个标签使用递归的思路,支持任意层次的 or,但是发现 mybatis 读取解析 xml 的方法和我想象的并不一样,他会在初始化的时候直接尝试 include sql片段,而不是根据条件分支走到实际的调用后再include,因此递归的思路最终会导致栈溢出,所以结论就是 单靠 sql script 无法做到任意层次的 andor 的拼接(后续或许可以尝试根据oredCriteria 实际的值,程序动态的生成 sql script 来解决无法任意层次 or 甚至是 and 的问题?

当前的动态SQL的XML实际上如下

io.mybatis.mapper.example.ExampleProvider#EXAMPLE_WHERE_CLAUSE

<where>
    <foreach collection="oredCriteria" item="criteria" separator=" OR ">
        <if test="criteria.valid">
            <trim prefix="(" prefixOverrides="AND" suffix=")">
                <foreach collection="criteria.criteria" item="criterion">
                    <choose>
                        <when test="criterion.noValue">
                            AND ${criterion.condition}
                        </when>
                        <when test="criterion.singleValue">
                            AND ${criterion.condition} #{criterion.value}
                        </when>
                        <when test="criterion.betweenValue">
                        AND ${criterion.condition} #{criterion.value} AND #{criterion.secondValue}
                        </when>
                        <when test="criterion.listValue">
                            AND ${criterion.condition}
                            <foreach collection="criterion.value" item="listItem" open="(" separator="," close=")">
                                #{listItem}
                            </foreach>
                        </when>
                        <when test="criterion.orValue">
                        <!-- !!正因为这里是手拼的,所以无法支持嵌套多层次,所以在OrCriteria类中将andOr系列方法标记为了废弃,且直接抛出异常 -->
                            <foreach collection="criterion.value" item="criteria" separator=" OR " open = " AND (" close = ")">
                                <if test="criteria.valid">
                                    <trim prefix="(" prefixOverrides="AND" suffix=")">
                                        <foreach collection="criteria.criteria" item="criterion">
                                            <choose>
                                                <when test="criterion.noValue">
                                                    AND ${criterion.condition}
                                                </when>
                                                <when test="criterion.singleValue">
                                                    AND ${criterion.condition} #{criterion.value}
                                                </when>
                                                <when test="criterion.betweenValue">
                                                    AND ${criterion.condition} #{criterion.value} AND #{criterion.secondValue}
                                                </when>
                                                <when test="criterion.listValue">
                                                    AND ${criterion.condition}
                                                    <foreach collection="criterion.value" item="listItem" open="(" separator="," close=")">
                                                        #{listItem}
                                                    </foreach>
                                                </when>
                                                <!-- 这里不再继续深层次的 <when test="criterion.listValue"> 了,因为常规的深层次的SQL也可以想办法改写平铺开来  -->
                                            </choose>
                                        </foreach>
                                    </trim>
                                </if>
                            </foreach>
                        </when>
                    </choose>
                </foreach>
            </trim>
        </if>
    </foreach>
</where>

解读当前的or(OrCriteria... orParts)的实现

当前Example 的结构为

Example
  │
  │   //当调用Example.or() 或者 wrapper.or()时会创建一个新的Criteria并追加到当前oredCriteria中 
  │   // oredCriteria 中的每个 Criteria 使用 OR 去拼接
  └─ List<Criteria<T>> oredCriteria:
          │
          │   //当调用 Criteria 中的每个方法,如 .andEqualTo 等都会往当前 Criteria中的criteria 集合中添加一个Criterion
          │   // criteria 中的每个 Criterion 使用 AND 去拼接
          └─ List<Criterion> criteria

而当前的 wrapper.or(Criteria... criterias) (以及 Criteria 中的 andOr )的实现 其实是将当前调用这个方法的 全部入参 条件 criterias 作为了一个特殊的值添加到了 当前 Criteria 中,作为了一个单独的Criterion , 这样就保证了

[其他Criterion] AND [其他Criterion] AND [OR条件片段的Criterion]

将这个映射带入到上面的 动态SQL的XML 最后我们自己的 [OR条件片段的Criterion] 就会命中到上面sql script中的下面一段

<when test="criterion.orValue"><!-- ... --></when>

如果要实现 .and(Criteria... andParts)

方案1:

因为 Criteria 中的每一个 Criterion 条件 本身就是 AND 去拼接,如果直接复用当前对象

优点:复用当前对象,实现简单,支持任意层级的 .and(xxx) 嵌套 以及下一个单层级的 .or(xxx) 条件

缺点:中间任意一个有 .or() 的调用都会不符合预期

/**
* 数组多个条件直接使用 and 拼接
*
* @param andParts 条件块
*/
@SafeVarargs
public final ExampleWrapper<T, I> and(Consumer<ExampleWrapper<T,I>>... andParts) {
if (andParts != null && andParts.length > 0) {
    and(Arrays.asList(andParts));
}
return this;
}

/**
* 数组多个条件直接使用 and 拼接
* 因为默认就是用and拼接的,所以直接用当前 wrapper 对象就可以
*
* @param andParts 条件块
*/
public final ExampleWrapper<T, I> and(List<Consumer<ExampleWrapper<T,I>>> andParts) {
if (andParts != null && andParts.size() > 0) {
    andParts.forEach(p->p.accept(this));
}
return this;
}

不包含 .or() 测试是OK的: SCR-20220802-sdd

但是,当包含有 or() 的情况下的话,则会不符合当前的预期

SCR-20220802-sbe

方案2:

为了应对方案1中的 .or() 这个问题,将 and条件组 与最外层的其他条件进行隔离,使用 OrCriteria 类屏蔽掉 .or() 方法

//ExampleWrapper 新增方法
public final ExampleWrapper<T, I> and(Function<Example.OrCriteria<T>, Example.OrCriteria<T>>... orParts) {
    if (orParts != null && orParts.length > 0) {
        this.current.andAnd(Arrays.stream(orParts).map(orPart -> orPart.apply(example.orPart())).collect(Collectors.toList()));
    }
    return this;
}
//Example.Criteria 新增方法
public Criteria<T> andAnd(List<OrCriteria<T>> orCriteriaList) {
    criteria.add(new Criterion(null, orCriteriaList,null,true));
    return (Criteria<T>) this;
}

//Criterion 新增 andValue 并新增构造
private boolean andValue;//新增一个标识 标记当前的 value 是否为 and 条件组

protected Criterion(String condition, Object value, String typeHandler, Boolean andValue) {
    super();
    this.condition = condition;
    this.value = value;
    if (value instanceof Collection<?>) {
        if (condition != null) {
            this.listValue = true;
        } else {
            this.orValue = true;
        }
    } else {
        this.singleValue = true;
    }
    if (andValue != null) {
        this.andValue = andValue;
        this.orValue = !this.andValue;
    }
}

然后动态SQL中可能又得再来一个

<when test="criterion.andValue"><!-- ... --></when>

优点:没有优点(或者说:阻止用户继续深层次的 .or() 调用,给用于以约束,防止出现不符合预期的情况)

缺点:代码(sql script那块)比较冗余,不支持下一层级的 .or(...)和 .and(...) 嵌套

方案2 依旧不支持在里面嵌套更深的层次,感觉有点得不偿失,除非能根据 oredCriteria 实际的值 动态的生成 sql script,支持到无限极的深度嵌套才会有意义

个人感觉实现 .and(xxx) 方法的性价比不高 ,毕竟本来最外层默认就是 and 拼接的

@abel533 看看 讨论下,是否还有必要需要继续实现?需要的话 可以讨论一下用何种方案会更合理,确定好方案之后我可以提个pr

另外 经过群里面的沟通,应该是该用户被文档的错误给误导了,使用方式上存在错误,https://github.com/mybatis-mapper/mapper-docs/pull/1 ,感觉 多条件的 .and(xxx) 应该是个伪需求

abel533 commented 1 year ago

分析的非常好了,我个人感觉是没必要实现...复杂点的逻辑直接写xml更合适。