Skip to content

Commit 52abbe3

Browse files
author
QuadStingray
committed
feat: Pagination for MongoDb Search and Aggregation
1 parent 0926b1d commit 52abbe3

File tree

8 files changed

+222
-1
lines changed

8 files changed

+222
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package dev.mongocamp.driver.mongodb.exception
2+
3+
case class MongoCampPaginationException(message: String) extends Exception(message)
4+

src/main/scala/dev/mongocamp/driver/mongodb/operation/Base.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ abstract class Base[A]()(implicit ct: ClassTag[A]) extends LazyLogging {
1414

1515
protected def coll: MongoCollection[A]
1616

17-
def count(filter: Bson = Document(), options: CountOptions = CountOptions()): Observable[Long] =
17+
def count(filter: Bson = Document(), options: CountOptions = CountOptions()): Observable[Long] = {
1818
coll.countDocuments(filter, options)
19+
}
1920

2021
def drop(): Observable[Void] = coll.drop()
2122

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
import com.mongodb.client.model.Facet
4+
import dev.mongocamp.driver.mongodb.exception.MongoCampPaginationException
5+
import dev.mongocamp.driver.mongodb.{MongoDAO, _}
6+
import org.mongodb.scala.bson.Document
7+
import org.mongodb.scala.bson.conversions.Bson
8+
import org.mongodb.scala.model.Aggregates
9+
10+
import scala.jdk.CollectionConverters._
11+
12+
case class MongoPaginatedAggregation[A <: Any](
13+
dao: MongoDAO[A],
14+
aggregationPipeline: List[Bson] = List(),
15+
allowDiskUse: Boolean = false,
16+
) {
17+
18+
private val AggregationKeyMetaData = "metadata"
19+
private val AggregationKeyData = "data"
20+
private val AggregationKeyMetaDataTotal = "total"
21+
22+
def paginate(page: Long, rows: Long): PaginationResult[org.bson.BsonDocument] = {
23+
if (rows <= 0) {
24+
throw MongoCampPaginationException("rows per page must be greater then 0.")
25+
}
26+
if (page <= 0) {
27+
throw MongoCampPaginationException("page must be greater then 0.")
28+
}
29+
30+
val skip = (page - 1) * rows
31+
32+
val listOfMetaData: List[Bson] = List(Map("$count" -> AggregationKeyMetaDataTotal))
33+
val listOfPaging: List[Bson] = List(Map("$skip" -> skip), Map("$limit" -> rows))
34+
35+
val pipeline =
36+
aggregationPipeline ++ List(
37+
Aggregates.facet(new Facet(AggregationKeyMetaData, listOfMetaData.asJava), new Facet(AggregationKeyData, listOfPaging.asJava))
38+
)
39+
40+
val dbResponse = dao.findAggregated(pipeline, allowDiskUse).result().asInstanceOf[Document]
41+
42+
val count: Long = dbResponse.get(AggregationKeyMetaData).get.asArray().get(0).asDocument().get(AggregationKeyMetaDataTotal).asNumber().longValue()
43+
val allPages = Math.ceil(count.toDouble / rows).toInt
44+
val list = dbResponse.get("data").get.asArray().asScala.map(_.asDocument())
45+
PaginationResult(list.toList, PaginationInfo(count, rows, page, allPages))
46+
}
47+
48+
def countResult: Long = {
49+
val listOfMetaData: List[Bson] = List(Map("$count" -> AggregationKeyMetaDataTotal))
50+
val listOfPaging: List[Bson] = List(Map("$skip" -> 0), Map("$limit" -> 1))
51+
52+
val pipeline = aggregationPipeline ++ List(
53+
Aggregates.facet(new Facet(AggregationKeyMetaData, listOfMetaData.asJava), new Facet(AggregationKeyData, listOfPaging.asJava))
54+
)
55+
val dbResponse = dao.findAggregated(pipeline, allowDiskUse).result().asInstanceOf[Document]
56+
val count: Long = dbResponse.get(AggregationKeyMetaData).get.asArray().get(0).asDocument().get(AggregationKeyMetaDataTotal).asNumber().longValue()
57+
count
58+
}
59+
60+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
import dev.mongocamp.driver.mongodb.exception.MongoCampPaginationException
4+
import dev.mongocamp.driver.mongodb.{MongoDAO, _}
5+
import org.mongodb.scala.bson.conversions.Bson
6+
7+
case class MongoPaginatedFilter[A <: Any](dao: MongoDAO[A], filter: Bson = Map(), sort: Bson = Map(), projection: Bson = Map()) {
8+
9+
def paginate(page: Long, rows: Long): PaginationResult[A] = {
10+
val count = countResult
11+
if (rows <= 0) {
12+
throw MongoCampPaginationException("rows per page must be greater then 0.")
13+
}
14+
if (page <= 0) {
15+
throw MongoCampPaginationException("page must be greater then 0.")
16+
}
17+
val allPages = Math.ceil(count.toDouble / rows).toInt
18+
val skip = (page - 1) * rows
19+
val responseList = dao.find(filter, sort, projection, rows.toInt).skip(skip.toInt).resultList()
20+
PaginationResult(responseList, PaginationInfo(count, rows, page, allPages))
21+
}
22+
23+
def countResult: Long = dao.count(filter).result()
24+
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
case class PaginationInfo(allCount: Long, perPage: Long, page: Long, pagesCount: Long)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
case class PaginationResult[A <: Any](databaseObjects: List[A], paginationInfo: PaginationInfo)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
// #agg_imports
4+
import dev.mongocamp.driver.mongodb.Aggregate._
5+
import dev.mongocamp.driver.mongodb.bson.BsonConverter
6+
import dev.mongocamp.driver.mongodb.dao.PersonSpecification
7+
// #agg_imports
8+
9+
import dev.mongocamp.driver.mongodb.test.TestDatabase._
10+
import org.mongodb.scala.bson.conversions.Bson
11+
import org.mongodb.scala.model.Aggregates.{filter, group, sort}
12+
import org.mongodb.scala.model.Filters.{and, equal}
13+
14+
class PaginationAggregationSpec extends PersonSpecification {
15+
16+
// #agg_stages
17+
val filterStage: Bson = filter(and(equal("gender", "female"), notNullFilter("balance")))
18+
19+
val groupStage: Bson = group(Map("age" -> "$age"), sumField("balance"), firstField("age"))
20+
21+
val sortStage: Bson = sort(sortByKey("age"))
22+
// #agg_stages
23+
24+
"Search" should {
25+
26+
"support aggregation filter" in {
27+
28+
val pipeline = List(filterStage, sortStage)
29+
30+
val pagination = MongoPaginatedAggregation(PersonDAO.Raw, pipeline, allowDiskUse = true)
31+
32+
val page = pagination.paginate(1, 10)
33+
34+
(page.paginationInfo.allCount must be).equalTo(98)
35+
36+
(page.paginationInfo.pagesCount must be).equalTo(10)
37+
38+
(page.databaseObjects.size must be).equalTo(10)
39+
}
40+
41+
"support aggregation filter and group" in {
42+
// #agg_execute
43+
val pipeline = List(filterStage, groupStage, sortStage)
44+
45+
val pagination = MongoPaginatedAggregation(PersonDAO.Raw, pipeline, allowDiskUse = true)
46+
47+
val page = pagination.paginate(1, 10)
48+
49+
// #agg_execute
50+
(page.paginationInfo.allCount must be).equalTo(21)
51+
52+
(page.paginationInfo.pagesCount must be).equalTo(3)
53+
54+
(page.databaseObjects.size must be).equalTo(10)
55+
56+
// #agg_convert
57+
val list: List[Map[String, Any]] = page.databaseObjects.map(d => BsonConverter.asMap(d))
58+
// #agg_convert
59+
list.foreach(m => println(m("age").toString + " -> " + m("balance")))
60+
61+
(list.head("age") must be).equalTo(20)
62+
(list.head("balance") must be).equalTo(8333.0)
63+
64+
}
65+
66+
}
67+
68+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package dev.mongocamp.driver.mongodb.pagination
2+
3+
import dev.mongocamp.driver.MongoImplicits
4+
import dev.mongocamp.driver.mongodb.Sort._
5+
import dev.mongocamp.driver.mongodb._
6+
import dev.mongocamp.driver.mongodb.dao.PersonSpecification
7+
import dev.mongocamp.driver.mongodb.test.TestDatabase._
8+
9+
class PaginationFilterSpec extends PersonSpecification with MongoImplicits {
10+
11+
"Search Operations" should {
12+
13+
"support findAll" in {
14+
15+
val pagination = MongoPaginatedFilter(PersonDAO)
16+
17+
val page = pagination.paginate(1, 10)
18+
19+
(page.paginationInfo.allCount must be).equalTo(PersonDAO.count().result().toInt)
20+
21+
page.databaseObjects.size must beEqualTo(10)
22+
23+
page.databaseObjects.head.name must not beEmpty
24+
25+
page.databaseObjects.head._id.toString must not beEmpty
26+
27+
}
28+
29+
"support with Filter" in {
30+
val paginationFemale = MongoPaginatedFilter(PersonDAO, Map("gender" -> "female"), sortByKey("name"))
31+
32+
val pageFemale = paginationFemale.paginate(1, 10)
33+
34+
pageFemale.paginationInfo.pagesCount mustEqual 10
35+
pageFemale.paginationInfo.allCount mustEqual 98
36+
pageFemale.paginationInfo.page mustEqual 1
37+
pageFemale.paginationInfo.perPage mustEqual 10
38+
39+
pageFemale.databaseObjects.size mustEqual 10
40+
pageFemale.databaseObjects.head.name mustEqual "Adele Melton"
41+
42+
val paginationMales = MongoPaginatedFilter(PersonDAO, Map("gender" -> "male"))
43+
val pageMale = paginationMales.paginate(1, 10)
44+
45+
pageMale.paginationInfo.pagesCount mustEqual 11
46+
pageMale.paginationInfo.allCount mustEqual 102
47+
pageMale.paginationInfo.page mustEqual 1
48+
pageMale.paginationInfo.perPage mustEqual 10
49+
50+
pageMale.databaseObjects.size mustEqual 10
51+
pageMale.databaseObjects.head.name mustEqual "Bowen Leon"
52+
53+
}
54+
55+
}
56+
57+
}

0 commit comments

Comments
 (0)