MyBatis if 標籤的坑,居然被我踩到了。。。
阿新 • • 發佈:2020-12-09
事件的原因是這樣的,需求是按條件查資料然後給前端展示就行了,寫的時候想著挺簡單的,不就是使用 MyBatis 動態 SQL 去查詢資料嗎?
![](https://img-blog.csdnimg.cn/img_convert/b2dc78f451c7a2e563bc877c7caa7ecb.webp?x-oss-process=image/format,png)
現實還是很殘酷的,等我寫完上完 UAT 後,前端同學說根據`state`查的資料與理想的資料不一致,這個`state`當時設計時只有兩個值:`0`和`1`。
```
/**
* 資料狀態
*/
@Range(min = 0, max = 1, message = "狀態只能為0(未處理),1(已處理)")
private Integer state;
```
理想情況下通過前端傳遞過來的值,然後進行sql查詢就可以了:
```
AND md.state = #{req.state}
```
上面的sql首先判斷`state`不為空且**不為空字串**時,然後新增比較`state`欄位。初步看下來`if`判斷沒什麼問題,但是我傳遞進去的`req.state`是`Integer`型的,仔細檢視`req.state != null`沒毛病,然後發現`req.state != ''`使用`Integer`與空字串做比較。
前端在查詢的時如果沒有傳遞`req.state`那`req.state != null `這裡不會滿足,但是前端傳遞了一個`0`過來的時候`req.state != ''`居然返回的是`false`也就是說在**MyBatis的if語法中0是等於空字串的**:
```
{
"state": 0
}
```
這樣的比較沒有報錯,也是有點想不通了,沒辦法只能去看MyBatis原始碼找出這原因。
### 檢視 MyBatis 原始碼
MyBatis 其他原始碼的查詢過程就不詳細說了,這裡直接找到`XMLScriptBuilder`類,找到`if`語法的解析過程,然後一步步的探究`0 == ''`的原因。 `XMLScriptBuilder`會解析`trim`、`if`等 MyBatis 支援的語法,它的解析原理是通過`NodeHandler`來分別解析不同的標籤:
```
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
```
由於是不正解的語法是`if`標籤,檢視`IfHandler`就好了,其他現在略過就好。
```
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
```
MyBatis會將`if`標籤抽象成`IfSqlNode`:
```
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
```
終於有一點眉頭了, MyBatis 會將`if`標籤的`test`屬性使用`ExpressionEvaluator`測試一下是否為`true`或者為`false`:
```
public class ExpressionEvaluator {
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
public Iterable> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
return (Iterable>) value;
}
if (value.getClass().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List