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;
}