Mongo环境 3.4
需求
我们有一个题库,需要随机生成题目组合,为保证每道题都可能被选中,避免某次选题都是集中在某一段中,所以希望实现分段随机,每段随机取一部分数据
数据样例
db.sample.insert({"name": 1, "age": 20});
db.sample.insert({"name": 2, "age": 20});
db.sample.insert({"name": 3, "age": 20});
db.sample.insert({"name": 4, "age": 20});
db.sample.insert({"name": 5, "age": 20});
db.sample.insert({"name": 6, "age": 20});
db.sample.insert({"name": 7, "age": 20});
db.sample.insert({"name": 8, "age": 20});
db.sample.insert({"name": 9, "age": 20});
db.sample.insert({"name": 10, "age": 20});
db.sample.insert({"name": 11, "age": 20});
db.sample.insert({"name": 12, "age": 20});
实现原理
核心:使用Mongo 语法 $facet
, Mongo官方文档-$facet
官方文档说明:对同一输入文档进行多个聚合管道,每个子管道在输出文档中都有自己的字段,其结果存储为文档数组
so: 每个分段就相当于一个聚合管道,分段大小为 从skip开始截至到limit位置, 然后采用Mongo支持的随机函数对每段进行随机取值
最后,查询结果为了方便查看,使用$concatArrays
将每个子管道的结果集合合并在一起,最后使用$unwind
将集合拆分成单行,这样每行仅有一个题目
算个缺陷吧
如果分段数变化则需要添加$face
中的对象,最后在$concatArrays
中添加改对象,才可以实现分段数的增加/减少
实现的Mongo语句
db.sample.aggregate([
{
$facet: {
"1":[{
$limit:4
},{
$skip:0
},{
$sample:{size:2}
}],
"2":[{
$limit:8
},{
$skip:4
},{
$sample:{size:2}
}],
"3":[{
$limit:20
},{
$skip:8
},{
$sample:{size:2}
}]
}
},
// 将每段获得的数据合并到一个集合中
{
$project:{
"questionList":{
$concatArrays: [ "$1", "$2","$3" ]
}
}
},{
$unwind:"$questionList"
},{
$project:{
'name':'$questionList.name',
'age':'$questionList.age',
}
}
]);
JAVA 代码生成Mongo语句示例
创建对象存储分块信息
import lombok.Data;
@Data
public class RandomQuestionBlockEntity {
private Integer limit;// 返回记录数
private Integer offset; // 跳过记录数
private Integer index; // 指针位置
private Integer size;// 块大小
private Integer returnSize;// 每块返回数据量
private String indexName;// 块名称
}
生成Mongo查询语句方法
需要考虑几种特殊情况:如题目数<分块数, 题目数不能平分到每个块中等
/**
* 分段随机出题 mongo语句生成
* @param defaultDb 默认条件
* @param size 查询题目数
* @param questionCount 题目总数
* @return
*/
public static BasicDBList randomQuestionListSQL(BasicDBObject defaultMatchDb, Integer size, Integer questionCount) {
List<RandomQuestionBlockEntity> splitList = new ArrayList<RandomQuestionBlockEntity>();// 分段信息存储
RandomQuestionBlockEntity blockEntity = null;
int splitNum = 10;// 分块数
if(splitNum > questionCount) {
// 分块数 大于 题目总数,将分块数设置为1
splitNum = 1;
}
if(splitNum > size) {
// 分块数 大于 查询记录数,则将分块数设置为记录数
splitNum = size;
}
int blockSize = questionCount/splitNum;// 块大小
int blockSurplus = questionCount%splitNum;// 不足平分一个块的题目数
int blockReturnSize = size/splitNum;// 每块返回记录数
int blockReturnSurplus = size%splitNum;// 不足平分到每个块的题目数
int lastSize = 0;// 之前块大小之和,用于计算当前块跳过的记录数
for (int i = 1; i <= splitNum; i++) {
blockEntity = new RandomQuestionBlockEntity();
blockEntity.setSize(blockSize);// 块大小
blockEntity.setReturnSize(blockReturnSize);// 返回记录数
if(blockSurplus > 0) {
blockEntity.setSize(blockSize+1);
blockSurplus --;
}
if(blockReturnSurplus > 0) {
blockEntity.setReturnSize(blockReturnSize+1);
blockReturnSurplus --;
}
blockEntity.setIndex(i);
blockEntity.setIndexName(String.valueOf(i));
blockEntity.setOffset(lastSize);
lastSize += blockEntity.getSize();
blockEntity.setLimit(lastSize);
splitList.add(blockEntity);
}
BasicDBList dbList = new BasicDBList();
BasicDBObject facet = new BasicDBObject();
BasicDBList concatArrays = new BasicDBList();
BasicDBObject[] dbObjects = null;
for (RandomQuestionBlockEntity entity : splitList) {
dbObjects = new BasicDBObject[3];
dbObjects[0] = new BasicDBObject("$limit", entity.getLimit());
dbObjects[1] = new BasicDBObject("$skip",entity.getOffset());
dbObjects[2] = new BasicDBObject("$sample", new BasicDBObject("size", entity.getReturnSize()));
facet.append(entity.getIndexName(), dbObjects);
concatArrays.add("$"+entity.getIndexName());
}
BasicDBObject match = new BasicDBObject("$match", defaultMatchDb);
BasicDBObject project = new BasicDBObject("$project",
new BasicDBObject("questionList",
new BasicDBObject("$concatArrays", concatArrays)));
dbList.add(match);
dbList.add(new BasicDBObject("$facet", facet));
dbList.add(project);
// TODO 最后拆分数组请自行实现
return dbList;
}