DZHG  / How to handle output parameter of callable statement in MyBatis
Table of contents

MyBatis is a first class persistence framework for Java. It’s feature rich and very straightforward in most cases. However, it’s missing documentation for advanced usage. The official document doesn’t have an concrete example for how to handle OUT parameters from an SQL procedure, specifically, a CURSOR type OUT parameter.

In this post, we will walk through a simple use case and demonstrate how to call a DB procedure with CURSOR type OUT parameter.

For example, we want to call a DB procedure to find all users from a specific country. The procedure has one IN parameter: p_country and an OUT parameter p_users. The OUT parameter p_users is a CURSOR to multiple User records.

Setup

Create the table:

CREATE TABLE users {
	id BIGINT PK
	first_name TEXT
	last_name TEXT
	country TEXT
}

Create the procedure:

CREATE PROCEDURE get_users_by_country(IN p_country VARCHAR, OUT p_users SYS_REFCURSOR) AS
BEGIN
	OPEN p_users FOR
	  SELECT id, first_name, last_name, country FROM users WHERE country=p_country;
END;

MyBatis

There are two ways to configure MyBatis mappers: XML or Java annotation. I prefer Java annotation. It combines the Java mapper interface and the actual query in the same place. It’s easy to navigate and maintain. The down side is, the annotations are not very descriptive in certain cases. Sometimes, you have to use multiple annotations to define one SQL mapper method while with XML, it might be just one XML tag.

So, let’s start to define our mapper interface first. Based on our requirement and the DB procedure, we may want to define our mapper method like below:

public interface UserMapper {
	List<User> getUserByCountry(String country);
}

Next, we would like to add MyBatis annotations and bind the method to the actual DB query. As the DB procedure is a query, we use Select annotation.

public interface UserMapper {
	@Results(id = "userMap", value = {
		@Result(property = "id", column = "id"),
		@Result(property = "firstName", column = "first_name"),
		@Result(property = "lastName", column = "last_name"),
		@Result(property = "country", column = "country")
	})
	@Select("{ call get_users_by_country(#{p_country}, #{p_users, mode=OUT, jdbcType=CURSOR, resultMap=userMap}) }")
	@Options(statementType = StatementType.CALLABLE)
	@ResultType(User.class)
	List<User> getUserByCountry(@Param("p_country") String country);
}

Besides @Select annotation, we also need to use @Options to specify the statement type is CALLABLE. @ResultType tells MyBatis convert the rows in the ResultSet to User class. @Results annotation defines the result mapping between the User class and the rows in the ResultSet.

It looks good till now. But when you run the query by calling the mapper method:

SqlSessionFactory factory = ...;
try (SqlSession session = factory.openSession()) {
    SampleMapper mapper = session.getMapper(UserMapper.class);
    List<User> result = mapper.getUserByCountry("Germany");
    System.out.println(result);
}

It doesn’t work. The method returns null. What’s going on? It turns out MyBatis doesn’t take OUT parameter as return value. It makes sense since there might be multiple OUT parameters in a procedure. So, how can we retrieve the value of OUT parameters? After reading the source code, the OUT parameters are being set to metaParam. metaParam is the object holding all parameters for a bound SQL in MyBatis. For example, for getUserByCountry method, the metaParam will contain a key p_country. After executing the statement, the value of OUT parameter will be set to the metaParam by key p_users. But how can we retrieve the value from metaParam? We have to provide the metaParam if we want to retrieve values for OUT parameters. So, we have to change our mapper method to:

public interface UserMapper {
	@Results(id = "userMap", value = {
		@Result(property = "id", column = "id"),
		@Result(property = "firstName", column = "first_name"),
		@Result(property = "lastName", column = "last_name"),
		@Result(property = "country", column = "country")
	})
	@Select("{ call get_users_by_country(#{country}, #{users, mode=OUT, jdbcType=CURSOR, resultMap=userMap}) }")
	@Options(statementType = StatementType.CALLABLE)
	@ResultType(User.class)
	void getUserByCountry(Map<String, Object> params);
}

Note that, the method no longer has return values. And, the argument is a Map now, instead of the original single String argument. As we pass in a Map. MyBatis will use it as metaParam. We can retrieve the value of users from the Map after executing the query.

SqlSessionFactory factory = ...;
try (SqlSession session = factory.openSession()) {
    SampleMapper mapper = session.getMapper(UserMapper.class);
    Map<String, Object> params = new HashMap<>();
    params.put("country", "Germany");
    mapper.getUserByCountry(params);
    System.out.println(params.get("users"));
}

We can improve it and make it more strong-typed by using a POJO class:

public class GetUserByCountryParams {
	private String country;
	private List<User> users;

	private GetUserByCountryParams(String country) {
		this.country = country;
	}

	public String getCountry() {
		return this.country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public List<User> getUsers() {
		return this.users;
	}

	public void setUsers(List<User> users) {
		this.users = users;
	}

	public static GetUserByCountryParams create(String country) {
		return new GetUserByCountryParams(country);
	}
}

We can replace the Map as a bean instance:

SqlSessionFactory factory = ...;
try (SqlSession session = factory.openSession()) {
    SampleMapper mapper = session.getMapper(UserMapper.class);
    GetUserByCountryParams params = GetUserByCountryParams.create("Germany");
    mapper.getUserByCountry(params);
    System.out.println(params.getUsers());
}

Conclusion

To retrieve the values of OUT parameters in a callable statment with MyBatis, we need to use a Map or a bean as the parameter object to the mapper method. MyBatis will set the values of OUT parameters to the Map or bean using the parameter name specified in the SQL. After invoking the mapper method, we can get the value by the parameter names from the parameter object.