springboot入门11 – MyBatis写入时null问题统一处理方案

如果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完成的自定义拦截器的注入。

配置完成后,再次执行写入和更新的测试案例,一次通过,搞定!

示例代码:zhyea / spring-boot-database-pad

发表评论

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理