diff --git a/data-model/src/main/kotlin/io/micronaut/data/repository/jpa/kotlin/CoroutineJpaSpecificationExecutor.kt b/data-model/src/main/kotlin/io/micronaut/data/repository/jpa/kotlin/CoroutineJpaSpecificationExecutor.kt index e8fa4cecaac..ab5fde29f46 100644 --- a/data-model/src/main/kotlin/io/micronaut/data/repository/jpa/kotlin/CoroutineJpaSpecificationExecutor.kt +++ b/data-model/src/main/kotlin/io/micronaut/data/repository/jpa/kotlin/CoroutineJpaSpecificationExecutor.kt @@ -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 @@ -51,6 +54,17 @@ interface CoroutineJpaSpecificationExecutor { */ suspend fun findOne(spec: PredicateSpecification?): T? + /** + * Returns a single entity using build criteria query. + * + * @param builder The criteria query builder + * @param the result type + * + * @return optional found result + * @since 4.10 + */ + suspend fun findOne(builder: CriteriaQueryBuilder?): R? + /** * Returns all entities matching the given [QuerySpecification]. * @@ -67,6 +81,17 @@ interface CoroutineJpaSpecificationExecutor { */ fun findAll(spec: PredicateSpecification?): Flow + /** + * Returns multiple entities using build criteria query. + * + * @param builder The criteria query builder + * @param the result type + * + * @return found results + * @since 4.10 + */ + fun findAll(builder: CriteriaQueryBuilder?): Flow + /** * Returns a [Page] of entities matching the given [QuerySpecification]. * @@ -85,6 +110,18 @@ interface CoroutineJpaSpecificationExecutor { */ suspend fun findAll(spec: PredicateSpecification?, pageable: Pageable): Page + /** + * Returns a page using build criteria query. + * + * @param builder The criteria query builder + * @param pageable The pageable object + * @param the result type + * + * @return found results + * @since 4.10 + */ + suspend fun findAll(builder: CriteriaQueryBuilder?, pageable: Pageable): Page + /** * Returns all entities matching the given [QuerySpecification] and [Sort]. * @@ -153,6 +190,15 @@ interface CoroutineJpaSpecificationExecutor { */ suspend fun deleteAll(spec: PredicateSpecification?): 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?): Long + /** * Updates all entities matching the given [UpdateSpecification]. * @@ -160,4 +206,13 @@ interface CoroutineJpaSpecificationExecutor { * @return the number records updated. */ suspend fun updateAll(spec: UpdateSpecification?): 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?): Long } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index 67883d3989b..08cb685d532 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -177,7 +177,7 @@ protected final Long count(RepositoryMethodKey methodKey, MethodInvocationContex Set 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)); } @@ -219,7 +219,7 @@ protected final PreparedQuery preparedQueryForCriteria(Repository QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context); StoredQuery 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); @@ -292,30 +292,18 @@ protected final CriteriaDelete buildDeleteQuery(MethodInvocationContext StoredQuery buildCount(RepositoryMethodKey methodKey, - MethodInvocationContext context) { - CriteriaQuery criteriaQuery = buildCountQuery(context); + MethodInvocationContext context, + Set methodJoinPaths) { + CriteriaQuery 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 CriteriaQuery buildCountQuery(MethodInvocationContext context) { - Class rootEntity = getRequiredRootEntity(context); - QuerySpecification specification = getQuerySpecification(context); - CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); - Root 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 buildCountQuery(MethodInvocationContext context, Set methodJoinPaths) { + CriteriaQueryBuilder criteriaQueryBuilder = getCountCriteriaQueryBuilder(context, methodJoinPaths); + return criteriaQueryBuilder.build(criteriaBuilder); } private StoredQuery buildFind(RepositoryMethodKey methodKey, @@ -432,6 +420,54 @@ protected CriteriaQueryBuilder 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 the entity type + * @return found specification + */ + @NonNull + private CriteriaQueryBuilder getCountCriteriaQueryBuilder(MethodInvocationContext context, Set joinPaths) { + final Object parameterValue = context.getParameterValues()[0]; + if (parameterValue instanceof CriteriaQueryBuilder providedCriteriaQueryBuilder) { + return new CriteriaQueryBuilder() { + + @Override + public CriteriaQuery 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 rootEntity = getRequiredRootEntity(context); + QuerySpecification specification = getQuerySpecification(context); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); + Root 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 optAlias = joinPath.getAlias(); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/AbstractAsyncSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/AbstractAsyncSpecificationInterceptor.java index bdeaf3d53f4..ce3dc12c6a3 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/AbstractAsyncSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/AbstractAsyncSpecificationInterceptor.java @@ -101,7 +101,7 @@ protected final CompletionStage findOneAsync(RepositoryMethodKey methodK protected final CompletionStage countAsync(RepositoryMethodKey methodKey, MethodInvocationContext context) { Set 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)); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java index faa2b22c6c6..7b08703262b 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java @@ -103,7 +103,7 @@ protected final Publisher findOneReactive(RepositoryMethodKey methodKey, protected final Publisher countReactive(RepositoryMethodKey methodKey, MethodInvocationContext context) { Set 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)); } diff --git a/data-runtime/src/main/kotlin/io/micronaut/data/runtime/criteria/KCriteriaBuilderExt.kt b/data-runtime/src/main/kotlin/io/micronaut/data/runtime/criteria/KCriteriaBuilderExt.kt index ecf0d8a8ca2..03bd380b8df 100644 --- a/data-runtime/src/main/kotlin/io/micronaut/data/runtime/criteria/KCriteriaBuilderExt.kt +++ b/data-runtime/src/main/kotlin/io/micronaut/data/runtime/criteria/KCriteriaBuilderExt.kt @@ -81,6 +81,9 @@ inline fun query(noinline dsl: SelectQuery.() -> Un @Experimental inline fun update(noinline dsl: UpdateQuery.() -> Unit) = UpdateQueryBuilder(dsl, E::class.java) +@Experimental +inline fun delete(noinline dsl: Where.() -> Unit) = DeleteQueryBuilder(dsl, E::class.java) + @Experimental class QueryBuilder(private var dsl: SelectQuery.() -> Unit, private var entityType: Class, var resultType: Class) : CriteriaQueryBuilder { @@ -144,7 +147,6 @@ class QueryPredicate(var query: SelectQuery.() -> Unit) : QuerySpecific @Experimental class SelectQuery(var root: Root, var query: CriteriaQuery, var criteriaBuilder: CriteriaBuilder) : WhereQuery(root, criteriaBuilder) { - fun select(prop: KProperty) { select(prop.asPath(root)) } @@ -161,6 +163,10 @@ class SelectQuery(var root: Root, var query: CriteriaQuery, var crit query.multiselect(*props) } + fun distinct() { + query.distinct(true) + } + override fun where(dsl: Where.() -> Unit) { super.where(dsl) query.where(predicate) diff --git a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/PersonRepository.kt b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/PersonRepository.kt index d147eeb3682..44f31fdcc99 100644 --- a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/PersonRepository.kt +++ b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/PersonRepository.kt @@ -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.* @@ -107,6 +110,33 @@ interface PersonRepository : CrudRepository, JpaSpecificationExecu fun nameInList(names: List) = where { root[Person::name] inList names } + + fun nameOrAgeMatches(name: String, age: Int) = query { + where { + or { + root[Person::name] eq name + root[Person::age] eq age + } + } + } + + fun nameMatches(name: String) = query { + where { + or { + root[Person::name] eq name + } + } + } + + fun updateName(newName: String, existingName: String) = update { + set(Person::name, newName) + where { + root[Person::name] eq existingName + } + } + fun deleteByName(name: String) = delete { + root[Person::name] eq name + } // tag::specifications[] } // end::allSpecifications[] diff --git a/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/PersonSuspendRepositorySpec.kt b/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/PersonSuspendRepositorySpec.kt index 7621560060f..becd9696df5 100644 --- a/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/PersonSuspendRepositorySpec.kt +++ b/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/PersonSuspendRepositorySpec.kt @@ -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 @@ -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() @@ -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? = 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? = 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) + } }