From fdf8ddab6f8709ce88662f2f4894c9e5de4c6a48 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 3 May 2021 13:06:42 -0600 Subject: [PATCH] Allow limited no-id entity operations --- .../DefaultJdbcRepositoryOperations.java | 4 +- .../postgres/PostgresNoIdEntitySpec.groovy | 89 +++++++++++++++++++ .../PostgresSequenceDefaultSpec.groovy | 37 ++------ .../PostgresTestPropertyProvider.groovy | 2 +- .../DefaultR2dbcRepositoryOperations.java | 5 +- .../repository/ReactorCrudRepository.java | 1 - .../AbstractSqlRepositoryOperations.java | 31 ++++++- 7 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresNoIdEntitySpec.groovy diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index 98386e6897..3d59a4ed46 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -476,9 +476,7 @@ private Stream findStream(@NonNull PreparedQuery preparedQuery, } } ); - boolean onlySingleEndedJoins = joinFetchPaths.stream() - .flatMap(jp -> Arrays.stream(jp.getAssociationPath())) - .anyMatch(association -> association.getKind().isSingleEnded()); + boolean onlySingleEndedJoins = isOnlySingleEndedJoins(getEntity(preparedQuery.getRootEntity()), joinFetchPaths); // Cannot stream ResultSet for "many" joined query if (!onlySingleEndedJoins) { try { diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresNoIdEntitySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresNoIdEntitySpec.groovy new file mode 100644 index 0000000000..4d351e1aa7 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresNoIdEntitySpec.groovy @@ -0,0 +1,89 @@ +package io.micronaut.data.jdbc.postgres + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.jdbc.annotation.JdbcRepository +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort +import io.micronaut.data.model.query.builder.sql.Dialect +import io.micronaut.data.runtime.config.SchemaGenerate +import io.micronaut.transaction.SynchronousTransactionManager +import io.micronaut.transaction.TransactionCallback +import io.micronaut.transaction.TransactionStatus +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import javax.inject.Inject + +class PostgresNoIdEntitySpec extends Specification implements PostgresTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties()) + + @Shared + @Inject + NoIdEntityRepository noIdEntityRepository = applicationContext.getBean(NoIdEntityRepository) + + @Shared + @Inject + SynchronousTransactionManager transactionManager = applicationContext.getBean(SynchronousTransactionManager) + + void setup() { + transactionManager.executeWrite(new TransactionCallback() { + @Override + Object call(TransactionStatus status) throws Exception { + status.connection.prepareStatement(''' +drop sequence if exists "no_id_names_seq"; +drop table if exists "no_id_names"; +create sequence "no_id_names_seq" increment by 1; +create table "no_id_names" +( + "id" bigint default nextval('no_id_names_seq'::regclass) not null, + "first_name" varchar(255) , + "last_name" varchar(255) +); +''').withCloseable { it.executeUpdate() } + return null + } + + }) + } + + void 'test no-id entity operations'() { + when: + noIdEntityRepository.save(new NoIdEntity(firstName: "Xyz", lastName: "Abc")) + noIdEntityRepository.save(new NoIdEntity(firstName: "Qwe", lastName: "Jkl")) + def all = noIdEntityRepository.listAll() + def allPageable = noIdEntityRepository.listAll(Pageable.from(1, 1).order("firstName", Sort.Order.Direction.DESC)) + then: + all.size() == 2 + all.find { it.firstName == "Xyz" } + all.find { it.firstName == "Qwe" } + allPageable.size() == 1 + allPageable.find { it.firstName == "Qwe" } + } + + @Override + SchemaGenerate schemaGenerate() { + return SchemaGenerate.NONE + } +} + +@JdbcRepository(dialect = Dialect.POSTGRES) +interface NoIdEntityRepository { + + NoIdEntity save(NoIdEntity entity) + + List listAll() + + List listAll(Pageable pageable) + +} + +@MappedEntity("no_id_names") +class NoIdEntity { + + String firstName + String lastName +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresSequenceDefaultSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresSequenceDefaultSpec.groovy index 52faa23ab9..612cdb8873 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresSequenceDefaultSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresSequenceDefaultSpec.groovy @@ -15,36 +15,15 @@ */ package io.micronaut.data.jdbc.postgres -import io.micronaut.data.model.query.builder.sql.Dialect +import io.micronaut.data.runtime.config.SchemaGenerate import io.micronaut.test.extensions.spock.annotation.MicronautTest -import io.micronaut.test.support.TestPropertyProvider -import org.testcontainers.containers.PostgreSQLContainer -import spock.lang.Shared import spock.lang.Specification import javax.inject.Inject import java.sql.Connection @MicronautTest -class PostgresSequenceDefaultSpec extends Specification implements TestPropertyProvider { - - @Shared - PostgreSQLContainer postgres - - @Override - Map getProperties() { - postgres = new PostgreSQLContainer<>("postgres:10") - .withDatabaseName("test-database") - .withUsername("test") - .withPassword("test") - postgres.start() - [ - "datasources.default.url":postgres.getJdbcUrl(), - "datasources.default.username":postgres.getUsername(), - "datasources.default.password":postgres.getPassword(), - "datasources.default.dialect": Dialect.POSTGRES - ] as Map - } +class PostgresSequenceDefaultSpec extends Specification implements PostgresTestPropertyProvider { @Inject Connection connection @@ -54,6 +33,8 @@ class PostgresSequenceDefaultSpec extends Specification implements TestPropertyP void setup() { connection.prepareStatement(''' +drop sequence if exists "user_seq"; +drop table if exists "user_"; create sequence "user_seq" increment by 1; create table "user_" ( @@ -66,11 +47,6 @@ create table "user_" ''').withCloseable { it.executeUpdate( )} } - def cleanup() { - postgres.close() - } - - void "test sequence generation with default nextval"() { when: User user = userRepository.save("Fred") @@ -82,4 +58,9 @@ create table "user_" user.id != user2.id user.id == userRepository.findById(user.id).id } + + @Override + SchemaGenerate schemaGenerate() { + return SchemaGenerate.CREATE_DROP + } } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresTestPropertyProvider.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresTestPropertyProvider.groovy index dbd6c7f8d5..6492786311 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresTestPropertyProvider.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresTestPropertyProvider.groovy @@ -31,7 +31,7 @@ trait PostgresTestPropertyProvider implements SharedDatabaseContainerTestPropert @Override int sharedSpecsCount() { - return 9 + return 11 } @Override diff --git a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java index 29da701166..fc813da330 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java @@ -89,7 +89,6 @@ import java.io.Serializable; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -662,9 +661,7 @@ public Flux findAll(@NonNull PreparedQuery preparedQuery) { } } ); - boolean onlySingleEndedJoins = joinFetchPaths.stream() - .flatMap(jp -> Arrays.stream(jp.getAssociationPath())) - .anyMatch(association -> association.getKind().isSingleEnded()); + boolean onlySingleEndedJoins = isOnlySingleEndedJoins(getEntity(preparedQuery.getRootEntity()), joinFetchPaths); // Cannot stream ResultSet for "many" joined query if (!onlySingleEndedJoins) { SqlResultEntityTypeMapper.PushingMapper> manyReader = entityTypeMapper.readAllWithJoins(); diff --git a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/repository/ReactorCrudRepository.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/repository/ReactorCrudRepository.java index 7b216d4fbd..4ceab39ccd 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/repository/ReactorCrudRepository.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/repository/ReactorCrudRepository.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractSqlRepositoryOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractSqlRepositoryOperations.java index 249307dae5..bd56b24543 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractSqlRepositoryOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractSqlRepositoryOperations.java @@ -46,6 +46,7 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.Sort; +import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.QueryModel; import io.micronaut.data.model.query.QueryParameter; import io.micronaut.data.model.query.builder.AbstractSqlLikeQueryBuilder; @@ -106,7 +107,7 @@ * @author Denis Stepanov * @since 1.0.0 */ -@SuppressWarnings("unchecked") +@SuppressWarnings("FileLength") @Internal public abstract class AbstractSqlRepositoryOperations { protected static final Logger QUERY_LOG = DataSettings.QUERY_LOG; @@ -1261,6 +1262,29 @@ protected void checkOptimisticLocking(int expected, int received) { } } + /** + * Check if joined associated are all single ended (Can produce only one result). + * + * @param rootPersistentEntity The root entity + * @param joinFetchPaths The join paths + * @return true if there are no "many" joins + */ + protected boolean isOnlySingleEndedJoins(RuntimePersistentEntity rootPersistentEntity, Set joinFetchPaths) { + boolean onlySingleEndedJoins = joinFetchPaths.isEmpty() || joinFetchPaths.stream() + .flatMap(jp -> { + PersistentPropertyPath propertyPath = rootPersistentEntity.getPropertyPath(jp.getPath()); + if (propertyPath == null) { + return Stream.empty(); + } + if (propertyPath.getProperty() instanceof Association) { + return Stream.concat(propertyPath.getAssociations().stream(), Stream.of((Association) propertyPath.getProperty())); + } + return propertyPath.getAssociations().stream(); + }) + .allMatch(association -> association.getKind() == Relation.Kind.EMBEDDED || association.getKind().isSingleEnded()); + return onlySingleEndedJoins; + } + private static List associated(List associations, Association association) { if (associations == null) { return Collections.singletonList(association); @@ -1804,6 +1828,11 @@ protected SqlOperation(String query, Dialect dialect) { this.dialect = dialect; } + /** + * Expanded query. + * + * @return expanded query + */ public String exandedQuery() { return query; }