背景
越来越多的场景下,从数据库获取数据被要求简单、不得包含更多的业务逻辑,而是建议单纯的打中【索引】取【合理数量】的数据至内存中,再通过代码进行二次处理。
在这一样的背景下,通过
steam
相关方法进行二次数据处理感觉是一个较为方便的方式。
场景
我们构建相关场景,并建立相关表进行后续案列表述。
相关项目地址: github(含sql语句)
1.数据表相关
- 班级表
CREATE TABLE `t_test_class`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
`name` varchar(32) NOT NULL COMMENT '名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='班级表';
- 爱好表
CREATE TABLE `t_test_hobby`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
`desc` varchar(32) NOT NULL COMMENT '描述',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='爱好';
- 学生表
CREATE TABLE `t_test_student`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
`class_id` bigint(20) unsigned NOT NULL COMMENT '班级id',
`hobby_id` bigint(20) unsigned NOT NULL COMMENT '兴趣班id',
`age` int(11) unsigned NOT NULL COMMENT '年龄',
`last_attend_time` timestamp NULL DEFAULT NULL COMMENT '最后一次参加兴趣班时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='学生表';
模型比较简单,这次的案例主要涉及学生相关事项;学生:班级=1:1 , 学生:爱好=1:1。
为了后续扩展相关案例比较方便,搭建了一个springboot+mybatis plus 的一个案例,做数据填充跟持久层数据交互。
2.代码程序相关
- 基础测试引导类 :cn.lcy.stream#
BaseTransactionTest
@Slf4j
@Rollback(value = true)
@SpringBootTest(classes = StreamApplication.class)
public class BaseTransactionTest extends AbstractTransactionalTestNGSpringContextTests {
@Test
public void testEnv() throws InterruptedException {
Thread.sleep(2000L);
System.out.println("test env running");
}
}
- cn.lcy.stream.demo1#
StreamApplicationTests
extends BaseTransactionTest
数据初始化,方便后续测试。
@BeforeClass(description = "初始化数据")
public void initData() {
Long classId = 10L;
Long hobbyId = 100L;
//create students;
ArrayList<StudentDO> firstConditionHobbyAndClass = new ArrayList<>();
for (int i = 0; i < 10; i++) {
StudentDO studentDO = new StudentDO();
studentDO.setClassId(classId + RandomUtils.nextInt(5));
studentDO.setHobbyId(hobbyId + RandomUtils.nextInt(5));
studentDO.setAge(RandomUtils.nextInt(18));
studentDO.setLastAttendTime(DateTime.now().plusHours(RandomUtils.nextInt(10)).toDate());
firstConditionHobbyAndClass.add(studentDO);
}
studentDataSupport.saveBatch(firstConditionHobbyAndClass);
}
case 1 :
从简单开始,第一个案例是根据一个字段或者多个字段对List等进行排序。
@Test(description = "case1 : 根据字段排序")
public void sortTest() {
//当前的全量学生
List<StudentDO> requestData = studentDataSupport.list();
List<StudentDO> sortList = requestData.stream().sorted(
Comparator.comparing(StudentDO::getAge) //首先根据age排序
.thenComparing(StudentDO::getHobbyId).reversed()//其次根据id编号排序
).collect(Collectors.toList());
List<StudentDO> sortList2 = requestData.stream().sorted(Comparator.comparing(StudentDO::getAge))
.collect(Collectors.toList());
log.info("sortList response = {} ", sortList);
log.info("sortList2 response = {} ", sortList);
}
case 2 :
下面是一个分组的归并;
当groupBy的字段于list中的元素重复时,使用List进行装载。
目前发现两种比较好用的方式,分别是stream
与guava中Multimaps
。
简单测试了下,10万数据量下两种方法耗时对于业务开发可以忽略不计。
@Test(description = "case2 : 根据hobbyId对students进行分组,如果hobbyId重复,则使用集合进行装填;")
public void extractedByHobbyId() {
//当前的全量学生
List<StudentDO> requestData = studentDataSupport.list();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//test stream
Map<Long/*hobby_id*/, List<StudentDO>/*hobby_id对应的students*/> result = requestData.stream()
.collect(
Collectors.toMap(
StudentDO::getHobbyId, // key wrapper
s -> { // value wrapper
List<StudentDO> list = new ArrayList<>();
list.add(s);
return list;
},
(List<StudentDO> value1, List<StudentDO> value2) -> { //mergeFunc
value1.addAll(value2);
return value1;
}
)
);
stopWatch.stop();
log.info("stream : elapsed[{}]", stopWatch.getTime());
log.info("stream : size[{}]", result.size());// elapsed[27]
stopWatch.reset();
stopWatch.start();
//test Multimaps
ImmutableListMultimap<Long, StudentDO> index = Multimaps.index(requestData, StudentDO::getHobbyId);
log.info("guava : elapsed[{}]", stopWatch.getTime());//elapsed[58]
}
case 3:
根据condition进行分组,感觉蛮好用但是比较冷僻的方法。
@Test(description = "case3 : 根据表达式进行分组")
public void testCondition() {
List<StudentDO> requestData = studentDataSupport.list();
Map<Boolean/*true 有兴趣班*/, List<StudentDO>> haveHobbyStudents =
requestData.stream().collect(Collectors.groupingBy(e -> e.getHobbyId() > 0));
}
case 4:
重点
,根据字段进行分组,对于分组后的数据进行排序。
- 题设:根据hobby_id进行分组,随后对分组后的数据选取last_attend_time字段最大的记录。
@Test(description = "case4 : 根据字段分组随后进行排序并选取top1或者topN")
public void groupAndThenSort() {
//当前的全量学生
List<StudentDO> requestData = studentDataSupport.list();
//方法一: 先分组,随后从组内寻找【指定】数据
Collector<StudentDO, ?, Optional<StudentDO>> sortFunction = Collectors.reducing((c1, c2) -> c1.getAge() > c2.getAge() ? c1 : c2);
Map<Long/*hobby_id*/, StudentDO> groupAndSorting1 = requestData.stream()
.collect(Collectors.groupingBy(StudentDO::getHobbyId, // 先根据HobbyId分组
Collectors.collectingAndThen(sortFunction, Optional::get)));// 随后排序
//方法二: 当key冲突时,以怎样的方式保留唯一key相关的数据
Map<Long/*hobby_id*/, StudentDO> groupAndSorting2 = requestData.stream()
.collect(Collectors.toMap(StudentDO::getHobbyId, Function.identity(), (c1, c2) -> c1.getAge() > c2.getAge() ? c1 : c2));// 随后排序
//case 3: 类似的sql
/**
* SELECT *
* FROM (SELECT id,
* hobby_id,
* class_id,
* last_attend_time,
* IF(@bak = hobby_id, @rounum := @rounum + 1, @rounum := 1) AS
* row_number,
* @bak := hobby_id
* FROM (SELECT id,
* hobby_id,
* class_id,
* last_attend_time
* FROM t_test_student
* ORDER BY hobby_id ASC,
* last_attend_time DESC) a,
* (SELECT @rounum := 0,
* @bak := 0) b) c
* WHERE c.row_number < 2
*/
}
- groupAndSorting1 是做groupby随后进行andThen调用一个func,func中进行stream reduce计算。
- groupAndSorting2 是将groupBy至一个map中的思路,其中做处理的方式主要表述在key重复的时候,如何选取。
- SQL的方式比较通用,但同样写法与受众度比较小众,我们分拆来理解下。
SQL 解释
:总体思路是想数据先以期望得方式进行排序,随后添加相关计数器,对出现的有序数据进行计数。因为原数据已拍过序,所有计数器约大的数据说明越在尾端。
- 当前语句很简单的根据hobby_id 与 last_attend_time 进行排序,这一步得到的结果已经得到了相关期望顺序。
- 当前语句为声明两个变量,变量一为rownum,初始值为0。变量二为bak,用于记录当前记录读到的hobby_id。
- 当前语句看似麻烦,实则理解起来不为麻烦。
IF(@bak = hobby_id, @rounum := @rounum + 1, @rounum := 1) 共由三部分内容组成.3.1 :
@bak = hobby_id
:为条件表达式,指bak变量当前的值若等于hobby_id。
3.2 :@rounum := @rounum + 1
: 如果表达式成立,对当前记录的rounum进行+1。
3.3 :@rounum := 1
:如果表达式不成立,则使rounum等于1.
因为在前一步骤中,已经对数据进行过相应排序,而rownumber对应的数字越大说明排序越后。
- 最终将上述语句套起来,选取需要的topN条即可。