Intercepting MyBatis queries
By: Conor Prunty
Tags: Spring Boot, Mybatis, Java, Resultset

In a Spring Boot application, you may be using MyBatis as your persistence framework. MyBatis queries return null by default when all columns of a returned row are null.

Two straight forward ways to handle this are:

a) check for null responses for every MyBatis call in your code base

b) set the returnInstanceForEmptyRow property to true in your MyBatis configuration file — so MyBatis returns an empty instance instead. Note that it is also applied to nested results (i.e. collection and association). However, this might not be a useful use-case as then you’ll have an object with all its properties being null , and you’ll have to check for null for all of these and so on.

Solution

So the solution I propose here is to intercept the query, before it is returned to the code. Under the hood essentially.

What happens within the interceptor is entirely up to the developers, but I will show you how to set it up and an example of what can be done.

Create an interceptor class

import java.sql.Statement;

import java.util.List;

import org.apache.ibatis.executor.resultset.ResultSetHandler;

import org.apache.ibatis.plugin.Interceptor;

import org.apache.ibatis.plugin.Intercepts;

import org.apache.ibatis.plugin.Invocation;

import org.apache.ibatis.plugin.Plugin;

import org.apache.ibatis.plugin.Signature;

import org.mybatis.spring.MyBatisSystemException;

* Class to intercept mybatis query results

*

*

*/ /*** Class to intercept mybatis query results @author conorprunty@medium*/

args = {Statement.class})})

public class QueryInterceptor implements Interceptor { @Intercepts ({ @Signature (type = ResultSetHandler.class, method = “handleResultSets”,args = {Statement.class})})public class QueryInterceptor implements Interceptor { @Override

public Object intercept(Invocation invocation) throws Throwable {

/*

* Here we are intercepting the result of the invocation. The variable ‘result’ will contain

* the result of the query and we can then throw an exception or manipulate the result

* however necessary

*/

Object result = invocation.proceed();

List<?> queryResult = (List<?>) result;

if (queryResult == null || queryResult.isEmpty()) {

/*

* Note: the exception thrown here isn’t specifically important. Under the MyBatis hood

* — specifically SqlSessionInterceptor.java — it will unwrap the exception and throw a

* MyBatisSystemException

*/

throw new MyBatisSystemException(new Throwable());

}

return result;

} public Object intercept(Invocation invocation) throws Throwable {/** Here we are intercepting the result of the invocation. The variable ‘result’ will contain* the result of the query and we can then throw an exception or manipulate the result* however necessary*/Object result = invocation.proceed();List> queryResult = (List>) result;if (queryResult == null || queryResult.isEmpty()) {/** Note: the exception thrown here isn’t specifically important. Under the MyBatis hood* — specifically SqlSessionInterceptor.java — it will unwrap the exception and throw a* MyBatisSystemException*/throw new MyBatisSystemException(new Throwable());return result; @Override

public Object plugin(Object target) {

return Plugin.wrap(target, this);

} public Object plugin(Object target) {return Plugin.wrap(target, this); }

I have mentioned the two most important points in the comments in the code snippet above. Basically, the result variable holds the result of the MyBatis query. As previously mentioned, this is null by default. In my example above, this then checks that if the query is null (or an empty List — it can depend on your MyBatis mappings), then I throw an exception. I am throwing a MyBatisSystemException because it appears that within the make up of MyBatis, it will unwrap the exception thrown here anyway, and re-throw a MyBatisSystemException . You can specify different types and different methods in the @Intercepts annotation, more on that in the further reading section below.

Update/add the configuration xml

You need to update your mybatis-config.xml (or equivalent). If, like me, you didn’t have one at this point, then simply create one. The easiest place to put it is just with your properties files (this is within a Spring Boot application), so under src/main/resources.

You obviously need to make sure the path to the class ( QueryInterceptor ) matches the package it is in.

Setting the property

You now need to tell MyBatis to use this plugin, which in turn uses the manual interceptor we created.

To do this, you need to add the file to wherever you set your SqlSessionFactory in your Spring Boot configuration. How I set it is to add the file location as a property in my application.properties file:

mybatis.resource.file=mybatis-config.xml

Then add it to the configuration class

@Value("${mybatis.resource.file}")

private String resourceFile;

Then add that to my existing configuration

sqlSessionFactory.setConfigLocation(

new PathMatchingResourcePatternResolver().getResource(resourceFile)

);

And that’s it for the configuration.

Update codebase

So now that our Spring Boot application configuration is ready, we can make some minor tweaks to our code.

Remove any existing code where we check for null / empty List after sending request to a mapper interface as the exception will be thrown before the MyBatis request returns to the code.

For example:

Object obj = mapper.doSomeMyBatisRequest();



if(obj == null) { // <-- this is now unnecessary

// continue

}

We can remove the if statement here because if obj was null here, it would never make it back to the code (assuming we threw an exception like above).

Exception handling

As mentioned in the comments in QueryInterceptor.class , the underlying code will unwrap any Exception thrown and rethrow a MyBatisSystemException .

So the simplest way of handling it, should we still want a custom exception message, is to catch the MyBatisSystemException and throw whatever exception we require:

try {

Object obj = mapper.doSomeMyBatisRequest();

} catch (MyBatisSystemException mbse) {

throw new MyCustomException("error message here");

}

This isn’t necessary, but it’s nice to throw a specific exception where possible. You can go further then and handle MyCustomException in your Spring Boot global handling exception class, assuming you have one.

Conclusion

This is an implementation of intercepting and handling responses from MyBatis queries where there essentially is no valid entries in the result set. It saves littering the code with null checks, but it’s definitely a nice-to-have feature rather than essential.

Further Reading