笔者在使用 MyBatis 进行一对多查询的时候遇到一个奇怪的问题。对于笔者的一对多的查询结果,出现了这样的一个现象:原来每个组里有多个元素,查询目标是查询所查的组,以及每个组中的元素。但查询的结果却是变成了这样:每组元素变得只有一个,且总组数与元素数总数相等。举个例子,假设一共有 3 个组,每组 4 个元素。而现在的查询结果却是,显示出了 12 个组,每组 1 个元素。
笔者原来的表的情况比这要复杂很多,这里为了便于说明,简单抽象出这样一个情景。数据库中有很多用户(User),每个用户有他的好友分组(Folder),每个分组下面有该用户的好友(Contact)。现在需要查找这个用户所有的分组及好友,返回的数据结构需要是一个一个 List 分组,且一个分组中包含一个 List 好友。(List 指的是 Java 的一个内置的数据结构。)
User 表建表示例代码如下:
CREATE TABLE User (
id VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
# ...为了简化说明,此表省略其它字段...
PRIMARY KEY (id)
);
Folder 表建表示例代码如下:
CREATE TABLE Folder (
id VARCHAR(64) NOT NULL,
userId VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
# ...为了简化说明,此表省略其它字段...
PRIMARY KEY (userId, id),
# 因为上面的是复合主键,所以自动创建的是联合索引,而其它表的外键引用需要的是单个索引
INDEX idIndex (id),
FOREIGN KEY (userId) REFERENCES User (id)
);
Contact 表建表示例代码如下:
CREATE TABLE Contact (
id VARCHAR(64) NOT NULL,
# 表示此联系人属于谁的好友
userId VARCHAR(64) NOT NULL,
# 表示此联系人对应 User 中的 id
linkedUserId VARCHAR(64) NOT NULL,
folderId VARCHAR(64) NOT NULL,
# ...为了简化说明,此表省略其它字段...
PRIMARY KEY (userId, id),
# 因为上面的是复合主键,所以自动创建的是联合索引,而其它表的外键引用需要的是单个索引
INDEX idIndex (id),
# 同一个用户,不能拥有两个相同 ID 的 Contact
UNIQUE (userId, linkedUserId),
# 当复合主键成为外键时,必须整个复合主键一起作为外键,不能只引用复合主键其中的某个属性
FOREIGN KEY (userId) REFERENCES Folder (userId),
FOREIGN KEY (folderId) REFERENCES Folder (id),
FOREIGN KEY (linkedUserId) REFERENCES User (id)
);
建表示意图如下:
查询之后的 Java 数据结构如下:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class FolderWithContacts {
private Folder folder;
private List<Contact> contacts;
}
其中,
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Folder {
private String id;
private String userId;
private String name;
}
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Contact {
private String id;
private String userId;
private String linkedUserId;
private String folderId;
}
DAO 类代码如下:
public interface ContactDao {
List<FolderWithContacts> getFolderWithContacts(String userId);
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="XXX.xxx.ContactDao">
<resultMap id="folderResultMap" type="XXX.xxx.Folder">
<id property="id" column="folder_id"/>
<id property="userId" column="folder_user_id"/>
<result property="name" column="folder_name"/>
resultMap>
<resultMap id="contactResultMap" type="XXX.xxx.Contact">
<id property="id" column="contact_id"/>
<id property="userId" column="contact_user_id"/>
<result property="folderId" column="contact_folder_id"/>
<result property="name" column="contact_name"/>
resultMap>
<resultMap id="folderWithContactsResultMap" type="XXX.xxx.FolderWithContacts">
<association property="folder" resultMap="folderResultMap"/>
<collection property="contacts" javaType="java.util.ArrayList" resultMap="contactResultMap"/>
resultMap>
<select id="getFolderWithContacts" resultMap="folderWithContactsResultMap">
SELECT Folder.id AS folder_id,
Folder.userId AS folder_user_id,
Folder.name AS folder_name,
Folder.sequence AS folder_sequence,
Contact.id AS contact_id,
Contact.userId AS contact_user_id,
Contact.folderId AS contact_folder_id,
Contact.name AS contact_name,
Contact.description AS contact_description
FROM Contact,
Folder
WHERE Contact.folderId = Folder.id
AND Contact.userId = #{userId}
AND Folder.userId = #{userId}
ORDER BY folder_sequence ASC
select>
mapper>
以上就是笔者用于某个用户的好友分组及每个分组下的好友的示例代码。但使用上面的代码的查询会出现问题。如果一个用户有 3 个好友,每组 4 个好友,则上述代码的查询结果会变成,该用户有 12 个好友分组,每个分组 1 个好友。而且,上面的整个查询过程在运行中都是正常的,不会发生报错。而且返回结果的每个字段都没有出现 null 值。
可以看出,上面的代码会导致无法区分好友与分组,把好友当成分组返回了。
是什么原因出现上述问题呢?由于上面的整个查询过程都没有发生报错,且返回数据没有 null 值。因此不会是笔者的语法编写出现问题。
于是,笔者将上面的 SQL 单独在 MySQL 客户端命令行运行了一下,运行输出是正常的,确实是一个一对多查询的输出。一个一对多查询的输出,输出结果的数量应该和元素总个数相等,且同一个分组的所有元素关于这个分组的属性列的值应该也都是相等的。
这就说明并不是笔者 SQL 代码的问题,所以问题出现在 MyBatis 对 MySQL 输出结果的解析上。笔者非常确定,MyBatis 是肯定支持一对多查询的,因此一定是笔者关于 MyBatis 的 mapper 文件的编写出现问题。
笔者之后在不断地建新的更基本的表,进行一对多查询,终于让笔者发现了问题所在。
MyBatis 对于多表查询,要求组元素的字段必须是基本类型,而笔者编程时非常喜欢隔离、封装、解耦,擅自在上面将组元素的字段封装成了一个单独的类,然后把这个类的对象作为组元素的字段。在这种情况下,虽然 MyBatis 注入数据没有出问题,但它却没能识别出这是一对多查询的数据,因此将其当成一对一的数据来注入了。
可以看出,笔者在上面使用了
来映射一个 Java 对象,因此引发了上述问题。
知道原因就好办了。可以直接将上面类 Folder 的字段合并在类 FolderWithContacts,然后去掉类 Folder。
改进后的相关代码如下:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class FolderWithContacts {
private String id;
private String userId;
private String name;
private List<Contact> contacts;
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="XXX.xxx.ContactDao">
<resultMap id="contactResultMap" type="XXX.xxx.Contact">
<id property="id" column="contact_id"/>
<id property="userId" column="contact_user_id"/>
<result property="folderId" column="contact_folder_id"/>
<result property="name" column="contact_name"/>
resultMap>
<resultMap id="folderWithContactsResultMap" type="XXX.xxx.FolderWithContacts">
<id property="id" column="folder_id"/>
<id property="userId" column="folder_user_id"/>
<result property="name" column="folder_name"/>
<collection property="contacts" javaType="java.util.ArrayList" resultMap="contactResultMap"/>
resultMap>
<select id="getFolderWithContacts" resultMap="folderWithContactsResultMap">
SELECT Folder.id AS folder_id,
Folder.userId AS folder_user_id,
Folder.name AS folder_name,
Folder.sequence AS folder_sequence,
Contact.id AS contact_id,
Contact.userId AS contact_user_id,
Contact.folderId AS contact_folder_id,
Contact.name AS contact_name,
Contact.description AS contact_description
FROM Contact,
Folder
WHERE Contact.folderId = Folder.id
AND Contact.userId = #{userId}
AND Folder.userId = #{userId}
ORDER BY folder_sequence ASC
select>
mapper>
现在,这段代码运行起来,查询到的数据就是正常的了。