使用JDK1.8 default关键字解决MyBatis不支持方法重载问题

配置:
mybatis >= 3.4.2
JDK >= 1.8

问题

MyBatis是不支持在接口中定义重载方法(如下代码片段)的,然而方法重载是一个兼容复杂业务重要手段。

public interface AccountMapper {
	public List<Account> getAccounts(final String status);
	public List<Account> getAccounts(final String status, final int owner);
}

场景

用户账户,每个用户可以拥有多个账户,这是一个1:N的关系。我们简单定义它们的属性:

  • Account.java
public class Account {
	// 主键
	private int id;
	// 账户名字
	private String name;
	// 账户状态:active 或者 inactive
	private String status;
	// 拥有者id
	private int owner;

	// setters & getters
}
  • Owner.java
    我并不会使用到Owner这个对象,写出来只是让大家有个简单理解。
public class Owner {
	private int id;
	private String name;
	
	// setters & getters
}

我们尝试实现如下2个功能:

  1. 根据状态status获取所有Account;
  2. 根据状态status和所有者owner获取所有Account;

一个可能方案(使用注解@Param只是我的习惯,不是必要,只要参数名字能对上即可):

  • AccountMapper.java
public interface AccountMapper {

	/**
	 * 根据状态获取所有Account
	 * 
	 * @param status active or inactive
	 * @return list of accounts
	 */
	List<Account> getAccountsByStatus(@Param("status") String status);

	/**
	 * 根据状态和所有者获取所有Account
	 *
	 * @param status active or inactive
	 * @param owner owner id
	 * @return list of accounts
	 */
	List<Account> getAccountsByStatusAndOwner(@Param("status") String status, @Param("owner") int owner);
}

上面的实现是因为传入的参数数量不一致,所以需要拆开两个方法(深层次原因是因为mybatis不支持相同的方法名字但参数不同但方法,简单说重载方法会出现问题)。当参数的数量不断增加,日后可能需要写更多的方法,如何解决?

相信聪明的你已经想到:

  1. 提供全部参数签名的方法,结合动态SQL实现
  2. 使用HashMap来包裹参数,结合动态SQL实现

一个可能的写法:

  • AccountMapper.java
/**
 * get accounts according to status and owner
 * 
 * @param status active or inactive
 * @param owner owner id
 * @return list of accounts
 */
List<Account> getAccounts(@Param("status") String status, @Param("owner") int owner);

或者:

/**
 *get accounts by conditions
 *
 * @param args conditions
 * @return list of accounts
 */
List<Account> getAccounts(Map<String, Object> args);
  • AccountMapp.xml
<select id="getAccounts" resultType="overwrite.Account">
    select * from ACCOUNT
    <where>
        <if test="status != null">
            status = #{status}
        </if>
        <if test="owner != null">
            and owner = #{owner}
        </if>
    </where>
</select>

上面两种方法,都可以实现需求,但是无论哪种实现,都有其缺憾

  1. 带所有参数都方法:每次都需要显式传递null或-1来辨别是否有值,调用方式丑陋
  2. 使用Map包裹:调用者无法知道有所有可选参数以及参数的名字,调用参数不清晰

优雅的方案

使用JDK1.8提供的default关键字,并结合动态SQL可以很好的处理这个问题:

  • AccountMapper.java
    这里实现了方法的重载,较少参数的方法使用default关键字实现,调用其他重载方法并出入默认值。
public interface AccountMapper {
	/**
	 * get accounts according to status
	 *
	 * @param status active or inactive
	 * @return list of accounts
	 */
	default List<Account> getAccounts(@Param("status") String status) {
		return this.getAccounts(status, -1);
	}

	/**
	 * get accounts according to status and owner
	 * 
	 * @param status active or inactive
	 * @param owner owner id
	 * @return list of accounts
	 */
	List<Account> getAccounts(@Param("status") String status, @Param("owner") int owner);
}
  • AccountMapper.xml
    status是一个强制的参数(当前这个场景),而owner则属于可选参数(可以附带更多其他可选参数)。
<select id="getAccounts" resultType="overwrite.Account">
    select * from ACCOUNT
    where status = #{status}
    <!-- optional arguments -->
    <if test="owner > 0">
        and owner = #{owner}
    </if>
</select>
  • Main.java
public class Main {

	public static void main(String[] args) throws Exception {
		
		final String configuration = "overwrite/mybatis-config.xml";
		InputStream is = Resources.getResourceAsStream(configuration);
		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

		SqlSession session = factory.openSession();
		AccountMapper mapper = session.getMapper(AccountMapper.class);
		
		// 使用只有一个status参数的重载方法
		List<Account> accounts = mapper.getAccounts("active");
		accounts.forEach(account -> {
			System.out.println(account.getId());
			System.out.println(account.getName());
			System.out.println(account.getStatus());
			System.out.println(account.getOwner());
		});
		
		// 使用拥有两个参数(status和owner)的重载方法
		List<Account> accounts2 = mapper.getAccounts("inactive", 1);
		accounts2.forEach(account -> {
			System.out.println(account.getId());
			System.out.println(account.getName());
			System.out.println(account.getStatus());
			System.out.println(account.getOwner());
		});
	}
}

欢迎大家留言讨论,另外完整的例子:mybatis-examples