ReactiveMongo Pagination with Count - mongodb

I am trying to implement Pagination with Reactive Mongo.
def get(page: Int, pageSize: Int, filter: JsObject = Json.obj()): Future[Seq[Thing]] = {
val actualPage = if(page > 1) page else 1
collection
.find(filter)
.options(QueryOpts(skipN = (actualPage - 1) * pageSize, batchSizeN = pageSize))
.cursor[Thing]()
.collect[Seq](pageSize, Cursor.FailOnError[Seq[Thing]]())
}
This works and returns me a sequence of documents filtering by the page and pageSize. The problem is I have no idea how many results match the specified query, so I cant tell how many page's exist.
Is there a way I can also get a $count as part of the same query? Or do I need to resort to querying the database a second time with the below?
collection.count(filter)
Ideally I would change my return type to Future[Page[Seq[Thing]]] where Page would contain the total result count. e.g.
case class Page[T](results: Seq[T], count: Int)

Related

Conditional insert returning option of inserted row in Slick

I am trying to create a conditional insert into a table with responses for an event. The event may have a limit on how many responses/attendees it can support so to prevent overbooking I want to check the status before I insert a new response.
The tables match the database models I have (IDs are generated by DB/auto inc):
case class Event(id: Option[Int], name: String, limitedCapacity: Option[Int])
case class Response(id: Option[Int], eventId: Int, email: String)
I have constructed a SQL statement that describes my conditional insert (eg, only insert if event has no limited capacity or if number of responses are less than the limit) that looks like this:
INSERT INTO responses(eventId, email)
SELECT :eventId, :email
FROM events
WHERE id = :eventId
AND (limitedCapacity IS NULL
OR (SELECT COUNT(1) FROM responses WHERE eventId = :eventId) < limitedCapacity);
but I don't know how to translate that into Slick DSL that also returns the inserted row. I am using PostgreSQL so I know return-row-on-insert is possible for normal insertions at least.
Here is some code that shows what I am after but I want this as a single transaction:
def create(response: Response): Future[Option[Response]] = {
val event = db.run(events.filter(_.id === response.eventId).result.head)
if (event.limitedCapacity.isEmpty) {
db.run((responses returning responses) += response).map(Some(_))
}
else {
val responseCount = db.run(responses.filter(_.eventId === response.id).length.result)
if (responseCount < event.limitedCapacity.get) {
db.run((responses returning responses) += response).map(Some(_))
}
else {
Future.sucessful(None)
}
}
}
If it is not possible to return the inserted row that is fine but I need some kind of confirmation of the insertion at least.

ReactiveMongo failing to descend in sort

Using ReactiveMongo 0.11 for Scala 2.11. I have an issue where my queries are failing to descend. The following is my Index and ReactiveMongo query:
collection.indexesManager.ensure(Index(Seq("userId" -> IndexType.Ascending, "lastActivity" -> IndexType.Descending), background = true))
def listEfforts(userId: String, page: Int, pageSize: Int): Future[\/[ErrMsg, List[EffortDesc]]] = {
val query = BSONDocument("userId" -> userId)
val sort = BSONDocument("lastActivity" -> -1)
val skipN = (page - 1) * pageSize
val queryOptions = new QueryOpts(skipN = skipN, batchSizeN = pageSize, flagsN = 0)
collection.find(query).options(queryOptions).sort(sort).cursor[EffortDesc](ReadPreference.primaryPreferred).collect[List](pageSize).flatMap {
case list => Future(\/.right(list))
}
}
What's happening is my results are all ascending, even though my sort variable has been set to -1. lastActivity is a Unix timestamp field in milliseconds. I've tried other debugging issues (like recompiling, etc.)
Any idea what could be causing this? Thanks for your help!
Found the issue. If I put a IndexType.Descending on lastActivity field and then additionally sort as "descending" (via "lastActivity" -> -1) MongoDB will first return a descended sort according to its index and then sort it again.
I'm not sure if this is normal/expected behavior in Mongo but changing -1 to 1 fixed the issue.
Using either
("fieldName", BSONInteger.apply(1))
or
("fieldName", BSONInteger.apply(-1))
works for me.

ReactiveMongo query failing to paginate

Having some issues getting ReactiveMongo 0.11 to paginate my query. The behavior is that it is returning all results, instead of the page-by-page results.
Here's my query:
def listConvos(userId: String, page: Int, pageSize: Int) = {
val query = BSONDocument("userId" -> userId)
val sort = BSONDocument("lastActivity" -> -1)
val skipN = (page-1) * pageSize
val queryOptions = new QueryOpts(skipN = skipN, batchSizeN = pageSize, flagsN = 0)
collection.find(query).options(queryOptions).sort(sort).cursor[ConvoDesc](ReadPreference.primaryPreferred).collect[List]()
Note that pagination starts at 1. Using the IntelliJ debugger, the values for the variables above are:
userId = "29aosidfj43903p"
query = BSONDocument(<non-empty>)
sort = BSONDocument(<non-empty>)
queryOptions = QueryOpts(10,10,0)
page = 2
pageSize = 10
I can also confirm I've set a compound index on the userId and lastActivity fields. Thanks for your help!
The solution was to also include the page size in the collect method like so:
collection.find(query).options(queryOptions).sort(sort).cursor[ConvoDesc](ReadPreference.primaryPreferred).collect[List](pageSize)

Pagination with native query in slick

I am using Slick to connect to Postgres Database in our application. I have a generic filtering logic, where a Filter object will be passed from the UI, and it should return results with pagination. The Filter object should be generic so that it can be re-used.
Pseudo code of filter object is given below:
Filter = {
type: table
prop: List_of_conditions
page : 1
size : 10
}
Currently, I am building a native SQL from the Filter object and executing it. However, I am not able to use take and drop before the query is actually getting executed. It is currently getting all the results, and then dropping the unnecessary records.
I know how to do it with the slick queries, but not sure how to use pagination with the native queries?
val res = StaticQuery.queryNA[Entity](queryStr).list.drop((filter.pageNo- 1) * filter.pageSize).take(filter.pageSize)
I am using Slick 2.1
When you are using plain sql, you can't use the collection operators to build a query. You have to do it all in SQL:
val limit = filter.pageSize
val offset = (filter.pageNo- 1) * filter.pageSize
val res = StaticQuery.queryNA[Entity](queryStr ++ s" LIMIT $limit OFFSET $offset").list
I haven't tested it but i would suggest you to try to move .list call to the end
val res = StaticQuery.queryNA[Entity](queryStr).drop((filter.pageNo- 1) * filter.pageSize).take(filter.pageSize).list

ReactiveMongo - getting latest document in collection?

I've probably missed something obvious, but within the ReactiveMongo API (v0.8) how do you set a limit on the number of documents returned by a query?
I'd like to return the single most recent document added to a collection. This is my code so far:
def getLatest()(implicit reader: reactivemongo.bson.handlers.RawBSONReader[T]): Future[Option[T]] = {
collection.find (QueryBuilder(
queryDoc = Some(BSONDocument()),
sortDoc = Some(BSONDocument("_id" -> BSONInteger(-1)))
)).headOption().mapTo[Option[T]]
}
headOption() works to retrieve a single result, but I'm not explicitly using any kind of Mongo limit clause so I'm worried about this query's impact on the DB. Please help me improve this code. Thanks in advance.
In 0.8 you have to set the batchSize option to 1 in order to tell MongoDB to close the database cursor automatically:
val maybedoc = collection.find(BSONDocument(), QueryOpts().batchSize(1)).headOption
// or using QueryBuilder like you do
val maybedoc2 = collection.find (QueryBuilder(
queryDoc = Some(BSONDocument()),
sortDoc = Some(BSONDocument("_id" -> BSONInteger(-1)))
), QueryOpts().batchSize(1)).headOption()
In 0.9 collections have been refactored and greatly simplified. Now you can do this:
val maybedoc = collection.
find(BSONDocument()).
sort(BSONDocument("_id" -> -1)).
one[BSONDocument]
The one[T] method in 0.9 sets the batchSize flag for you and returns an Option[T].
Yes, the headOption() function limits the query to just one result:
def headOption()(implicit ec: ExecutionContext) :Future[Option[T]] = {
collect[Iterable](1).map(_.headOption)
}
https://github.com/zenexity/ReactiveMongo/blob/0.8/src/main/scala/api/cursor.scala#L180