上周三下午刚睡醒,生产环境的消息报警中出现了一条空指针错误,这条常见的错误消息,拉开了8个小时修数据的帷幕。
根据空指针的错误信息找到了对应的代码和数据,诡异的是这条数据中一个重要的关联字段竟然是空的。随后写了一个sql,查出全表中此字段为空的数据竟然高达29万条,近乎全表的数据都被洗了。这下可坏了!这时候钉钉里频繁出现业务人员拉工单,反馈系统单据异常、已结算的订单找不到、订单金额错误等问题。线上系统近乎瘫痪了!
我赶快反馈了组长和其他同事,分头行动。最终经历了2次代码上线和n次脚本修复,终于补完了,结束已经是晚上9点。
导致问题的代码简化如下:
public int updateOrderByCode(OrderDto entity) {
entity.setCode="123456";
entity.setRefCode = "999999";
Example e = new Example(Order.class);
Example.Criteria cri = e.createCriteria();
cri.andEqualTo(Order.Fields.code,entity.getCode());
return orderManager.updateByExampleSelective(entity,e);
}
这个方法的逻辑是将编码为”123456“的refCode字段更新为“999999”。
但是由于前端的和接口没做校验,导致传入的参数都是空,逻辑变成了下面的内容:
public int updateOrderByCode(OrderDto entity) {
entity.setCode=null;
entity.setRefCode = "";
Example e = new Example(Order.class);
Example.Criteria cri = e.createCriteria();
cri.andEqualTo(Order.Fields.code,entity.getCode());
return orderManager.updateByExampleSelective(entity,e);
}
如果这段代码在原生的mybatis执行是不会有问题的,因为底层会检验条件参数值是否为null,为null则抛错,不会再更新数据。
//mybatis对Example.Criteria条件参数的检验
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
但是在tkmybatis(版本3.5.3)中,逻辑不同了!
tkmybatis在检验参数值时需要满足变量值notNull是true才会抛错,不然它会舍弃这个条件!而notNull默认是false,这意味着如果仅有的参数值是null,则更新全表!
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
if (this.notNull) {
throw new MapperException("Value for " + property + " cannot be null");
}
} else if (property != null) {
this.criteria.add(new Example.Criterion(condition, value));
}
}
Example构造方法的源码如下,可以看到在声明时可以指定entityClass、exists,、notNull这三个参数。exists是当条件对应字段不存在则抛错的开关,notNull是当条件的参数值为null则抛错的开关。
如果不传则默认为exists : true,notNull:false。
public Example(Class<?> entityClass) {
this(entityClass, true);
}
public Example(Class<?> entityClass, boolean exists) {
this(entityClass, exists, false);
}
public Example(Class<?> entityClass, boolean exists, boolean notNull) {
this.exists = exists;
this.notNull = notNull;
this.oredCriteria = new ArrayList();
this.entityClass = entityClass;
this.table = EntityHelper.getEntityTable(entityClass);
this.propertyMap = this.table.getPropertyMap();
this.ORDERBY = new Example.OrderBy(this, this.propertyMap);
}
正确的做法是在声明条件参数Example同时指定notNull为true
Example e = new Example(Order.class,true,true);
至此问题定位和解决方案已经描述完了。如果你的项目中也使用了tkmybatis,快检查你的用法正确吗😨?
数据无价,对技术要有敬畏之心,使用时做好调研!