今天面试的时候面试官对于一个查询场景提出了一系列问题。
现有以下表
CREATE TABLE `sys_dept` (
`id` bigint(20) NOT NULL COMMENT 'id',
`pid` bigint(20) NULL DEFAULT NULL COMMENT '上级ID',
`pids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有上级ID,用逗号分开',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称',
`sort` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '排序',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
`create_date` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updater` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`update_date` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_pid`(`pid`) USING BTREE,
INDEX `idx_sort`(`sort`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门管理' ROW_FORMAT = DYNAMIC;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL COMMENT 'id',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
`real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
`head_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
`gender` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '性别 0:男 1:女 2:保密',
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
`dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
`super_admin` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '超级管理员 0:否 1:是',
`status` tinyint(4) NULL DEFAULT NULL COMMENT '状态 0:停用 1:正常',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
`create_date` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updater` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`update_date` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_username`(`username`) USING BTREE,
INDEX `idx_create_date`(`create_date`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统用户' ROW_FORMAT = DYNAMIC;
要查询用户列表,模糊匹配姓名,模糊匹配部门名称、准确匹配年龄。
面试官要求不能使用联合查询,不能引入其他依赖,只能使用 Mybatis-plus 查询数据。
开始我的查询逻辑是先查部门表,通过模糊查询将所有部门查出来存入 Map,key 为部门 id,value 为部门名称。
之后通过条件,查询出对应的 user 信息,然后将再按 Map 中的 key-value 为最后的返回值填入部门名称。
这时候面试官提出问题了,先查部门,如果部门有1000多万的话,内存中存这样一个 Map 可行吗。
他这么一问,成功把我唬住了,哪家公司能有1000多万部门。
不过总体还是了解了一下 N+1 问题这个知识点。
N+1 问题是当你进行一次查询,后面因为关联查询的需求,你需要再遍历查询出来的表,每次遍历的时候再进行一次相关 MySQL 查询。造成巨大的查询压力。
解决该问题的方法有:
-
使用 JOIN,但是这次面试官不允许使用关联查询
SELECT u.id, u.username, d.name AS deptName FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id;这样一条 sql 就可以解决该问题。
-
一次性批量查询,将数据暂存本地,之后直接本地访问。用空间换时间。
为什么面试官会给出一个不能使用关联查询的条件呢。因为可能有以下失效场景:
-
笛卡儿积,关联查询其实就是笛卡尔积的应用,一张表的查询操作可能会很快,但是多张表联查就会非常慢,因为他们的数据量是n*m,所以有时候采用连接查询,还不如分成多次查询来的快。
-
分库分表,如果系统的数据库采用的是分库分表,这个时候有些表是不能够进行连接查询,我们只能分多次查询,然后组装到一起。
-
数据来源不一致,如果订单的数据是从第三方接口获取的,那我们自然没办法进行连表查询。
public SysUserVo querySysUser(SysUserDto dto) {
SysUserDto.Condition cond = dto.getCondition();
// 用户名为空可选择返回空 或 查询全部,看系统需求,但是题干没给
if (!StringUtils.hasText(cond.getUserName())) {
return new SysUserVo().setTotal(0).setList(Collections.emptyList());
}
/* ---------- 1. 查询部门 ---------- */
LambdaQueryWrapper<SysDept> deptQuery = Wrappers.lambdaQuery(SysDept.class)
.select(SysDept::getId, SysDept::getName);
if (StringUtils.hasText(cond.getDeptName())) {
deptQuery.like(SysDept::getName, cond.getDeptName());
}
List<SysDept> sysDepts = sysDeptMapper.selectList(deptQuery);
List<Long> deptIds = sysDepts.stream().map(SysDept::getId).collect(Collectors.toList());
Map<Long, String> deptNameMap = sysDepts.stream()
.collect(Collectors.toMap(SysDept::getId, SysDept::getName));
/* ---------- 2. 查询用户 ---------- */
Page<SysUser> page = new Page<>(dto.getPage().getCurrent(), dto.getPage().getSize());
LambdaQueryWrapper<SysUser> userQuery = Wrappers.lambdaQuery(SysUser.class)
.like(SysUser::getUsername, cond.getUserName());
if (cond.getGender() != null) {
userQuery.eq(SysUser::getGender, cond.getGender());
}
if (!deptIds.isEmpty()) {
userQuery.in(SysUser::getDeptId, deptIds);
}
Page<SysUser> sysUserPage = sysUserMapper.selectPage(page, userQuery);
List<SysUser> sysUsers = sysUserPage.getRecords();
/* ---------- 3. 补全部门信息 ---------- */
if (!sysUsers.isEmpty()) {
List<Long> userDeptIds = sysUsers.stream().map(SysUser::getDeptId).collect(Collectors.toList());
List<Long> missing = userDeptIds.stream()
.filter(id -> !deptNameMap.containsKey(id))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
List<SysDept> missingDepts = sysDeptMapper.selectBatchIds(missing);
missingDepts.forEach(d -> deptNameMap.put(d.getId(), d.getName()));
}
}
/* ---------- 4. 组装结果 ---------- */
List<SysUserVo.SysUserVoElement> list = sysUsers.stream().map(u -> {
SysUserVo.SysUserVoElement e = new SysUserVo.SysUserVoElement();
e.setId(u.getId());
e.setUsername(u.getUsername());
e.setGender(u.getGender());
e.setRealName(u.getRealName());
e.setDeptName(deptNameMap.get(u.getDeptId()));
return e;
}).collect(Collectors.toList());
return new SysUserVo()
.setList(list)
.setTotal(sysUserPage.getTotal());
}
下面看一下我按笔试题规则构建的部分 DTO 和 VO
@Data
@Accessors(chain = true)
public class SysUserDto {
private Condition condition;
private Page page;
@Data
public static class Condition {
private String deptName;
private int gender;
private String userName;
}
@Data
public static class Page {
private int size;
private int current;
private int total;
}
}
@Data
@Accessors(chain = true)
public class SysUserVo {
private List<SysUserVoElement> list;
private long total;
@Data
public static class SysUserVoElement {
private String username;
private int gender;
private String realName;
private long id;
private String deptName;
}
}
这两个用到了静态内部类,我们知道,一般使用非静态内部类是用来表示内部类和外部类的生命周期强绑定。内部类不能单独作为一个独立类出现。而静态内部类的生命周期不受外部类作用,使用他主要的意图是强聚合,表明该静态内部类只在对应的 dto 和 vo 中才有意义。单独使用并没有任何意义。JDK 源码中也大量使用了内部类。其实还有一种类内部接口,最典型的就是 JDK Map 中的 Entry 了,表示这个 Entry 规范的方法只能被 Map 使用,也可以看作是对类功能的进一步补充。
例如:
public class Fruits {
public interface Apple {
public String info();
}
}
public class Test {
public static void main(String[] args) {
Fruits.Apple apple = new Fruits.Apple() {
@Override
public String info() {return "I'm an Apple";}
};
System.out.println(apple.info());
}
}
具体还不能言说其意。