Skip to content
Bluetape4k docs1.11

Flow and result cardinality

Latest stable Based on Bluetape4k release 1.11.0

selectSuspending<T>(query) and selectAllSuspending<T>() return Flow<T>. Internally, they apply flow() to the Spring Data reactive select chain.

fun findComments(postId: Long): Flow<Comment> {
val query = Query.query(
Criteria.where(Comment::postId.name).isEqual(postId)
)
return operations.selectSuspending(query)
}

The query runs when the Flow is collected. Collecting the same instance twice may execute the database operation twice. If results must be reused, collect once and pass the materialized value explicitly after considering transaction and memory limits.

Expected resultAPIZero rowsMultiple rows
Exactly oneselectOneSuspending<T>ErrorError
Zero or exactly oneselectOneOrNullSuspending<T>nullError
First of one or moreselectFirstSuspending<T>ErrorFirst row
Zero or firstselectFirstOrNullSuspending<T>nullFirst row
Multiple rowsselectSuspending<T>Empty FlowAll rows

Making one nullable does not permit duplicates. Conversely, first does not verify uniqueness. Select the operation that matches the business invariant.

Use countAllSuspending<T>() for the full count and countSuspending<T>(query) for a filtered count. When only existence matters, existsSuspending<T>(query) avoids loading a row collection.

suspend fun hasComments(postId: Long): Boolean {
val query = Query.query(
Criteria.where(Comment::postId.name).isEqual(postId)
)
return operations.existsSuspending<Comment>(query)
}

No match returns 0L for count and false for exists. Database and mapping failures remain exceptions.

The 1.11.0 PostController returns the repository Flow<Post> directly.

@GetMapping
fun findAll(): Flow<Post> = postRepository.findAll()
@GetMapping("/{id}")
suspend fun findOne(@PathVariable id: Long): Post =
postRepository.findOneByIdOrNull(id)
?: throw PostNotFoundException(id)

Using Flow for multiple values and a suspending function for one value keeps the path non-blocking from controller to database. Do not insert runBlocking or Reactor block() in the middle.

When the first row has business meaning, add an explicit sort to the Query. Without sorting, the leading row may change with the database execution plan.

val latestQuery = Query.empty()
.sort(Sort.by(Sort.Direction.DESC, "createdAt"))
.limit(1)
val latest = operations.selectFirstOrNullSuspending<Post>(latestQuery)

Continue to Insert, update, and delete.