如果MySQL表的一些字段被设置为不允许为空,使用MyBatis写入的数据中相应字段的值是null就有可能会报类似下面的错误:
org.springframework.dao.DataIntegrityViolationException: ### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null ### The error may exist in org/chobit/service/mapper/MyMapper.java (best guess) ### The error may involve org.chobit.service.mapper.MyMapper.updateById-Inline ### The error occurred while setting parameters ### SQL: update my_table set name=?, `desc`=? where id=? ### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null ; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:87) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81) at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) at com.sun.proxy.$Proxy88.update(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:67)
对于这个问题,我通常的做法是为对应字段的null值直接赋值一个空字符串(或其它默认值)。但是这次,不知道为什么那么多不必要的字段被设置为了不能为空:七八个字段,要判断是否为空,还要一个个赋默认值 —— 想想就头疼。一定要想办法优化掉这种啰嗦且难看的东西。
参考分页插件 PageHelper 的实现后找到了思路:添加一个拦截器,获取参数实例,为实例的值为null的Field设置一个默认值。
拦截器的实现如下:
/**
* Mybatis拦截器,为null字段设置默认值
*
* @author robin
*/
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MybatisNullInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(MybatisNullInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
padNullFields(parameter);
Executor executor = (Executor) invocation.getTarget();
return executor.update(ms, parameter);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private void padNullFields(Object parameter) {
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(parameter.getClass());
for (PropertyDescriptor pd : targetPds) {
try {
padNullField(pd, parameter);
} catch (Exception e) {
logger.error("pad null fields failed", e);
}
}
}
private void padNullField(PropertyDescriptor pd, Object parameter)
throws InvocationTargetException, IllegalAccessException {
Method readMethod = pd.getReadMethod();
Method writeMethod = pd.getWriteMethod();
if (null == readMethod || null == writeMethod) {
return;
}
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(parameter);
if (null != value) {
return;
}
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
Class<?> clazz = pd.getPropertyType();
setDefaultValue(parameter, clazz, writeMethod);
}
/**
* 为字段设置默认值
*
* @param parameter 参数
* @param fieldClass 字段类型
* @param writeMethod 字段写方法
*/
private void setDefaultValue(Object parameter, Class<?> fieldClass, Method writeMethod)
throws InvocationTargetException, IllegalAccessException {
if (fieldClass.isAssignableFrom(String.class)) {
writeMethod.invoke(parameter, "");
}
if (fieldClass.isAssignableFrom(Boolean.class)) {
writeMethod.invoke(parameter, false);
}
if (fieldClass.isAssignableFrom(Byte.class)) {
writeMethod.invoke(parameter, (byte) 0);
}
if (fieldClass.isAssignableFrom(Integer.class)) {
writeMethod.invoke(parameter, 0);
}
if (fieldClass.isAssignableFrom(Long.class)) {
writeMethod.invoke(parameter, 0L);
}
if (fieldClass.isAssignableFrom(LocalDateTime.class)) {
writeMethod.invoke(parameter, LocalDateTime.now());
}
}
}
在这个拦截器中通过反射的方式为参数实例值为null的字段完成了赋默认值。这里只为几个我常用的类型(字符串、整型和日期类型)设置了默认值,如果还会用到其他值请自行修改setDefaultValue方法。
代码中有一个重要的点就是拦截器类上的注解@Signature:
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
这个注解指明了具体要拦截的类和方法。在这里,数据库操作主要是通过Executor类完成;并且不论是新增或是更新都是通过update方法实现的,args属性则明确了方法参数只有一个Object类型的值。
然后是让这个拦截器生效。代码如下:
@Configuration
public class MybatisExtConfigure {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addInterceptor() {
MybatisNullInterceptor interceptor = new MybatisNullInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
if (!containsInterceptor(configuration, interceptor)) {
configuration.addInterceptor(interceptor);
}
}
}
private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
try {
return configuration.getInterceptors().contains(interceptor);
} catch (Exception e) {
return false;
}
}
}
这里通过@Configuration完成的自定义拦截器的注入。
配置完成后,再次执行写入和更新的测试案例,一次通过,搞定!
发表评论