目录
1、业务场景需求
因最近所做项目,有个功能实现实现,查询班级的学员和学员测验完成状态;其中涉及的表有如下关系:
表中文名称 | 表英文名称 | 关联关系 |
---|---|---|
班级学生 | class_student | 关联student_id |
学生表 | student | 学生基本信息 |
学生测验结果表 | student_exam_record | 学员课程测验记 |
目前要求实现功能,能够按照学生手机号/姓名模糊搜索、测验状态能够按照【全部、完成、未完成】方式搜索实现相关功能。具体界面如下:
2、实现方案
2.1、简单版全用后端SQL实现
通过班级id与学生表 left join 查询获得班级下所有学员信息,然后继续left join连接班级测验表,获得有测验的(已经交卷或者未交卷的学员有效记录) 伪SQL代码如下:
select 所需字段 from
(select * from class_student where cs.class_id=20) ocs left join student s on ocs.student_id=s.id left join (select * from student_exam_record t where t.class_id=20 and exam_id =15) ser on ocs.student_id=ser.student_id
2.2、后端Java代码分两步骤查询和实现
为了减少与数据库关联表张数目,以及在实际业务之中可能关联更多表,超过了3张表。于是有了分解两步实现方案。
1、首先查询出指定班级包含的学员记录信息通过 left join 学生表,获得详情信息 查询之中加入 姓名、手机号搜索条件
2、查询出对应班级下,某个测验试卷的答题记录(已完成/未交卷)以及筛选条件交卷状态【全部、已完成、未开始】
注意事项:首先通过姓名、手机号模糊搜索出班级学员记录,以此学员记录student_id和测验状态 作为过滤条件在学生测验答题结果有效记录。此处特别注意:未开始【部分答题中、未答过任务测验的学生】
获得结果逻辑过程:过滤学员记录、用过滤的学员记录ids作为查询条件传递给学员测验记录进行 状态和学员过滤操作。最后展现结果的时候需要有以下几点注意事项:
1、测验状态全部:以学员过滤记录返回的结果集为基础显示结果记录,因为测验记录虽然有为交卷、已交卷记录。但是还有部分学员未参与测验的也需要显示。
2、测验状态为已完成:则以已经完成交卷的学员记录未基准,此时可能一个班级50名学员,实际当前时间点只有10名学员完成测验,可能有15名学员未交卷,还有25名学员未参加测验的。此种状态相对显示记录最少。但也在某个时段有可能全部学员完成了测验。
3、未完成状态:此时需要显示的未交卷学员部分、未回答测验的学员。意味着需要显示记录未筛选学员记录集合去除已经完成测验记录部分的学员。
4、因分拆为两步骤实现,在返回最后结果记录的时候要注意,两个List集合循环时候,只有在测验状态为全部、未完成才会出现部分未参与测验的学员会显示出来,已完成测验时候显示记录不显示未参与学员记录。
5、还有因为未参与测验的学员在测验记录表之中无记录,无法获得测验试题总数,所以为了统一获得测验卷的试题总数需要通过测验id获得测验总题目数量。
3、实现代码
/**
* 获得班级课程某个测验所有学员回答情况
*
* @param mapParams
* @return
*/
@Override
public List<ClazzCourseOneExamStudentAnswerVO> selectClazzCourseQuizStudentAnswerResultList(Map<String, Object> mapParams) {
//1、查询出满足过滤条件的班级学员
List<ClazzStudentFilterVO> clazzStudentFilterList = clazzCourseExamExtendMapper.selectClazzStudentFilterList(mapParams);
//2、根据测验状态返回不同记录
String quizStatus = mapParams.get("quizStatus").toString();
List<Long> studentIdList = clazzStudentFilterList.stream().map(
clazzStudent -> clazzStudent.getStudentId()).collect(Collectors.toList());
List<ClazzCourseOneExamStudentAnswerVO> courseQuizStudentAnswerResultList = new ArrayList<>();
String clazzExamId = mapParams.get("clazzExamId").toString();
Integer quizQuestionTotal = clazzCourseExamExtendMapper.getClazzCourseQuizHasQuestionsTotal(clazzExamId);
if (clazzStudentFilterList.size() == 0) {
return courseQuizStudentAnswerResultList;
}
switch (quizStatus) {
// 全部
case "0":
//2.2 根据测验状态 全部时候获得本班级所有学员答题记录
mapParams.put("studentIdList", studentIdList);
List<ClazzCourseExamStudentAnswerResultVO> allStudentAnsweredList = clazzCourseExamExtendMapper.selectClazzQuizAllStudentAnsweredList(mapParams);
courseQuizStudentAnswerResultList = assembleClazzCourseQuizStudentAnswerResultList(
clazzStudentFilterList, allStudentAnsweredList, quizStatus, quizQuestionTotal);
break;
// 已经完成
case "1":
//2.1、选择已经完成 过滤出筛选条件下所有已经完成的有效记录
mapParams.put("studentIdList", studentIdList);
List<ClazzCourseExamStudentAnswerResultVO> allStudentAnsweredFinishList = clazzCourseExamExtendMapper.selectClazzQuizAllStudentFinishList(mapParams);
if (allStudentAnsweredFinishList.size() == 0) {
return courseQuizStudentAnswerResultList;
} else {
courseQuizStudentAnswerResultList = assembleClazzCourseQuizStudentAnswerResultList(
clazzStudentFilterList, allStudentAnsweredFinishList, quizStatus, quizQuestionTotal);
}
break;
// 未完成
case "2":
//2.3、选择未完成 首先过滤出已经答卷但是未交卷测验记录
mapParams.put("studentIdList", studentIdList);
// 通过获得已经完成的学员idList从已经过滤条件学员去除掉已经完成部分
List<ClazzCourseExamStudentAnswerResultVO> studentAnsweredFinishList = clazzCourseExamExtendMapper.selectClazzQuizAllStudentFinishList(mapParams);
List<Long> finishStudentIdList = studentAnsweredFinishList.stream().map(
clazzStudent -> clazzStudent.getStudentId()).collect(Collectors.toList());
// 差集 (过滤用户List - 已经完成学员lIst)
List<Long> differenceStudentList = studentIdList.stream().filter(item -> !finishStudentIdList.contains(item)).collect(Collectors.toList());
//执行查询获得未交卷的测验记录
mapParams.put("studentIdList", differenceStudentList);
List<ClazzCourseExamStudentAnswerResultVO> noSubmitStudentAnsweredList = clazzCourseExamExtendMapper.selectClazzQuizNoSubmitStudentAnsweredList(mapParams);
//通过过滤获得记录未回答学员信息
//获得去除已经提交测验,没有任何开始的学员
clazzStudentFilterList = clazzStudentFilterList.stream().filter(studentFilter -> !finishStudentIdList.contains(studentFilter.getStudentId())).collect(Collectors.toList());
courseQuizStudentAnswerResultList = assembleClazzCourseQuizStudentAnswerResultList(clazzStudentFilterList, noSubmitStudentAnsweredList,
quizStatus, quizQuestionTotal);
break;
}
return courseQuizStudentAnswerResultList;
}
/**
* 根据结果组装结果返回给结果
*
* @param clazzStudentFilterList
* @param studentAnsweredResultList
* @param quizStatus
* @param quizQuestionTotal
* @return
*/
private List<ClazzCourseOneExamStudentAnswerVO> assembleClazzCourseQuizStudentAnswerResultList
(List<ClazzStudentFilterVO> clazzStudentFilterList,
List<ClazzCourseExamStudentAnswerResultVO> studentAnsweredResultList,
String quizStatus, Integer quizQuestionTotal) {
List<ClazzCourseOneExamStudentAnswerVO> courseQuizStudentAnswerResultList = new ArrayList<>();
//通过循环组装结果返回
for (ClazzStudentFilterVO clazzStudent : clazzStudentFilterList) {
Long studentId = clazzStudent.getStudentId();
ClazzCourseOneExamStudentAnswerVO oneExamStudentAnswer = null;
boolean isAddFlag = false;
if (studentAnsweredResultList.size() > 0) {
for (ClazzCourseExamStudentAnswerResultVO studentExamAnswered : studentAnsweredResultList) {
Long answerStudentId = studentExamAnswered.getStudentId();
switch (quizStatus) {
// 全部
case "0":
oneExamStudentAnswer = new ClazzCourseOneExamStudentAnswerVO();
oneExamStudentAnswer.setStudentId(studentId);
oneExamStudentAnswer.setClazzStudentNumber(clazzStudent.getClazzStudentNumber());
oneExamStudentAnswer.setStudentName(clazzStudent.getName());
if (studentId.equals(answerStudentId)) {
oneExamStudentAnswer.setBeginTime(studentExamAnswered.getBeginTime());
oneExamStudentAnswer.setEndTime(studentExamAnswered.getEndTime());
oneExamStudentAnswer.setUsedTime(studentExamAnswered.getUsedTime());
oneExamStudentAnswer.setRightCount(studentExamAnswered.getRightCount());
oneExamStudentAnswer.setErrorCount(studentExamAnswered.getErrorCount());
oneExamStudentAnswer.setTotalCount(quizQuestionTotal);
oneExamStudentAnswer.setScore(studentExamAnswered.getScore());
if (studentExamAnswered.getStatus().equals(BusinessConstants.CONSTANT_INT_ONE)
|| studentExamAnswered.getStatus().equals(BusinessConstants.CONSTANT_INT_THREE)) {
oneExamStudentAnswer.setQuizStatus(BusinessConstants.CONSTANT_INT_ONE);
} else {
oneExamStudentAnswer.setQuizStatus(BusinessConstants.CONSTANT_INT_TWO);
}
courseQuizStudentAnswerResultList.add(oneExamStudentAnswer);
isAddFlag = true;
}
break;
// 已经完成
case "1":
oneExamStudentAnswer = new ClazzCourseOneExamStudentAnswerVO();
oneExamStudentAnswer.setStudentId(studentId);
oneExamStudentAnswer.setClazzStudentNumber(clazzStudent.getClazzStudentNumber());
oneExamStudentAnswer.setStudentName(clazzStudent.getName());
if (studentId.equals(answerStudentId)) {
oneExamStudentAnswer.setBeginTime(studentExamAnswered.getBeginTime());
oneExamStudentAnswer.setEndTime(studentExamAnswered.getEndTime());
oneExamStudentAnswer.setUsedTime(studentExamAnswered.getUsedTime());
oneExamStudentAnswer.setRightCount(studentExamAnswered.getRightCount());
oneExamStudentAnswer.setErrorCount(studentExamAnswered.getErrorCount());
oneExamStudentAnswer.setTotalCount(quizQuestionTotal);
oneExamStudentAnswer.setScore(studentExamAnswered.getScore());
if (studentExamAnswered.getStatus().equals(BusinessConstants.CONSTANT_INT_ONE)
|| studentExamAnswered.getStatus().equals(BusinessConstants.CONSTANT_INT_THREE)) {
oneExamStudentAnswer.setQuizStatus(BusinessConstants.CONSTANT_INT_ONE);
}
courseQuizStudentAnswerResultList.add(oneExamStudentAnswer);
isAddFlag = true;
}
break;
// 未完成
case "2":
oneExamStudentAnswer = new ClazzCourseOneExamStudentAnswerVO();
oneExamStudentAnswer.setStudentId(studentId);
oneExamStudentAnswer.setClazzStudentNumber(clazzStudent.getClazzStudentNumber());
oneExamStudentAnswer.setStudentName(clazzStudent.getName());
if (studentId.equals(answerStudentId)) {
oneExamStudentAnswer.setBeginTime(studentExamAnswered.getBeginTime());
oneExamStudentAnswer.setEndTime(studentExamAnswered.getEndTime());
oneExamStudentAnswer.setUsedTime(studentExamAnswered.getUsedTime());
oneExamStudentAnswer.setRightCount(studentExamAnswered.getRightCount());
oneExamStudentAnswer.setErrorCount(studentExamAnswered.getErrorCount());
oneExamStudentAnswer.setTotalCount(quizQuestionTotal);
oneExamStudentAnswer.setScore(studentExamAnswered.getScore());
oneExamStudentAnswer.setQuizStatus(BusinessConstants.CONSTANT_INT_TWO);
courseQuizStudentAnswerResultList.add(oneExamStudentAnswer);
isAddFlag = true;
}
break;
}
}
}
if ((quizStatus.equals("0") || quizStatus.equals("2")) && (isAddFlag == false)) {
oneExamStudentAnswer = new ClazzCourseOneExamStudentAnswerVO();
oneExamStudentAnswer.setStudentId(studentId);
oneExamStudentAnswer.setClazzStudentNumber(clazzStudent.getClazzStudentNumber());
oneExamStudentAnswer.setStudentName(clazzStudent.getName());
oneExamStudentAnswer.setBeginTime(BusinessConstants.CONSTANT_ZERO);
oneExamStudentAnswer.setEndTime(BusinessConstants.CONSTANT_ZERO);
oneExamStudentAnswer.setUsedTime(BusinessConstants.CONSTANT_ZERO);
oneExamStudentAnswer.setRightCount(BusinessConstants.CONSTANT_INT_ZERO);
oneExamStudentAnswer.setErrorCount(BusinessConstants.CONSTANT_INT_ZERO);
oneExamStudentAnswer.setTotalCount(quizQuestionTotal);
oneExamStudentAnswer.setQuizStatus(BusinessConstants.CONSTANT_INT_TWO);
oneExamStudentAnswer.setScore(BusinessConstants.CONSTANT_INT_ZERO);
courseQuizStudentAnswerResultList.add(oneExamStudentAnswer);
}
}
return courseQuizStudentAnswerResultList;
}
4、成果展现
5、扩展实现(内存分页)
如果业务需求需要分页,遇见为了不在数据库之中关联多张表的情况下,并且分步骤计算不同的List记录集,最后组装为业务需求返回的结果集合,最后需要对组装后的结果集进行分页。经过分析如果要交给数据库分页目前主流的分页实现方式有以下3种:
- 通过limit分页查询
- 通过Rowbounds
- 通过分页插件pagehelper
但是我们目前为了减少关联表,分拆步骤后获得查询结果,如果此时需要分页,只能针对组装后满足业务需求的List进行内存分页。
6、总结
在实际业务之中,需要根据实际场景和数据量大小确认是直接使用SQL关联表进行直接查询实现,还是使用分拆步骤实现。实现方式优缺点总结:
实现方式 | 优点 | 缺点 |
直接SQL关联 limit 方式分页 | 一条SQL语句进行条件关联查询获取最后结果 分页很方便快捷 | 某些公司要求关联表不能太多或者不能关联表,在这种限定条件是就不能使用此种方式 |
分拆步骤组合方式 | 能够分拆多部,减少了关联表查询 | 1、分拆步骤多,需要经过多步骤计算后在组装返回结果 2、分页方式不够灵活,只能借助内存分页方式是比较好实现方式 |