9

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

 3 years ago
source link: http://www.zhyea.com/2020/10/05/springboot-basic-11-mybatis-null.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK