MyBatis 一对多关系使用 @Many 注解
一对多关系
在讨论使用 MyBatis 实现一对多查找之前,有必要在数据表和数据类设计层次上明确一对多关系。
假设一个用户有多个账户。在最简单的情况下,用户表仅包含 user_id 和 user_name 列,而账户表则包含 account_id、account_name 和 user_id 外键,其中 *_id 列为各个表的主键。
现在来看数据类的设计。一个用户拥有多个账号,自然想到 User 类中包含一个 Account 列表。但是,Account 类中能包含 User 对象吗?
如果 Account 中包含 User 对象,这意味着用户的信息有泄露的可能(如果用户表还包含其他隐私信息)。比如,当我们只需要知道用户的某个账号时,却附赠了用户的一系列其他信息。另外这种互相包含的关系也容易导致写出无限递归的查询语句。Account 要为 User 赋值,而 User 中包含 Account 列表,Account 列表中的元素要为 User 赋值……
这里给出感觉合理的数据表和数据类模型。
# users table
CREATE TABLE `users` (
`user_id` int NOT NULL,
`user_name` varchar(45) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
// user 类,忽略 setter 和 getter
public class User {
private Integer userId;
private String userName;
private List<Account> accounts;
}
# accounts table
CREATE TABLE `accounts` (
`account_id` int NOT NULL,
`account_name` varchar(45) NOT NULL,
`user_id` int NOT NULL,
PRIMARY KEY (`account_id`),
KEY `user_id_idx` (`user_id`),
CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
// account 类,忽略 setter 和 getter
public class Account {
private Integer accountId;
private String accountName;
private Integer userId;
}
MyBatis 一对多查询
查询的主要难度在查询 User 上,其关键是理解 @Many 注解相关的参数。
先把简单的 AccountDao 搞了。
@Mapper
public interface AccountDao {
@Results(id = "accountResultMap", value = {
@Result(property = "accountId", column = "account_id", id = true),
@Result(property = "accountName", column = "account_name"),
@Result(property = "userId", column = "user_id")
})
@Select(value = "SELECT * FROM accounts WHERE account_id = #{id}")
public Account findAccountById(@Param("id") Integer id);
@ResultMap(value = "accountResultMap")
@Select(value = "SELECT * FROM accounts WHERE user_id=#{id}")
public List<Account> findAccountByUserId(@Param("id") Integer id);
}
注意:
这里我们定义了一个 findAccountByUserId 查询函数。因为 User 类在填充 Account 列表时,是用 user_id 进行查询的。
再看 UserDao。
@Mapper
public interface UserDao {
@Results(id = "userResultMap", value = {
@Result(property = "userId", column = "user_id", id = true),
@Result(property = "userName", column = "user_name"),
@Result(property = "accounts", column = "user_id", javaType = List.class,
many = @Many(select = "com.sugarian.dao.AccountDao.findAccountByUserId"))
})
@Select(value = "SELECT * FROM users WHERE user_id=#{id}")
public User findUserById(@Param("id") Integer id);
}
注意下面抽取的部分
@Result(property = "userName", column = "user_name"),
@Result(property = "accounts", column = "user_id", javaType = List.class,
many = @Many(select = "com.sugarian.dao.AccountDao.findAccountByUserId"))
})
User 中的 accounts 是存放 Account 的列表,其在表结构中是没有对应的列,所以它对应的这条 @Result 实际上是再次执行了一次 sql 索引,然后将值填充到 accounts 中。另外一点,请看 column 是 user_id,这实际上就是以 user_id 作为参数,findAccountByUserId
用进行索引。
我们可以看看调试工具给出的信息:
2022-11-25 23:39:09,552 [DEBUG] com.sugarian.dao.UserDao.findUserById - ==> Preparing: SELECT * FROM users WHERE user_id=?
2022-11-25 23:39:09,552 [DEBUG] com.sugarian.dao.UserDao.findUserById - ==> Parameters: 1(Integer)
2022-11-25 23:39:09,553 [DEBUG] com.sugarian.dao.AccountDao.findAccountByUserId - ====> Preparing: SELECT * FROM accounts WHERE user_id=?
2022-11-25 23:39:09,553 [DEBUG] com.sugarian.dao.AccountDao.findAccountByUserId - ====> Parameters: 1(Integer)
2022-11-25 23:39:09,556 [DEBUG] com.sugarian.dao.AccountDao.findAccountByUserId - <==== Total: 3
2022-11-25 23:39:09,556 [DEBUG] com.sugarian.dao.UserDao.findUserById - <== Total: 1
明显执行了两次 sql 查询。对于第二个查询,它从第一个查询结果中取出其中的 user_id 列,作为查询的参数。
其他参数如 javaType 指定返回类型。@Many 中的参数 select 指定第二次查询用的函数。
总结
也算是掌握了一个技术细节,由于自己没细看官方文档,所以浪费了很多时间,下次还是耐心看文档吧。