Skip to content

Commit

Permalink
Add missing criteria methods to CoroutineJpaSpecificationExecutor (#3107
Browse files Browse the repository at this point in the history
)

* draft enhance the CoroutineJpaSpecificationExecutor

* Add missing criteria methods to `CoroutineJpaSpecificationExecutor`

* Update data-model/src/main/kotlin/io/micronaut/data/repository/jpa/kotlin/CoroutineJpaSpecificationExecutor.kt

Co-authored-by: Radovan Radic <radicr@gmail.com>

* Javadoc

---------

Co-authored-by: Kareem Elzayat <kareem.elzayat@uberall.com>
Co-authored-by: Radovan Radic <radicr@gmail.com>
  • Loading branch information
3 people authored Sep 5, 2024
1 parent d64303e commit 0f7eb17
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package io.micronaut.data.repository.jpa.kotlin
import io.micronaut.data.model.Page
import io.micronaut.data.model.Pageable
import io.micronaut.data.model.Sort
import io.micronaut.data.repository.jpa.criteria.CriteriaDeleteBuilder
import io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder
import io.micronaut.data.repository.jpa.criteria.CriteriaUpdateBuilder
import io.micronaut.data.repository.jpa.criteria.DeleteSpecification
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification
import io.micronaut.data.repository.jpa.criteria.QuerySpecification
Expand Down Expand Up @@ -51,6 +54,17 @@ interface CoroutineJpaSpecificationExecutor<T> {
*/
suspend fun findOne(spec: PredicateSpecification<T>?): T?

/**
* Returns a single entity using build criteria query.
*
* @param builder The criteria query builder
* @param <R> the result type
*
* @return optional found result
* @since 4.10
*/
suspend fun <R> findOne(builder: CriteriaQueryBuilder<R>?): R?

/**
* Returns all entities matching the given [QuerySpecification].
*
Expand All @@ -67,6 +81,17 @@ interface CoroutineJpaSpecificationExecutor<T> {
*/
fun findAll(spec: PredicateSpecification<T>?): Flow<T>

/**
* Returns multiple entities using build criteria query.
*
* @param builder The criteria query builder
* @param <R> the result type
*
* @return found results
* @since 4.10
*/
fun <R> findAll(builder: CriteriaQueryBuilder<R>?): Flow<R>

/**
* Returns a [Page] of entities matching the given [QuerySpecification].
*
Expand All @@ -85,6 +110,18 @@ interface CoroutineJpaSpecificationExecutor<T> {
*/
suspend fun findAll(spec: PredicateSpecification<T>?, pageable: Pageable): Page<T>

/**
* Returns a page using build criteria query.
*
* @param builder The criteria query builder
* @param pageable The pageable object
* @param <R> the result type
*
* @return found results
* @since 4.10
*/
suspend fun <R> findAll(builder: CriteriaQueryBuilder<R>?, pageable: Pageable): Page<R>

/**
* Returns all entities matching the given [QuerySpecification] and [Sort].
*
Expand Down Expand Up @@ -153,11 +190,29 @@ interface CoroutineJpaSpecificationExecutor<T> {
*/
suspend fun deleteAll(spec: PredicateSpecification<T>?): Long

/**
* Delete all entities using build criteria query.
*
* @param builder The delete criteria query builder
* @return the number records updated.
* @since 4.10
*/
suspend fun deleteAll(builder: CriteriaDeleteBuilder<T>?): Long

/**
* Updates all entities matching the given [UpdateSpecification].
*
* @param spec The update specification
* @return the number records updated.
*/
suspend fun updateAll(spec: UpdateSpecification<T>?): Long

/**
* Updates all entities using build criteria query.
*
* @param builder The update criteria query builder
* @return the number records updated.
* @since 4.10
*/
suspend fun updateAll(builder: CriteriaUpdateBuilder<T>?): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ protected final Long count(RepositoryMethodKey methodKey, MethodInvocationContex
Set<JoinPath> methodJoinPaths = getMethodJoinPaths(methodKey, context);
Long count;
if (criteriaRepositoryOperations != null) {
count = criteriaRepositoryOperations.findOne(buildCountQuery(context));
count = criteriaRepositoryOperations.findOne(buildCountQuery(context, methodJoinPaths));
} else {
count = operations.findOne(preparedQueryForCriteria(methodKey, context, Type.COUNT, methodJoinPaths));
}
Expand Down Expand Up @@ -219,7 +219,7 @@ protected final <E, QR> PreparedQuery<E, QR> preparedQueryForCriteria(Repository
QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context);
StoredQuery<E, ?> storedQuery = switch (type) {
case FIND_ALL, FIND_ONE, FIND_PAGE -> buildFind(methodKey, context, type, methodJoinPaths);
case COUNT -> buildCount(methodKey, context);
case COUNT -> buildCount(methodKey, context, methodJoinPaths);
case DELETE_ALL -> buildDeleteAll(context, sqlQueryBuilder);
case UPDATE_ALL -> buildUpdateAll(context, sqlQueryBuilder);
case EXISTS -> buildExists(context, sqlQueryBuilder, methodJoinPaths);
Expand Down Expand Up @@ -292,30 +292,18 @@ protected final <E> CriteriaDelete<E> buildDeleteQuery(MethodInvocationContext<T
}

private <E> StoredQuery<E, ?> buildCount(RepositoryMethodKey methodKey,
MethodInvocationContext<T, R> context) {
CriteriaQuery<Long> criteriaQuery = buildCountQuery(context);
MethodInvocationContext<T, R> context,
Set<JoinPath> methodJoinPaths) {
CriteriaQuery<Long> criteriaQuery = buildCountQuery(context, methodJoinPaths);
QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context);
QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaQuery).buildQuery(context, sqlQueryBuilder);
return QueryResultStoredQuery.count(context.getName(), context.getAnnotationMetadata(), queryResult, getRequiredRootEntity(context));
}

@NonNull
protected final <E> CriteriaQuery<Long> buildCountQuery(MethodInvocationContext<T, R> context) {
Class<E> rootEntity = getRequiredRootEntity(context);
QuerySpecification<E> specification = getQuerySpecification(context);
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
Root<E> root = criteriaQuery.from(rootEntity);
if (specification != null) {
Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);
if (predicate != null) {
criteriaQuery.where(predicate);
}
}
if (criteriaQuery.isDistinct()) {
return criteriaQuery.select(criteriaBuilder.countDistinct(root));
} else {
return criteriaQuery.select(criteriaBuilder.count(root));
}
protected final CriteriaQuery<Long> buildCountQuery(MethodInvocationContext<T, R> context, Set<JoinPath> methodJoinPaths) {
CriteriaQueryBuilder<Long> criteriaQueryBuilder = getCountCriteriaQueryBuilder(context, methodJoinPaths);
return criteriaQueryBuilder.build(criteriaBuilder);
}

private <E> StoredQuery<E, Object> buildFind(RepositoryMethodKey methodKey,
Expand Down Expand Up @@ -432,6 +420,54 @@ protected <K> CriteriaQueryBuilder<K> getCriteriaQueryBuilder(MethodInvocationCo
};
}

/**
* Find {@link io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder}
* or {@link io.micronaut.data.repository.jpa.criteria.QuerySpecification} in context.
*
* @param context The context
* @param joinPaths The join fetch paths
* @param <E> the entity type
* @return found specification
*/
@NonNull
private <E> CriteriaQueryBuilder<Long> getCountCriteriaQueryBuilder(MethodInvocationContext<?, ?> context, Set<JoinPath> joinPaths) {
final Object parameterValue = context.getParameterValues()[0];
if (parameterValue instanceof CriteriaQueryBuilder providedCriteriaQueryBuilder) {
return new CriteriaQueryBuilder<Long>() {

@Override
public CriteriaQuery<Long> build(CriteriaBuilder criteriaBuilder) {
CriteriaQuery<?> criteriaQuery = providedCriteriaQueryBuilder.build(criteriaBuilder);
Root<?> root = criteriaQuery.getRoots().iterator().next();
if (criteriaQuery.isDistinct()) {
Expression longExpression = criteriaBuilder.countDistinct(root);
return criteriaQuery.select(longExpression);
} else {
Expression count = criteriaBuilder.count(root);
return criteriaQuery.select(count);
}
}
};
}
return criteriaBuilder -> {
Class<E> rootEntity = getRequiredRootEntity(context);
QuerySpecification<E> specification = getQuerySpecification(context);
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
Root<E> root = criteriaQuery.from(rootEntity);
if (specification != null) {
Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);
if (predicate != null) {
criteriaQuery.where(predicate);
}
}
if (criteriaQuery.isDistinct()) {
return criteriaQuery.select(criteriaBuilder.countDistinct(root));
} else {
return criteriaQuery.select(criteriaBuilder.count(root));
}
};
}

private void join(Root<?> root, JoinPath joinPath) {
if (root instanceof PersistentEntityFrom<?, ?> persistentEntityFrom) {
Optional<String> optAlias = joinPath.getAlias();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ protected final CompletionStage<Object> findOneAsync(RepositoryMethodKey methodK
protected final CompletionStage<Number> countAsync(RepositoryMethodKey methodKey, MethodInvocationContext<T, R> context) {
Set<JoinPath> methodJoinPaths = getMethodJoinPaths(methodKey, context);
if (asyncCriteriaOperations != null) {
return asyncCriteriaOperations.findOne(buildCountQuery(context)).thenApply(n -> n);
return asyncCriteriaOperations.findOne(buildCountQuery(context, methodJoinPaths)).thenApply(n -> n);
}
return asyncOperations.findOne(preparedQueryForCriteria(methodKey, context, Type.COUNT, methodJoinPaths));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ protected final Publisher<Object> findOneReactive(RepositoryMethodKey methodKey,
protected final Publisher<Long> countReactive(RepositoryMethodKey methodKey, MethodInvocationContext<T, R> context) {
Set<JoinPath> methodJoinPaths = getMethodJoinPaths(methodKey, context);
if (reactiveCriteriaOperations != null) {
return reactiveCriteriaOperations.findOne(buildCountQuery(context));
return reactiveCriteriaOperations.findOne(buildCountQuery(context, methodJoinPaths));
}
return reactiveOperations.findOne(preparedQueryForCriteria(methodKey, context, Type.COUNT, methodJoinPaths));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ inline fun <reified E, reified R> query(noinline dsl: SelectQuery<E, R>.() -> Un
@Experimental
inline fun <reified E> update(noinline dsl: UpdateQuery<E>.() -> Unit) = UpdateQueryBuilder(dsl, E::class.java)

@Experimental
inline fun <reified E> delete(noinline dsl: Where<E>.() -> Unit) = DeleteQueryBuilder(dsl, E::class.java)

@Experimental
class QueryBuilder<E, R>(private var dsl: SelectQuery<E, R>.() -> Unit, private var entityType: Class<E>, var resultType: Class<R>) : CriteriaQueryBuilder<R> {

Expand Down Expand Up @@ -144,7 +147,6 @@ class QueryPredicate<T>(var query: SelectQuery<T, *>.() -> Unit) : QuerySpecific
@Experimental
class SelectQuery<T, V>(var root: Root<T>, var query: CriteriaQuery<V>, var criteriaBuilder: CriteriaBuilder) : WhereQuery<T>(root, criteriaBuilder) {


fun select(prop: KProperty<V>) {
select(prop.asPath(root))
}
Expand All @@ -161,6 +163,10 @@ class SelectQuery<T, V>(var root: Root<T>, var query: CriteriaQuery<V>, var crit
query.multiselect(*props)
}

fun distinct() {
query.distinct(true)
}

override fun where(dsl: Where<T>.() -> Unit) {
super.where(dsl)
query.where(predicate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import io.micronaut.data.repository.jpa.criteria.DeleteSpecification
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification
import io.micronaut.data.repository.jpa.criteria.QuerySpecification
import io.micronaut.data.repository.jpa.criteria.UpdateSpecification
import io.micronaut.data.runtime.criteria.delete
import io.micronaut.data.runtime.criteria.get
import io.micronaut.data.runtime.criteria.query
import io.micronaut.data.runtime.criteria.update
import io.micronaut.data.runtime.criteria.where
import java.util.*

Expand Down Expand Up @@ -107,6 +110,33 @@ interface PersonRepository : CrudRepository<Person, Long>, JpaSpecificationExecu
fun nameInList(names: List<String>) = where<Person> {
root[Person::name] inList names
}

fun nameOrAgeMatches(name: String, age: Int) = query<Person, Person> {
where {
or {
root[Person::name] eq name
root[Person::age] eq age
}
}
}

fun nameMatches(name: String) = query<Person, Person> {
where {
or {
root[Person::name] eq name
}
}
}

fun updateName(newName: String, existingName: String) = update<Person> {
set(Person::name, newName)
where {
root[Person::name] eq existingName
}
}
fun deleteByName(name: String) = delete<Person> {
root[Person::name] eq name
}
// tag::specifications[]
}
// end::allSpecifications[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package example

import example.PersonRepository.Specifications.ageIsLessThan
import example.PersonRepository.Specifications.deleteByName
import example.PersonRepository.Specifications.nameEquals
import example.PersonRepository.Specifications.nameInList
import example.PersonRepository.Specifications.nameMatches
import example.PersonRepository.Specifications.nameOrAgeMatches
import example.PersonRepository.Specifications.setNewName
import example.PersonRepository.Specifications.updateName
import io.micronaut.data.model.Pageable
import io.micronaut.data.model.Sort
import jakarta.inject.Inject
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification.not
import io.micronaut.data.runtime.criteria.get
import io.micronaut.data.runtime.criteria.where
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -119,7 +125,6 @@ internal class PersonSuspendRepositorySpec {
Assertions.assertEquals("Denis", countAgeLess30Page.content[0].name)
}


@Test
fun testInListWithPagination() = runBlocking {
val people = personRepository.findAll(nameInList(listOf("Denis", "Josh")), Sort.of(Sort.Order.asc("name"))).toList()
Expand All @@ -131,4 +136,58 @@ internal class PersonSuspendRepositorySpec {
Assertions.assertEquals(1, peoplePage.content.size)
Assertions.assertEquals("Denis", peoplePage.content[0].name)
}

@Test
fun testFindAllCriteriaQueryBuilder() = runBlocking {
val people = personRepository.findAll(nameOrAgeMatches("Denis", 22)).toList()
Assertions.assertEquals(2, people.size)
}

@Test
fun testFindAllCriteriaQueryBuilderPageable() = runBlocking {
val pageable = Pageable.from(0, 1).order("name")
val criteria = nameOrAgeMatches("Denis", 22)
val page1 = personRepository.findAll(criteria, pageable)
Assertions.assertEquals(2, page1.totalPages)
Assertions.assertEquals(1, page1.content.size)
Assertions.assertEquals("Denis", page1.content[0].name)
val page2 = personRepository.findAll(criteria, pageable.next())
Assertions.assertEquals(2, page2.totalPages)
Assertions.assertEquals(1, page2.content.size)
Assertions.assertEquals("Josh", page2.content[0].name)
}

@Test
fun testFindOneCriteriaQueryBuilder() = runBlocking {
val person = personRepository.findOne(nameMatches("Denis"))!!
Assertions.assertEquals("Denis", person.name)
}

@Test
fun testUpdateCriteria() = runBlocking {
val empty: PredicateSpecification<Person>? = null
var all = personRepository.findAll(empty).toList()
Assertions.assertEquals(2, all.size)
Assertions.assertTrue(all.stream().anyMatch { p: Person -> p.name == "Denis" })
Assertions.assertTrue(all.stream().anyMatch { p: Person -> p.name == "Josh" })

val recordsUpdated = personRepository.updateAll(updateName("Steven", "Denis"))
Assertions.assertEquals(1, recordsUpdated)
all = personRepository.findAll(empty).toList()
Assertions.assertEquals(2, all.size)
Assertions.assertTrue(all.stream().anyMatch { p: Person -> p.name == "Steven" })
Assertions.assertTrue(all.stream().anyMatch { p: Person -> p.name == "Josh" })
}

@Test
fun testDeleteUsingCriteriaBuilder() = runBlocking {
val empty: PredicateSpecification<Person>? = null
var all = personRepository.findAll(empty).toList()
Assertions.assertEquals(2, all.size)

val recordsDeleted = personRepository.deleteAll(deleteByName("Denis"))
Assertions.assertEquals(1, recordsDeleted)
all = personRepository.findAll(empty).toList()
Assertions.assertEquals(1, all.size)
}
}

0 comments on commit 0f7eb17

Please sign in to comment.