diff --git a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java index a686842d3b..0447b54e5e 100644 --- a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java +++ b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java @@ -42,7 +42,6 @@ import io.micronaut.data.model.runtime.UpdateOperation; import io.micronaut.data.operations.reactive.ReactorCriteriaRepositoryOperations; import io.micronaut.data.runtime.convert.DataConversionService; -import io.micronaut.data.runtime.operations.internal.query.BindableParametersPreparedQuery; import io.micronaut.transaction.reactive.ReactorReactiveTransactionOperations; import jakarta.persistence.EntityGraph; import jakarta.persistence.FlushModeType; diff --git a/data-jdbc/build.gradle b/data-jdbc/build.gradle index 17d6c2ad9f..a40cf38554 100644 --- a/data-jdbc/build.gradle +++ b/data-jdbc/build.gradle @@ -36,6 +36,7 @@ dependencies { testImplementation libs.groovy.sql testImplementation mnValidation.micronaut.validation testImplementation mnValidation.micronaut.validation.processor + testImplementation mn.micronaut.http.client testImplementation(mnTestResources.testcontainers.mysql) testImplementation(mnTestResources.testcontainers.mariadb) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancyRecordSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancyRecordSpec.groovy new file mode 100644 index 0000000000..64c73cadec --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancyRecordSpec.groovy @@ -0,0 +1,283 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.h2 + +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.annotation.Introspected +import io.micronaut.data.connection.ConnectionDefinition +import io.micronaut.data.connection.annotation.Connectable +import io.micronaut.data.tck.entities.AccountRecord +import io.micronaut.data.tck.repositories.AccountRecordRepository +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Delete +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Put +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import jakarta.transaction.Transactional +import spock.lang.Specification + +class H2DiscriminatorMultitenancyRecordSpec extends Specification implements H2TestPropertyProvider { + + Map getExtraProperties() { + return [accountRepositoryClass: H2AccountRecordRepository.name] + } + + def "test discriminator multitenancy"() { + setup: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, getExtraProperties() + getProperties() + [ + 'spec.name' : 'discriminator-multitenancy-record', + 'micronaut.data.multi-tenancy.mode' : 'DISCRIMINATOR', + 'micronaut.multitenancy.tenantresolver.httpheader.enabled': 'true', + 'datasource.default.schema-generate' : 'create-drop' + ], Environment.TEST) + def context = embeddedServer.applicationContext + FooAccountClient fooAccountClient = context.getBean(FooAccountClient) + BarAccountClient barAccountClient = context.getBean(BarAccountClient) + fooAccountClient.deleteAll() + barAccountClient.deleteAll() + when: 'An account created in FOO tenant' + AccountDto fooAccount = fooAccountClient.save("The Stand") + then: 'The account exists in FOO tenant' + fooAccount.id + when: + fooAccount = fooAccountClient.findOne(fooAccount.getId()).orElse(null) + then: + fooAccount + fooAccount.name == "The Stand" + fooAccount.tenancy == "foo" + and: 'There is one account' + fooAccountClient.findAll().size() == 1 + and: 'There is no accounts in BAR tenant' + barAccountClient.findAll().size() == 0 + + when: "Update the tenancy" + fooAccountClient.updateTenancy(fooAccount.getId(), "bar") + then: + fooAccountClient.findAll().size() == 0 + barAccountClient.findAll().size() == 1 + fooAccountClient.findOne(fooAccount.getId()).isEmpty() + barAccountClient.findOne(fooAccount.getId()).isPresent() + + when: "Update the tenancy" + barAccountClient.updateTenancy(fooAccount.getId(), "foo") + then: + fooAccountClient.findAll().size() == 1 + barAccountClient.findAll().size() == 0 + fooAccountClient.findOne(fooAccount.getId()).isPresent() + barAccountClient.findOne(fooAccount.getId()).isEmpty() + + when: + AccountDto barAccount = barAccountClient.save("The Bar") + def allAccounts = barAccountClient.findAllTenants() + then: + barAccount.tenancy == "bar" + allAccounts.size() == 2 + allAccounts.find { it.id == barAccount.id }.tenancy == "bar" + allAccounts.find { it.id == fooAccount.id }.tenancy == "foo" + allAccounts == fooAccountClient.findAllTenants() + + when: + def barAccounts = barAccountClient.findAllBarTenants() + then: + barAccounts.size() == 1 + barAccounts[0].id == barAccount.id + barAccounts[0].tenancy == "bar" + barAccounts == fooAccountClient.findAllBarTenants() + + when: + def fooAccounts = barAccountClient.findAllFooTenants() + then: + fooAccounts.size() == 1 + fooAccounts[0].id == fooAccount.id + fooAccounts[0].tenancy == "foo" + fooAccounts == fooAccountClient.findAllFooTenants() + + when: + def exp = barAccountClient.findTenantExpression() + then: + exp.size() == 1 + exp[0].tenancy == "bar" + exp == fooAccountClient.findTenantExpression() + + when: 'Delete all BARs' + barAccountClient.deleteAll() + then: "FOOs aren't deletes" + fooAccountClient.findAll().size() == 1 + + when: 'Delete all FOOs' + fooAccountClient.deleteAll() + then: "All FOOs are deleted" + fooAccountClient.findAll().size() == 0 + cleanup: + embeddedServer?.stop() + } + +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy-record") +@ExecuteOn(TaskExecutors.IO) +@Controller("/accounts") +class AccountController { + + private final AccountRecordRepository accountRepository + + AccountController(ApplicationContext beanContext) { + def className = beanContext.getProperty("accountRepositoryClass", String).get() + this.accountRepository = beanContext.getBean(Class.forName(className)) as AccountRecordRepository + } + + @Post + AccountDto save(String name) { + def newAccount = new AccountRecord(null, name, null) + def account = accountRepository.save(newAccount) + return new AccountDto(account) + } + + @Put("/{id}/tenancy") + void updateTenancy(Long id, String tenancy) { + def account = accountRepository.findById(id).orElseThrow() + accountRepository.update( + new AccountRecord(account.id(), account.name(), tenancy) + ) + } + + @Get("/{id}") + Optional findOne(Long id) { + return accountRepository.findById(id).map(AccountDto::new) + } + + @Get + List findAll() { + return findAll0() + } + + @Get("/alltenants") + List findAllTenants() { + return accountRepository.findAll$withAllTenants().stream().map(AccountDto::new).toList() + } + + @Get("/foo") + List findAllFooTenants() { + return accountRepository.findAll$withTenantFoo().stream().map(AccountDto::new).toList() + } + + @Get("/bar") + List findAllBarTenants() { + return accountRepository.findAll$withTenantBar().stream().map(AccountDto::new).toList() + } + + @Get("/expression") + List findTenantExpression() { + return accountRepository.findAll$withTenantExpression().stream().map(AccountDto::new).toList() + } + + @Connectable + protected List findAll0() { + findAll1() + } + + @Connectable(propagation = ConnectionDefinition.Propagation.MANDATORY) + protected List findAll1() { + accountRepository.findAll().stream().map(AccountDto::new).toList() + } + + @Delete + void deleteAll() { + deleteAll0() + } + + @Transactional + protected deleteAll0() { + deleteAll1() + } + + @Transactional(Transactional.TxType.MANDATORY) + protected deleteAll1() { + accountRepository.deleteAll() + } + +} + +@Introspected +@EqualsAndHashCode +class AccountDto { + Long id + String name + String tenancy + + AccountDto() { + } + + AccountDto(AccountRecord account) { + id = account.id() + name = account.name() + tenancy = account.tenancy() + } + +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy-record") +@Client("/accounts") +interface AccountClient { + + @Post + AccountDto save(String name); + + @Put("/{id}/tenancy") + void updateTenancy(Long id, String tenancy) + + @Get("/{id}") + Optional findOne(Long id); + + @Get + List findAll(); + + @Get("/alltenants") + List findAllTenants(); + + @Get("/foo") + List findAllFooTenants(); + + @Get("/bar") + List findAllBarTenants(); + + @Get("/expression") + List findTenantExpression(); + + @Delete + void deleteAll(); +} + + +@Requires(property = "spec.name", value = "discriminator-multitenancy-record") +@Header(name = "tenantId", value = "foo") +@Client("/accounts") +interface FooAccountClient extends AccountClient { +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy-record") +@Header(name = "tenantId", value = "bar") +@Client("/accounts") +interface BarAccountClient extends AccountClient { +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..5b45d4e67c --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2DiscriminatorMultitenancySpec.groovy @@ -0,0 +1,12 @@ +package io.micronaut.data.jdbc.h2 + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class H2DiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements H2TestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: H2AccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mariadb/MariaDiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mariadb/MariaDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..fc8084e757 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mariadb/MariaDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.mariadb + +import io.micronaut.data.jdbc.mysql.MySqlAccountRepository +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class MariaDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements MariaTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MySqlAccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..d3c5bb8c9e --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.mysql + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class MySqlDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements MySQLTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MySqlAccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..9bf18318ac --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.oraclexe + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class OracleXEDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements OracleTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: OracleXEAccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..ef3ee89a37 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.postgres + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class PostgresDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements PostgresTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: PostgresAccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..a07e4eebac --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.sqlserver + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class SqlServerDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements MSSQLTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MSAccountRepository.name] + } + +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRecordRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRecordRepository.java new file mode 100644 index 0000000000..3f22898679 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRecordRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.h2; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.AccountRecordRepository; +import io.micronaut.data.tck.repositories.AccountRepository; + +@JdbcRepository(dialect = Dialect.H2) +public interface H2AccountRecordRepository extends AccountRecordRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRepository.java new file mode 100644 index 0000000000..263e4d94b1 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2AccountRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.h2; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.Account; +import io.micronaut.data.tck.repositories.AccountRepository; +import io.micronaut.data.tck.repositories.ArraysEntityRepository; + +@JdbcRepository(dialect = Dialect.H2) +public interface H2AccountRepository extends AccountRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/mysql/MySqlAccountRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/mysql/MySqlAccountRepository.java new file mode 100644 index 0000000000..2e0d7acbbb --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/mysql/MySqlAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.mysql; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.AccountRepository; + +@JdbcRepository(dialect = Dialect.MYSQL) +public interface MySqlAccountRepository extends AccountRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAccountRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAccountRepository.java new file mode 100644 index 0000000000..730ab02217 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.oraclexe; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.AccountRepository; + +@JdbcRepository(dialect = Dialect.ORACLE) +public interface OracleXEAccountRepository extends AccountRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/postgres/PostgresAccountRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/postgres/PostgresAccountRepository.java new file mode 100644 index 0000000000..8e7ddefd74 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/postgres/PostgresAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.postgres; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.AccountRepository; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface PostgresAccountRepository extends AccountRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/sqlserver/MSAccountRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/sqlserver/MSAccountRepository.java new file mode 100644 index 0000000000..113121f7f7 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/sqlserver/MSAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.sqlserver; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.AccountRepository; + +@JdbcRepository(dialect = Dialect.SQL_SERVER) +public interface MSAccountRepository extends AccountRepository { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/AutoPopulated.java b/data-model/src/main/java/io/micronaut/data/annotation/AutoPopulated.java index 353482a603..ba65de635b 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/AutoPopulated.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/AutoPopulated.java @@ -15,6 +15,8 @@ */ package io.micronaut.data.annotation; +import io.micronaut.context.annotation.AliasFor; + import java.lang.annotation.*; /** @@ -34,10 +36,24 @@ */ String NAME = AutoPopulated.class.getName(); + /** + * @deprecated Replaced by {@link #UPDATABLE} + */ + @Deprecated(since = "4.8", forRemoval = true) String UPDATEABLE = "updateable"; + String UPDATABLE = "updatable"; + /** * @return Whether the property can be updated following an insert + * @deprecated Replaced by {@link #updatable()} */ + @Deprecated(since = "4.8", forRemoval = true) boolean updateable() default true; + + /** + * @return Whether the property can be updated following an insert + */ + @AliasFor(member = UPDATEABLE) + boolean updatable() default true; } diff --git a/data-model/src/main/java/io/micronaut/data/annotation/DateCreated.java b/data-model/src/main/java/io/micronaut/data/annotation/DateCreated.java index d4b01363d2..2b07cadc16 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/DateCreated.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/DateCreated.java @@ -27,7 +27,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD}) @Documented -@AutoPopulated(updateable = false) +@AutoPopulated(updatable = false) public @interface DateCreated { /** * The annotation name. diff --git a/data-model/src/main/java/io/micronaut/data/annotation/TenantId.java b/data-model/src/main/java/io/micronaut/data/annotation/TenantId.java new file mode 100644 index 0000000000..82ef2789a8 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/TenantId.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.annotation; + +import io.micronaut.core.annotation.Experimental; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark the entity's property as a tenant ID. + * + * @author Denis Stepanov + * @since 4.8.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Documented +@Experimental +@AutoPopulated +public @interface TenantId { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/WithTenantId.java b/data-model/src/main/java/io/micronaut/data/annotation/WithTenantId.java new file mode 100644 index 0000000000..b0666ab2f9 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/WithTenantId.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.annotation; + +import io.micronaut.core.annotation.Experimental; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark the repository method to include the tenant id supplied by this annotation. + * + * @author Denis Stepanov + * @since 4.8.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +@Experimental +public @interface WithTenantId { + + /** + * @return The tenant ID. + */ + String value(); +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/WithoutTenantId.java b/data-model/src/main/java/io/micronaut/data/annotation/WithoutTenantId.java new file mode 100644 index 0000000000..f21998aeec --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/WithoutTenantId.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.annotation; + +import io.micronaut.core.annotation.Experimental; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark the repository method to exclude implicit tenant id check. + * + * @author Denis Stepanov + * @since 4.8.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +@Experimental +public @interface WithoutTenantId { +} diff --git a/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQueryParameter.java b/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQueryParameter.java index adaafe9e8e..5ee1ea9487 100644 --- a/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQueryParameter.java +++ b/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQueryParameter.java @@ -96,6 +96,11 @@ */ String META_MEMBER_EXPRESSION = "expression"; + /** + * @return The query parameter value + */ + String value() default ""; + /** * @return The query parameter name */ diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityRoot.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityRoot.java index 50022072fa..570e605852 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityRoot.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityRoot.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.data.model.PersistentEntity; +import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.jpa.criteria.impl.IdExpression; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Root; @@ -42,7 +43,7 @@ public interface PersistentEntityRoot extends Root, PersistentEntityFrom Expression id() { PersistentEntity persistentEntity = getPersistentEntity(); if (persistentEntity.hasIdentity()) { - return get(persistentEntity.getIdentity().getName()); + return get(persistentEntity.getIdentity()); } else if (persistentEntity.hasCompositeIdentity()) { return new IdExpression<>(this); } @@ -58,10 +59,24 @@ default Expression id() { @NonNull default PersistentPropertyPath version() { PersistentEntity persistentEntity = getPersistentEntity(); - if (persistentEntity.getVersion() == null) { + PersistentProperty version = persistentEntity.getVersion(); + if (version == null) { throw new IllegalStateException("No version is present"); } - return get(persistentEntity.getVersion().getName()); + return get(version); + } + + /** + * Returns the property expression. + * + * @param persistentProperty The persistent property + * @param The persistent property + * @return The property expression + * @see 4.8.0 + */ + @NonNull + default PersistentPropertyPath get(@NonNull PersistentProperty persistentProperty) { + return get(persistentProperty.getName()); } } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java index 8492ab4bd6..88d8065572 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java @@ -377,7 +377,7 @@ private CriterionHandler likeConcatC } } return (Runnable) () -> query.append(p); - }).collect(Collectors.toList())); + }).toList()); }; } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java index 38fc7203bc..bc35c7b617 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java @@ -565,7 +565,7 @@ private List createIndexes(PersistentEntity entity, String tableName) { .map(idxes -> idxes.getAnnotations(VALUE_MEMBER, Index.class)); Stream.of(indexes) - .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty)) + .flatMap(Optional::stream) .flatMap(Collection::stream) .forEach(index -> indexStatements.add(addIndex(entity, new IndexConfiguration(index, tableName, entity.getPersistedName())))); @@ -764,9 +764,8 @@ private List getJoinedColumns(AnnotationMetadata annotationMetadata, boo if (joinTable != null) { return joinTable.getAnnotations(associationOwner ? "joinColumns" : "inverseJoinColumns") .stream() - .map(ann -> ann.stringValue(columnType).orElse(null)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .flatMap(ann -> ann.stringValue(columnType).stream()) + .toList(); } return Collections.emptyList(); } @@ -780,7 +779,7 @@ private Collection getJoinTableAssociations(PersistentEntity persis return isForeignKeyWithJoinTable(a); } return false; - }).map(p -> (Association) p).collect(Collectors.toList()); + }).map(p -> (Association) p).toList(); } @Override @@ -920,14 +919,12 @@ public String resolveJoinType(Join.Type jt) { if (!this.dialect.supportsJoinType(jt)) { throw new IllegalArgumentException("Unsupported join type [" + jt + "] by dialect [" + this.dialect + "]"); } - - String joinType = switch (jt) { + return switch (jt) { case LEFT, LEFT_FETCH -> " LEFT JOIN "; case RIGHT, RIGHT_FETCH -> " RIGHT JOIN "; case OUTER, OUTER_FETCH -> " FULL OUTER JOIN "; default -> " INNER JOIN "; }; - return joinType; } @NonNull @@ -1466,10 +1463,10 @@ protected void buildJoin(String joinType, List associationJoinColumns = resolveJoinTableAssociatedColumns(annotationMetadata, !isAssociationOwner, associatedEntity, namingStrategy); List associationJoinTableColumns = resolveJoinTableJoinColumns(annotationMetadata, !isAssociationOwner, associatedEntity, namingStrategy); if (escape) { - ownerJoinColumns = ownerJoinColumns.stream().map(this::quote).collect(Collectors.toList()); - ownerJoinTableColumns = ownerJoinTableColumns.stream().map(this::quote).collect(Collectors.toList()); - associationJoinColumns = associationJoinColumns.stream().map(this::quote).collect(Collectors.toList()); - associationJoinTableColumns = associationJoinTableColumns.stream().map(this::quote).collect(Collectors.toList()); + ownerJoinColumns = ownerJoinColumns.stream().map(this::quote).toList(); + ownerJoinTableColumns = ownerJoinTableColumns.stream().map(this::quote).toList(); + associationJoinColumns = associationJoinColumns.stream().map(this::quote).toList(); + associationJoinTableColumns = associationJoinTableColumns.stream().map(this::quote).toList(); } String joinTableSchema = annotationMetadata @@ -1579,15 +1576,13 @@ private void join(StringBuilder sb, onLeftColumns.addAll( joinColumnsHolder.getAnnotations(VALUE_MEMBER) .stream() - .map(ann -> ann.stringValue(isOwner ? "name" : "referencedColumnName").orElse(null)) - .filter(Objects::nonNull) + .flatMap(ann -> ann.stringValue(isOwner ? "name" : "referencedColumnName").stream()) .toList() ); onRightColumns.addAll( joinColumnsHolder.getAnnotations(VALUE_MEMBER) .stream() - .map(ann -> ann.stringValue(isOwner ? "referencedColumnName" : "name").orElse(null)) - .filter(Objects::nonNull) + .flatMap(ann -> ann.stringValue(isOwner ? "referencedColumnName" : "name").stream()) .toList() ); } @@ -1613,8 +1608,8 @@ private void join(StringBuilder sb, getTableName(associatedEntity), rightTableAlias, leftTableAlias, - escape ? onLeftColumns.stream().map(this::quote).collect(Collectors.toList()) : onLeftColumns, - escape ? onRightColumns.stream().map(this::quote).collect(Collectors.toList()) : onRightColumns + escape ? onLeftColumns.stream().map(this::quote).toList() : onLeftColumns, + escape ? onRightColumns.stream().map(this::quote).toList() : onRightColumns ); } @@ -1633,31 +1628,6 @@ private Optional findOwner(List associations, Per return Optional.empty(); } - private void join(StringBuilder sb, - QueryModel queryModel, - String joinType, - String tableName, - String tableAlias, - String onTableName, - String onTableColumn, - String tableColumnName) { - sb - .append(joinType) - .append(tableName) - .append(SPACE) - .append(tableAlias); - appendForUpdate(QueryPosition.AFTER_TABLE_NAME, queryModel, sb); - sb - .append(" ON ") - .append(onTableName) - .append(DOT) - .append(onTableColumn) - .append('=') - .append(tableAlias) - .append(DOT) - .append(tableColumnName); - } - private void join(StringBuilder builder, QueryModel queryModel, String joinType, diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java index 031f1706fa..f2e7099aa2 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.criteria.ParameterExpression; @@ -30,6 +31,17 @@ @Experimental public interface SourcePersistentEntityCriteriaBuilder extends PersistentEntityCriteriaBuilder { + /** + * Create parameter expression from {@link ParameterElement}. + * + * @param property The property + * @param expression The expression + * @param The expression type + * @return new parameter + */ + @NonNull + ParameterExpression expression(@NonNull PersistentProperty property, @NonNull String expression); + /** * Create parameter expression from {@link ParameterElement}. * @@ -37,7 +49,8 @@ public interface SourcePersistentEntityCriteriaBuilder extends PersistentEntityC * @param The expression type * @return new parameter */ - @NonNull ParameterExpression parameter(@NonNull ParameterElement parameterElement); + @NonNull + ParameterExpression parameter(@NonNull ParameterElement parameterElement); /** * Create parameter expression from {@link ParameterElement} that is representing an entity instance. @@ -46,5 +59,6 @@ public interface SourcePersistentEntityCriteriaBuilder extends PersistentEntityC * @param The expression type * @return new parameter */ - @NonNull ParameterExpression entityPropertyParameter(@NonNull ParameterElement entityParameter); + @NonNull + ParameterExpression entityPropertyParameter(@NonNull ParameterElement entityParameter); } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java index f3ef3844b9..bb2131d7cb 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; import io.micronaut.data.model.jpa.criteria.impl.AbstractCriteriaBuilder; import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder; @@ -67,6 +68,11 @@ public SourcePersistentEntityCriteriaUpdate createCriteriaUpdate(Class return new SourcePersistentEntityCriteriaUpdateImpl<>(methodMatchContext::getEntity, targetEntity); } + @Override + public ParameterExpression expression(PersistentProperty property, String expression) { + return new SourceParameterStringExpressionImpl(property, expression); + } + @Override public ParameterExpression parameter(ParameterElement parameterElement) { return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), parameterElement, false); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterStringExpressionImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterStringExpressionImpl.java new file mode 100644 index 0000000000..cef33df650 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterStringExpressionImpl.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.processor.model.criteria.impl; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.DataType; +import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.PersistentProperty; +import io.micronaut.data.model.jpa.criteria.impl.ParameterExpressionImpl; +import io.micronaut.data.model.query.BindingParameter; +import io.micronaut.data.model.query.builder.QueryParameterBinding; + +import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.notSupportedOperation; + +/** + * The internal source implementation of {@link ParameterExpressionImpl}. + * + * @author Denis Stepanov + * @since 4.8.0 + */ +@Internal +public final class SourceParameterStringExpressionImpl extends ParameterExpressionImpl implements BindingParameter { + + private final PersistentProperty persistentProperty; + private final String expression; + + public SourceParameterStringExpressionImpl(PersistentProperty persistentProperty, + String expression) { + super(null, null); + this.persistentProperty = persistentProperty; + this.expression = expression; + } + + @Override + public Class getParameterType() { + throw notSupportedOperation(); + } + + @Override + public QueryParameterBinding bind(BindingContext bindingContext) { + String bindName; + if (bindingContext.getName() == null) { + bindName = String.valueOf(bindingContext.getIndex()); + } else { + bindName = bindingContext.getName(); + } + return new QueryParameterBinding() { + + @Override + public String getName() { + return SourceParameterStringExpressionImpl.this.getName(); + } + + @Override + public String[] getPropertyPath() { + return new String[]{persistentProperty.getName()}; + } + + @Override + public String getKey() { + return bindName; + } + + @Override + public DataType getDataType() { + return persistentProperty.getDataType(); + } + + @Override + public JsonDataType getJsonDataType() { + return persistentProperty.getJsonDataType(); + } + + @Override + public boolean isExpression() { + return true; + } + + @Override + public Object getValue() { + return expression; + } + }; + } + +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java index 51be68439f..ed4f1df103 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java @@ -16,6 +16,7 @@ package io.micronaut.data.processor.model.criteria.impl; import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; import io.micronaut.data.model.jpa.criteria.impl.AbstractCriteriaBuilder; import io.micronaut.data.processor.model.SourcePersistentEntity; @@ -66,6 +67,11 @@ public SourcePersistentEntityCriteriaUpdate createCriteriaUpdate(Class return new SourcePersistentEntityCriteriaUpdateImpl<>(entityResolver, targetEntity); } + @Override + public ParameterExpression expression(PersistentProperty property, String expression) { + throw notSupportedOperation(); + } + @Override public ParameterExpression parameter(ParameterElement parameterElement) { throw notSupportedOperation(); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/MatchContext.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/MatchContext.java index d1da32539d..2de35f3760 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/MatchContext.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/MatchContext.java @@ -137,7 +137,6 @@ public ClassElement getReturnType() { /** * @return The parameters */ - @NonNull public ParameterElement[] getParameters() { return parameters; } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java index 8e6ff37198..61d79e5963 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java @@ -23,7 +23,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.core.naming.NameUtils; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.CollectionUtils; @@ -62,6 +64,7 @@ import io.micronaut.data.processor.visitors.finders.RawQueryMethodMatcher; import io.micronaut.data.processor.visitors.finders.TypeUtils; import io.micronaut.data.repository.GenericRepository; +import io.micronaut.inject.annotation.EvaluatedExpressionReferenceCounter; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.MethodElement; @@ -515,6 +518,25 @@ private void bindParameters(boolean supportsImplicitQueries, if (!supportsImplicitQueries) { builder.member(DataMethodQueryParameter.META_MEMBER_NAME, p.getName()); } + Object value = p.getValue(); + if (value != null) { + if (value instanceof String expression) { + // TODO: Support adding an expression annotation value in Core + String originatingClassName = DataMethodQueryParameter.class.getName(); + String packageName = NameUtils.getPackageName(originatingClassName); + String simpleClassName = NameUtils.getSimpleName(originatingClassName); + String exprClassName = "%s.$%s%s".formatted(packageName, simpleClassName, EvaluatedExpressionReferenceCounter.EXPR_SUFFIX); + + Integer expressionIndex = EvaluatedExpressionReferenceCounter.nextIndex(exprClassName); + + builder.members(Map.of( + AnnotationMetadata.VALUE_MEMBER, + new EvaluatedExpressionReference(expression, originatingClassName, AnnotationMetadata.VALUE_MEMBER, exprClassName + expressionIndex) + )); + } else { + throw new IllegalStateException("The expression value should be a String!"); + } + } } if (supportsImplicitQueries) { builder.member(DataMethodQueryParameter.META_MEMBER_NAME, p.getKey()); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java index 7c25d27148..536338655e 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java @@ -16,11 +16,13 @@ package io.micronaut.data.processor.visitors.finders; import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; @@ -31,8 +33,11 @@ import io.micronaut.data.annotation.QueryHint; import io.micronaut.data.annotation.Relation; import io.micronaut.data.annotation.RepositoryConfiguration; +import io.micronaut.data.annotation.TenantId; import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.annotation.Where; +import io.micronaut.data.annotation.WithTenantId; +import io.micronaut.data.annotation.WithoutTenantId; import io.micronaut.data.annotation.repeatable.QueryHints; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.model.Association; @@ -41,9 +46,6 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaUpdate; import io.micronaut.data.model.jpa.criteria.PersistentEntityFrom; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; @@ -152,9 +154,9 @@ public final MethodMatchInfo buildMatchInfo(MethodMatchContext matchContext) { if (supportedByImplicitQueries() && matchContext.supportsImplicitQueries() && hasNoWhereAndJoinDeclaration(matchContext)) { FindersUtils.InterceptorMatch entry = resolveReturnTypeAndInterceptor(matchContext); methodMatchInfo = new MethodMatchInfo( - getOperationType(), - entry.returnType(), - entry.interceptor() + getOperationType(), + entry.returnType(), + entry.interceptor() ); } else { methodMatchInfo = build(matchContext); @@ -188,16 +190,16 @@ protected FindersUtils.InterceptorMatch resolveReturnTypeAndInterceptor(MethodMa ParameterElement entitiesParameter = getEntitiesParameter(); return FindersUtils.resolveInterceptorTypeByOperationType( - entityParameter != null, - entitiesParameter != null, - getOperationType(), - matchContext); + entityParameter != null, + entitiesParameter != null, + getOperationType(), + matchContext); } @Nullable - private Predicate extractPredicates(List queryParams, - PersistentEntityRoot root, - SourcePersistentEntityCriteriaBuilder cb) { + protected final Predicate extractPredicates(List queryParams, + PersistentEntityRoot root, + SourcePersistentEntityCriteriaBuilder cb) { if (CollectionUtils.isNotEmpty(queryParams)) { PersistentEntity rootEntity = root.getPersistentEntity(); List predicates = new ArrayList<>(queryParams.size()); @@ -241,142 +243,67 @@ private Predicate extractPredicates(List queryParams, } /** - * Apply predicates. - * - * @param querySequence The query sequence - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(String querySequence, - ParameterElement[] parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaQuery query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(querySequence, Arrays.asList(parameters).iterator(), root, cb); - if (predicate != null) { - query.where(predicate); - } - } - - /** - * Apply predicates. - * - * @param querySequence The query sequence - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(String querySequence, - ParameterElement[] parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaDelete query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(querySequence, Arrays.asList(parameters).iterator(), root, cb); - if (predicate != null) { - query.where(predicate); - } - } - - /** - * Apply predicates. - * - * @param querySequence The query sequence - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(String querySequence, - ParameterElement[] parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(querySequence, Arrays.asList(parameters).iterator(), root, cb); - if (predicate != null) { - query.where(predicate); - } - } - - /** - * Apply predicates based on parameters. - * - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaQuery query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(parameters, root, cb); - if (predicate != null) { - query.where(predicate); - } - } - - /** - * Apply a basic predicate. - * - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - } - - /** - * Apply predicates based on parameters. + * Intercept the predicate being applied. * - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type + * @param matchContext The matchContext + * @param notConsumedParameters The parameters + * @param root The root + * @param cb The criteria builder + * @param existingPredicate The existing predicate + * @param The entity type + * @return A new predicate */ - protected void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(parameters, root, cb); - if (predicate != null) { - query.where(predicate); - } - } - - /** - * Apply predicates based on parameters. - * - * @param parameters The parameters - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type - */ - protected void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaDelete query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate predicate = extractPredicates(parameters, root, cb); - if (predicate != null) { - query.where(predicate); + @Nullable + protected Predicate interceptPredicate(MethodMatchContext matchContext, + List notConsumedParameters, + PersistentEntityRoot root, + SourcePersistentEntityCriteriaBuilder cb, + @Nullable Predicate existingPredicate) { + if (matchContext.getMethodElement().hasAnnotation(WithoutTenantId.class)) { + return existingPredicate; + } + PersistentProperty tenantIdProperty = root.getPersistentEntity().getPersistentProperties() + .stream() + .filter(p -> p.getAnnotationMetadata().hasStereotype(TenantId.class)) + .findFirst() + .orElse(null); + if (tenantIdProperty != null) { + AnnotationValue withTenantId = matchContext.getMethodElement().getAnnotation(WithTenantId.class); + Predicate tenantIdEqual; + if (withTenantId != null) { + Object value = withTenantId.getValues().get(AnnotationMetadata.VALUE_MEMBER); + if (value instanceof String constant) { + tenantIdEqual = cb.equal( + root.get(tenantIdProperty), + cb.literal(constant) + ); + } else if (value instanceof EvaluatedExpressionReference ref) { + tenantIdEqual = cb.equal( + root.get(tenantIdProperty), + cb.expression(tenantIdProperty, (String) ref.annotationValue()) + ); + } else { + throw new IllegalStateException("Unrecognized tenantId annotation: " + withTenantId); + } + } else { + tenantIdEqual = cb.equal( + root.get(tenantIdProperty), + cb.expression(tenantIdProperty, "#{ctx[T(io.micronaut.data.runtime.multitenancy.TenantResolver)].resolveTenantIdentifier()}") + ); + } + if (existingPredicate != null) { + return cb.and(existingPredicate, tenantIdEqual); + } else { + return tenantIdEqual; + } } + return existingPredicate; } - private Predicate extractPredicates(String querySequence, - Iterator parametersIt, - PersistentEntityRoot root, - SourcePersistentEntityCriteriaBuilder cb) { + protected final Predicate extractPredicates(String querySequence, + Iterator parametersIt, + PersistentEntityRoot root, + SourcePersistentEntityCriteriaBuilder cb) { Predicate predicate = null; // if it contains operator and split @@ -502,14 +429,14 @@ private Predicate getPropertyRestriction(String propertyName, Expression prop = getProperty(root, propertyName); Predicate predicate = restriction.find(root, + cb, + prop, + provideParams(parameters, + restriction.getRequiredParameters(), + restriction.getName(), cb, - prop, - provideParams(parameters, - restriction.getRequiredParameters(), - restriction.getName(), - cb, - prop - ).toArray(new ParameterExpression[0])); + prop + ).toArray(new ParameterExpression[0])); if (negation) { predicate = predicate.not(); @@ -526,13 +453,13 @@ private Predicate getRestriction(PersistentEntityRoot root, property = root.id(); } return restriction.find(root, + cb, + provideParams(parameters, + restriction.getRequiredParameters(), + restriction.getName(), cb, - provideParams(parameters, - restriction.getRequiredParameters(), - restriction.getName(), - cb, - property - ).toArray(new ParameterExpression[0]) + property + ).toArray(new ParameterExpression[0]) ); } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/DeleteMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/DeleteMethodMatcher.java index 6320c7c38d..24b22a2fa8 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/DeleteMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/DeleteMethodMatcher.java @@ -17,16 +17,11 @@ import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.Embedded; -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; -import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; import io.micronaut.data.processor.model.SourcePersistentEntity; -import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder; import io.micronaut.data.processor.visitors.MatchFailedException; import io.micronaut.data.processor.visitors.MethodMatchContext; import io.micronaut.data.processor.visitors.finders.criteria.DeleteCriteriaMethodMatch; import io.micronaut.inject.ast.ParameterElement; -import jakarta.persistence.criteria.Predicate; import java.util.Arrays; import java.util.List; @@ -94,47 +89,8 @@ protected MethodMatch match(MethodMatchContext matchContext, List void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaDelete query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate restriction = query.getRestriction(); - Predicate predicate = root.id().in(cb.entityPropertyParameter(finalEntitiesParameter)); - if (restriction == null) { - query.where(predicate); - } else { - query.where(cb.and(predicate, restriction)); - } - } - - @Override - protected ParameterElement getEntityParameter() { - return finalEntityParameter; - } - - @Override - protected ParameterElement getEntitiesParameter() { - return finalEntitiesParameter; - } - - }; - } - - ParameterElement entityParam = entityParameter == null ? entitiesParameter : entityParameter; return new DeleteCriteriaMethodMatch(matches, isReturning) { @@ -143,28 +99,6 @@ protected boolean supportedByImplicitQueries() { return supportedByImplicitQueries; } - @Override - protected void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaDelete query, - SourcePersistentEntityCriteriaBuilder cb) { - Predicate restriction = query.getRestriction(); - Predicate predicate; - if (rootEntity.getVersion() != null) { - predicate = cb.and( - cb.equal(root.id(), cb.entityPropertyParameter(entityParam)), - cb.equal(root.version(), cb.entityPropertyParameter(entityParam)) - ); - } else { - predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParam)); - } - if (restriction == null) { - query.where(predicate); - } else { - query.where(cb.and(predicate, restriction)); - } - } - @Override protected ParameterElement getEntityParameter() { return finalEntityParameter; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodNameParser.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodNameParser.java index 8c2f3c4d20..5a1b18ec62 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodNameParser.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodNameParser.java @@ -86,7 +86,13 @@ public Builder match(MatchId matchId, String... prefixes) { matchSteps.add((input, chain) -> { Matcher matcher = pattern.matcher(input); if (matcher.matches()) { - chain.matched(matchId, matcher.group(1), matcher.group(2)); + String next = matcher.group(2); + // Allow to skip the last part of the method if prefixed `$` + int ignoreLastPart = next.lastIndexOf("$"); + if (ignoreLastPart > 0) { + next = next.substring(0, ignoreLastPart); + } + chain.matched(matchId, matcher.group(1), next); } }); return this; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java index 1dc5e307e1..0ead0fbb9e 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java @@ -384,7 +384,6 @@ private static SourceParameterExpressionImpl bindingParameter(MethodMatchContext type); } - /** * Extract the expression type. * @param matchContext The match context diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java index 0ca970a04f..5c352218fd 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java @@ -23,7 +23,6 @@ import io.micronaut.data.annotation.EntityRepresentation; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.MappedEntity; -import io.micronaut.data.annotation.Version; import io.micronaut.data.model.Association; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentEntityUtils; @@ -42,14 +41,10 @@ import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Path; -import jakarta.persistence.criteria.Predicate; import java.util.Arrays; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -74,20 +69,22 @@ public UpdateMethodMatcher() { protected MethodMatch match(MethodMatchContext matchContext, List matches) { MethodElement methodElement = matchContext.getMethodElement(); ParameterElement[] parameters = methodElement.getParameters(); - ParameterElement idParameter = Arrays.stream(parameters).filter(p -> p.hasAnnotation(Id.class)).findFirst().orElse(null); boolean isReturning = matches.stream().anyMatch(m -> m.id() == QueryMatchId.RETURNING); - - if (parameters.length > 1 && idParameter != null) { - if (!isReturning && !TypeUtils.isValidBatchUpdateReturnType(methodElement)) { - throw new MatchFailedException("Update methods only support void or number based return types"); + if (parameters.length > 1) { + ParameterElement idParameter = Arrays.stream(parameters).filter(p -> p.hasAnnotation(Id.class)).findFirst().orElse(null); + if (idParameter != null) { + if (!isReturning && !TypeUtils.isValidBatchUpdateReturnType(methodElement)) { + throw new MatchFailedException("Update methods only support void or number based return types"); + } + return batchUpdate(matches, isReturning); } - return batchUpdate(matchContext, matches, idParameter, isReturning); } final ParameterElement entityParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isEntity(p.getGenericType())).findFirst().orElse(null); final ParameterElement entitiesParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isIterableOfEntity(p.getGenericType())).findFirst().orElse(null); - if ((entityParameter != null || entitiesParameter != null)) { + + if (entityParameter != null || entitiesParameter != null) { return entityUpdate(matches, entityParameter, entitiesParameter, isReturning); } @@ -105,25 +102,8 @@ private UpdateCriteriaMethodMatch entityUpdate(List matc final ParameterElement entityParam = entityParameter == null ? entitiesParameter : entityParameter; - @Override - protected void applyPredicates(PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); - Predicate predicate; - if (rootEntity.getVersion() != null) { - predicate = cb.and( - cb.equal(root.id(), cb.entityPropertyParameter(entityParam)), - cb.equal(root.version(), cb.entityPropertyParameter(entityParam)) - ); - } else { - predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParam)); - } - query.where(predicate); - } - - @Override - protected void addPropertiesToUpdate(MethodMatchContext matchContext, + protected void addPropertiesToUpdate(List nonConsumedParameters, + MethodMatchContext matchContext, PersistentEntityRoot root, PersistentEntityCriteriaUpdate query, SourcePersistentEntityCriteriaBuilder cb) { @@ -180,61 +160,17 @@ protected ParameterElement getEntitiesParameter() { }; } - private UpdateCriteriaMethodMatch batchUpdate(MethodMatchContext matchContext, - List matches, - ParameterElement idParameter, - boolean isReturning) { + private UpdateCriteriaMethodMatch batchUpdate(List matches, boolean isReturning) { return new UpdateCriteriaMethodMatch(matches, isReturning) { @Override - protected void applyPredicates(String querySequence, - ParameterElement[] parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - super.applyPredicates(querySequence, parameters, root, query, cb); - - applyPredicates(root, query, cb); - } - - @Override - protected void applyPredicates(List parameters, - PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - - applyPredicates(root, query, cb); - } - - @Override - protected void applyPredicates(PersistentEntityRoot root, - PersistentEntityCriteriaUpdate query, - SourcePersistentEntityCriteriaBuilder cb) { - - ParameterElement versionParameter = Arrays.stream(matchContext.getParameters()) - .filter(p -> p.hasAnnotation(Version.class)).findFirst().orElse(null); - Predicate predicate; - if (versionParameter != null) { - predicate = cb.and( - cb.equal(root.id(), cb.parameter(idParameter)), - cb.equal(root.version(), cb.parameter(versionParameter)) - ); - } else { - predicate = cb.equal(root.id(), cb.parameter(idParameter)); - } - query.where(predicate); - } - - @Override - protected void addPropertiesToUpdate(MethodMatchContext matchContext, + protected void addPropertiesToUpdate(List nonConsumedParameters, + MethodMatchContext matchContext, PersistentEntityRoot root, PersistentEntityCriteriaUpdate query, SourcePersistentEntityCriteriaBuilder cb) { List parameters = matchContext.getParametersNotInRole(); - List remainingParameters = parameters.stream() - .filter(p -> !p.hasAnnotation(Id.class) && !p.hasAnnotation(Version.class)) - .toList(); ParameterElement idParameter = parameters.stream().filter(p -> p.hasAnnotation(Id.class)).findFirst() .orElse(null); @@ -254,7 +190,7 @@ protected void addPropertiesToUpdate(MethodMatchContext matchContext, throw new MatchFailedException("Cannot update by ID for entity that has no ID"); } - for (ParameterElement parameter : remainingParameters) { + for (ParameterElement parameter : nonConsumedParameters) { String name = getParameterName(parameter); SourcePersistentProperty prop = entity.getPropertyByName(name); if (prop == null) { @@ -277,20 +213,14 @@ private UpdateCriteriaMethodMatch batchUpdateBy(List mat return new UpdateCriteriaMethodMatch(matches, isReturning) { @Override - protected void addPropertiesToUpdate(MethodMatchContext matchContext, + protected void addPropertiesToUpdate(List nonConsumedParameters, + MethodMatchContext matchContext, PersistentEntityRoot root, PersistentEntityCriteriaUpdate query, SourcePersistentEntityCriteriaBuilder cb) { - Set queryParameters = query.getParameters() - .stream() - .map(ParameterExpression::getName) - .collect(Collectors.toSet()); - for (ParameterElement p : matchContext.getParametersNotInRole()) { + for (ParameterElement p : nonConsumedParameters) { String parameterName = getParameterName(p); - if (queryParameters.contains(parameterName)) { - continue; - } PersistentEntity persistentEntity = root.getPersistentEntity(); PersistentPropertyPath path = persistentEntity.getPropertyPath(persistentEntity.getPath(parameterName).orElse(parameterName)); if (path != null) { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java index 5138c23551..4271241c45 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.data.intercept.annotation.DataMethod; +import io.micronaut.data.model.Embedded; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; @@ -25,6 +26,7 @@ import io.micronaut.data.model.query.QueryModel; import io.micronaut.data.model.query.builder.QueryBuilder; import io.micronaut.data.model.query.builder.QueryResult; +import io.micronaut.data.processor.model.SourcePersistentEntity; import io.micronaut.data.processor.model.SourcePersistentProperty; import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder; import io.micronaut.data.processor.model.criteria.impl.MethodMatchSourcePersistentEntityCriteriaBuilderImpl; @@ -37,8 +39,13 @@ import io.micronaut.data.processor.visitors.finders.QueryMatchId; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ParameterElement; +import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Selection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -81,7 +88,7 @@ protected void apply(MethodMatchContext matchContext, boolean predicatedApplied = false; for (MethodNameParser.Match match : matches) { if (match.id() == QueryMatchId.PREDICATE) { - applyPredicates(match.part(), matchContext.getParameters(), root, query, cb); + applyPredicates(matchContext, match.part(), matchContext.getParameters(), root, query, cb); predicatedApplied = true; } if (match.id() == QueryMatchId.RETURNING) { @@ -89,12 +96,77 @@ protected void apply(MethodMatchContext matchContext, } } if (!predicatedApplied) { - applyPredicates(matchContext.getParametersNotInRole(), root, query, cb); + applyPredicates(matchContext, matchContext.getParametersNotInRole(), root, query, cb); } applyJoinSpecs(root, joinSpecsAtMatchContext(matchContext, true)); } + private void applyPredicates(MethodMatchContext matchContext, + List parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaDelete query, + SourcePersistentEntityCriteriaBuilder cb) { + ParameterElement entityParameter = getEntityParameter(); + if (entityParameter == null) { + entityParameter = getEntitiesParameter(); + } + Predicate predicate; + if (entityParameter != null) { + final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); + if (rootEntity.getVersion() != null) { + predicate = cb.and( + cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)), + cb.equal(root.version(), cb.entityPropertyParameter(entityParameter)) + ); + } else { + boolean generateInIdList = getEntitiesParameter() != null + && !rootEntity.hasCompositeIdentity() + && !(rootEntity.getIdentity() instanceof Embedded); + if (generateInIdList) { + predicate = root.id().in(cb.entityPropertyParameter(entityParameter)); + } else { + predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)); + } + } + } else { + predicate = extractPredicates(parameters, root, cb); + } + predicate = interceptPredicate(matchContext, List.of(), root, cb, predicate); + if (predicate != null) { + query.where(predicate); + } + } + + /** + * Apply predicates. + * + * @param matchContext The matchContext + * @param querySequence The query sequence + * @param parameters The parameters + * @param root The root + * @param query The query + * @param cb The criteria builder + * @param The entity type + */ + private void applyPredicates(MethodMatchContext matchContext, + String querySequence, + ParameterElement[] parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaDelete query, + SourcePersistentEntityCriteriaBuilder cb) { + Iterator parametersIterator = Arrays.asList(parameters).iterator(); + Predicate predicate = extractPredicates(querySequence, parametersIterator, root, cb); + List remainingParameters = new ArrayList<>(parameters.length); + while (parametersIterator.hasNext()) { + remainingParameters.add(parametersIterator.next()); + } + predicate = interceptPredicate(matchContext, remainingParameters, root, cb, predicate); + if (predicate != null) { + query.where(predicate); + } + } + /** * Apply projections. * @@ -138,8 +210,8 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { boolean optimisticLock = ((AbstractPersistentEntityCriteriaDelete) criteriaQuery).hasVersionRestriction(); final AnnotationMetadataHierarchy annotationMetadataHierarchy = new AnnotationMetadataHierarchy( - matchContext.getRepositoryClass().getAnnotationMetadata(), - matchContext.getAnnotationMetadata() + matchContext.getRepositoryClass().getAnnotationMetadata(), + matchContext.getAnnotationMetadata() ); MethodResult result = analyzeMethodResult( @@ -172,12 +244,12 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { QueryResult queryResult = queryBuilder.buildDelete(annotationMetadataHierarchy, queryModel); return new MethodMatchInfo( - getOperationType(), - resultType, - interceptorType + getOperationType(), + resultType, + interceptorType ) - .optimisticLock(optimisticLock) - .queryResult(queryResult); + .optimisticLock(optimisticLock) + .queryResult(queryResult); } @Override diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java index d4ff1acdad..8f1af3093b 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java @@ -46,11 +46,14 @@ import io.micronaut.data.processor.visitors.finders.TypeUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.criteria.Order; +import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Selection; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -92,7 +95,7 @@ protected void apply(MethodMatchContext matchContext, applyProjections(matchContext, match.part(), root, query, cb); projectionApplied = true; } else if (match.id() == QueryMatchId.PREDICATE) { - applyPredicates(match.part(), matchContext.getParameters(), root, query, cb); + applyPredicates(matchContext, match.part(), matchContext.getParameters(), root, query, cb); predicatedApplied = true; } else if (match.id() == QueryMatchId.ORDER) { applyOrderBy(match.part(), root, query, cb); @@ -115,7 +118,7 @@ protected void apply(MethodMatchContext matchContext, } } if (!predicatedApplied) { - applyPredicates(matchContext.getParametersNotInRole(), root, query, cb); + applyPredicates(matchContext, matchContext.getParametersNotInRole(), root, query, cb); } if (!projectionApplied) { applyProjections(matchContext, "", root, query, cb); @@ -124,8 +127,33 @@ protected void apply(MethodMatchContext matchContext, applyJoinSpecs(root, joinSpecsAtMatchContext(matchContext, true)); } + private void applyPredicates(MethodMatchContext matchContext, + String querySequence, + ParameterElement[] parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaQuery query, + SourcePersistentEntityCriteriaBuilder cb) { + Predicate predicate = extractPredicates(querySequence, Arrays.asList(parameters).iterator(), root, cb); + predicate = interceptPredicate(matchContext, List.of(), root, cb, predicate); + if (predicate != null) { + query.where(predicate); + } + } + + private void applyPredicates(MethodMatchContext matchContext, + List parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaQuery query, + SourcePersistentEntityCriteriaBuilder cb) { + Predicate predicate = extractPredicates(parameters, root, cb); + predicate = interceptPredicate(matchContext, List.of(), root, cb, predicate); + if (predicate != null) { + query.where(predicate); + } + } + /** - * Apply the distinct valu. + * Apply the distinct value. * * @param query The query * @param The query type diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java index a7dd88c56f..eecc1e9b2c 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java @@ -17,6 +17,8 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.Version; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaUpdate; @@ -40,8 +42,11 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ParameterElement; +import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Selection; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -84,9 +89,10 @@ protected void apply(MethodMatchContext matchContext, boolean predicatedApplied = false; boolean projectionApplied = false; + List nonConsumedParameters = new ArrayList<>(matchContext.getParametersNotInRole()); for (MethodNameParser.Match match : matches) { if (match.id() == QueryMatchId.PREDICATE) { - applyPredicates(match.part(), matchContext.getParameters(), root, query, cb); + applyPredicates(matchContext, match.part(), nonConsumedParameters, root, query, cb); predicatedApplied = true; } if (match.id() == QueryMatchId.RETURNING) { @@ -95,7 +101,7 @@ protected void apply(MethodMatchContext matchContext, } } if (!predicatedApplied) { - applyPredicates(root, query, cb); + applyPredicates(matchContext, nonConsumedParameters, root, query, cb); } if (!projectionApplied) { applyProjections("", root, query, cb); @@ -103,14 +109,14 @@ protected void apply(MethodMatchContext matchContext, SourcePersistentEntity entity = matchContext.getRootEntity(); - addPropertiesToUpdate(matchContext, root, query, cb); + addPropertiesToUpdate(nonConsumedParameters, matchContext, root, query, cb); AbstractPersistentEntityCriteriaUpdate criteriaUpdate = (AbstractPersistentEntityCriteriaUpdate) query; // Add updatable auto-populated parameters entity.getPersistentProperties().stream() - .filter(p -> p != null && p.findAnnotation(AutoPopulated.class).map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)).orElse(false)) - .forEach(p -> query.set(p.getName(), cb.parameter((ParameterElement) null))); + .filter(p -> p != null && p.findAnnotation(AutoPopulated.class).map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)).orElse(false)) + .forEach(p -> query.set(p.getName(), cb.parameter((ParameterElement) null))); if (entity.getVersion() != null && !entity.getVersion().isGenerated() && criteriaUpdate.hasVersionRestriction()) { query.set(entity.getVersion().getName(), cb.parameter((ParameterElement) null)); @@ -121,14 +127,94 @@ protected void apply(MethodMatchContext matchContext, } } + private void applyPredicates(MethodMatchContext matchContext, + String querySequence, + List parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaUpdate query, + SourcePersistentEntityCriteriaBuilder cb) { + Iterator parametersIterator = parameters.iterator(); + Predicate predicate = extractPredicates(querySequence, parametersIterator, root, cb); + List remainingParameters = new ArrayList<>(parameters.size()); + while (parametersIterator.hasNext()) { + remainingParameters.add(parametersIterator.next()); + } + parameters.retainAll(remainingParameters); + predicate = interceptPredicate(matchContext, parameters, root, cb, predicate); + if (predicate != null) { + query.where(predicate); + } + } + + private void applyPredicates(MethodMatchContext matchContext, + List parameters, + PersistentEntityRoot root, + PersistentEntityCriteriaUpdate query, + SourcePersistentEntityCriteriaBuilder cb) { + Predicate predicate = interceptPredicate(matchContext, parameters, root, cb, null); + if (predicate != null) { + query.where(predicate); + } + } + + @Override + protected Predicate interceptPredicate(MethodMatchContext matchContext, + List notConsumedParameters, + PersistentEntityRoot root, + SourcePersistentEntityCriteriaBuilder cb, + Predicate existingPredicate) { + ParameterElement entityParameter = getEntityParameter(); + if (entityParameter == null) { + entityParameter = getEntitiesParameter(); + } + Predicate predicate = null; + if (entityParameter != null) { + final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); + if (rootEntity.getVersion() != null) { + predicate = cb.and( + cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)), + cb.equal(root.version(), cb.entityPropertyParameter(entityParameter)) + ); + } else { + predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)); + } + } else { + ParameterElement idParameter = notConsumedParameters.stream() + .filter(p -> p.hasAnnotation(Id.class)).findFirst().orElse(null); + ParameterElement versionParameter = notConsumedParameters.stream() + .filter(p -> p.hasAnnotation(Version.class)).findFirst().orElse(null); + if (idParameter != null) { + notConsumedParameters.remove(idParameter); + predicate = cb.equal(root.id(), cb.parameter(idParameter)); + } + if (versionParameter != null) { + notConsumedParameters.remove(versionParameter); + Predicate versionPredicate = cb.equal(root.version(), cb.parameter(versionParameter)); + if (predicate != null) { + predicate = cb.and(predicate, versionPredicate); + } else { + predicate = versionPredicate; + } + } + } + if (existingPredicate != null) { + if (predicate != null) { + predicate = cb.and(existingPredicate, predicate); + } else { + predicate = existingPredicate; + } + } + return super.interceptPredicate(matchContext, notConsumedParameters, root, cb, predicate); + } + /** * Apply projections. * * @param projection The querySequence - * @param root The root - * @param query The query - * @param cb The criteria builder - * @param The entity type + * @param root The root + * @param query The query + * @param cb The criteria builder + * @param The entity type */ protected void applyProjections(String projection, PersistentEntityRoot root, @@ -147,7 +233,8 @@ protected void applyProjections(String projection, } } - protected void addPropertiesToUpdate(MethodMatchContext matchContext, + protected void addPropertiesToUpdate(List nonConsumedParameters, + MethodMatchContext matchContext, PersistentEntityRoot root, PersistentEntityCriteriaUpdate query, SourcePersistentEntityCriteriaBuilder cb) { @@ -200,8 +287,8 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { boolean optimisticLock = query.hasVersionRestriction(); final AnnotationMetadataHierarchy annotationMetadataHierarchy = new AnnotationMetadataHierarchy( - matchContext.getRepositoryClass().getAnnotationMetadata(), - matchContext.getAnnotationMetadata() + matchContext.getRepositoryClass().getAnnotationMetadata(), + matchContext.getAnnotationMetadata() ); QueryBuilder queryBuilder = matchContext.getQueryBuilder(); diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy index 8a500493bc..080fe5be34 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy @@ -367,4 +367,66 @@ interface BookRepository extends GenericRepository { getResultDataType(deleteReturningMethod) == DataType.ENTITY } + void "POSTGRES test build delete with tenant id"() { + given: + def repository = buildRepository('test.AccountRepository', """ +import io.micronaut.data.annotation.Id; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.tck.entities.Account; + +@JdbcRepository(dialect= Dialect.POSTGRES) +interface AccountRepository extends CrudRepository { + + List deleteReturning(Long id); + +} +""") + when: + def deleteReturningCustomMethod = repository.findPossibleMethods("deleteReturning").findFirst().get() + then: + getQuery(deleteReturningCustomMethod) == 'DELETE FROM "account" WHERE ("id" = ? AND "tenancy" = ?) RETURNING "id","name","tenancy"' + getParameterPropertyPaths(deleteReturningCustomMethod) == ["id", "tenancy"] as String[] + getDataResultType(deleteReturningCustomMethod) == "io.micronaut.data.tck.entities.Account" + getDataInterceptor(deleteReturningCustomMethod) == "io.micronaut.data.intercept.DeleteReturningManyInterceptor" + getResultDataType(deleteReturningCustomMethod) == DataType.ENTITY + + when: + def deleteOne = repository.findPossibleMethods("delete").findFirst().get() + then: + getQuery(deleteOne) == 'DELETE FROM "account" WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(deleteOne) == ["id", "tenancy"] as String[] + getDataResultType(deleteOne) == "void" + getDataInterceptor(deleteOne) == "io.micronaut.data.intercept.DeleteOneInterceptor" + getResultDataType(deleteOne) == null + + when: + def deleteAll = repository.findPossibleMethods("deleteAll").findFirst().get() + then: + getQuery(deleteAll) == 'DELETE FROM "account" WHERE ("tenancy" = ?)' + getParameterPropertyPaths(deleteAll) == ["tenancy"] as String[] + getDataResultType(deleteAll) == "void" + getDataInterceptor(deleteAll) == "io.micronaut.data.intercept.DeleteAllInterceptor" + getResultDataType(deleteAll) == null + + when: + def deleteEntities = repository.findMethod("deleteAll", Iterable).get() + then: + getQuery(deleteEntities) == 'DELETE FROM "account" WHERE ("id" IN (?) AND "tenancy" = ?)' + getParameterPropertyPaths(deleteEntities) == ["id", "tenancy"] as String[] + getDataResultType(deleteEntities) == "void" + getDataInterceptor(deleteEntities) == "io.micronaut.data.intercept.DeleteAllInterceptor" + getResultDataType(deleteEntities) == null + + when: + def deleteById = repository.findPossibleMethods("deleteById").findFirst().get() + then: + getQuery(deleteById) == 'DELETE FROM "account" WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(deleteById) == ["id", "tenancy"] as String[] + getDataResultType(deleteById) == "void" + getDataInterceptor(deleteById) == "io.micronaut.data.intercept.DeleteAllInterceptor" + getResultDataType(deleteById) == null + } + } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy index 4eb458faae..8f351bd94c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy @@ -565,4 +565,28 @@ interface PersonRepository extends GenericRepository { getResultDataType(saveReturningMethod) == DataType.INTEGER getOperationType(saveReturningMethod) == DataMethod.OperationType.INSERT } + + void "POSTGRES save with tenant id"() { + given: + def repository = buildRepository('test.AccountRepository', """ +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.tck.entities.Account; + +@JdbcRepository(dialect= Dialect.POSTGRES) +interface AccountRepository extends CrudRepository { +} +""") + when: + def saveMethod = repository.findPossibleMethods("save").findFirst().get() + then: + getQuery(saveMethod) == 'INSERT INTO "account" ("name","tenancy") VALUES (?,?)' + getDataResultType(saveMethod) == "io.micronaut.data.tck.entities.Account" + getParameterPropertyPaths(saveMethod) == ["name", "tenancy"] as String[] + getDataInterceptor(saveMethod) == "io.micronaut.data.intercept.SaveEntityInterceptor" + getResultDataType(saveMethod) == DataType.ENTITY + getOperationType(saveMethod) == DataMethod.OperationType.INSERT + } } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index 430bb70406..d7dcdfb9a9 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -21,7 +21,6 @@ import io.micronaut.data.intercept.annotation.DataMethod import io.micronaut.data.model.DataType import io.micronaut.data.model.Pageable import io.micronaut.data.model.PersistentEntity - import io.micronaut.data.model.entities.Invoice import io.micronaut.data.model.query.QueryModel import io.micronaut.data.model.query.builder.sql.Dialect @@ -31,7 +30,6 @@ import io.micronaut.data.processor.visitors.AbstractDataSpec import io.micronaut.data.tck.entities.Author import io.micronaut.data.tck.entities.Restaurant import io.micronaut.data.tck.jdbc.entities.EmployeeGroup -import io.micronaut.inject.ExecutableMethod import spock.lang.Issue import spock.lang.PendingFeature import spock.lang.Unroll @@ -1338,4 +1336,44 @@ interface UserRoleRepository extends GenericRepository { countQuery == 'SELECT COUNT(*) FROM `user_role_composite` user_role_' countDistinctQuery == 'SELECT COUNT(DISTINCT( CONCAT(user_role_.`id_user_id`,user_role_.`id_role_id`))) FROM `user_role_composite` user_role_' } + + void "test query with a tenant id"() { + given: + def repository = buildRepository('test.AccountRepository', """ + +import io.micronaut.data.annotation.WithTenantId; +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.entities.Account; + +@JdbcRepository(dialect = Dialect.MYSQL) +interface AccountRepository extends GenericRepository { + + @WithoutTenantId + List findAll\$withAllTenants(); + + @WithTenantId("bar") + List findAll\$withTenantBar(); + + @WithTenantId("foo") + List findAll\$withTenantFoo(); + + List findAll(); + + Account findOneByName(String name); +} +""") + def findOneByNameMethod = repository.getRequiredMethod("findOneByName", String) + def findAll__withAllTenantsMethod = repository.findPossibleMethods("findAll\$withAllTenants").findFirst().get() + def findAll__withTenantBar = repository.findPossibleMethods("findAll\$withTenantBar").findFirst().get() + def findAll__withTenantFoo = repository.findPossibleMethods("findAll\$withTenantFoo").findFirst().get() + expect: + getQuery(repository.getRequiredMethod("findAll")) == 'SELECT account_.`id`,account_.`name`,account_.`tenancy` FROM `account` account_ WHERE (account_.`tenancy` = ?)' + getQuery(findOneByNameMethod) == 'SELECT account_.`id`,account_.`name`,account_.`tenancy` FROM `account` account_ WHERE (account_.`name` = ? AND account_.`tenancy` = ?)' + getParameterPropertyPaths(findOneByNameMethod) == ["name", "tenancy"] as String[] + getQuery(findAll__withAllTenantsMethod) == 'SELECT account_.`id`,account_.`name`,account_.`tenancy` FROM `account` account_' + getQuery(findAll__withTenantBar) == 'SELECT account_.`id`,account_.`name`,account_.`tenancy` FROM `account` account_ WHERE (account_.`tenancy` = \'bar\')' + getQuery(findAll__withTenantFoo) == 'SELECT account_.`id`,account_.`name`,account_.`tenancy` FROM `account` account_ WHERE (account_.`tenancy` = \'foo\')' + } } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy index eff4117302..5c7585cfcf 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy @@ -241,12 +241,12 @@ interface CompanyRepository extends CrudRepository { def updateByNameMethod = repository.findPossibleMethods("updateByName").findFirst().get() then: - getQuery(updateByNameMethod) == "UPDATE `company` SET `last_updated`=? WHERE (`name` = ?)" - getDataTypes(updateByNameMethod) == [DataType.TIMESTAMP, DataType.STRING] - getParameterBindingIndexes(updateByNameMethod) == ["-1", "0"] - getParameterPropertyPaths(updateByNameMethod) == ["lastUpdated", "name"] - getParameterAutoPopulatedProperties(updateByNameMethod) == ["lastUpdated", ""] - getParameterRequiresPreviousPopulatedValueProperties(updateByNameMethod) == ["", ""] + getQuery(updateByNameMethod) == "UPDATE `company` SET `name`=?,`last_updated`=? WHERE (`name` = ?)" + getDataTypes(updateByNameMethod) == [DataType.STRING, DataType.TIMESTAMP, DataType.STRING] + getParameterBindingIndexes(updateByNameMethod) == ["1", "-1", "0"] + getParameterPropertyPaths(updateByNameMethod) == ["name", "lastUpdated", "name"] + getParameterAutoPopulatedProperties(updateByNameMethod) == ["", "lastUpdated", ""] + getParameterRequiresPreviousPopulatedValueProperties(updateByNameMethod) == ["", "", ""] when: def updateByLastUpdatedMethod = repository.findPossibleMethods("updateByLastUpdated").findFirst().get() @@ -554,6 +554,84 @@ interface TestRepository extends CrudRepository { updateQuery == 'UPDATE "ARTICLE" SET "NAME"=?,"PRICE"=? WHERE ("ID" = ? AND "VERSION" = ?)' } + void "POSTGRES test update with tenant id"() { + given: + def repository = buildRepository('test.AccountRepository', """ +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.Account; + +@JdbcRepository(dialect= Dialect.POSTGRES) +interface AccountRepository extends CrudRepository { + + List updateReturning(List books); + + void updateByIdAndTenancy(Long id, String tenancy, String name); + + void updateAccount1(@Id Long id, String tenancy, String name); + void updateAccount2(@Id Long id, String name); + + void updateById(@Id Long id, String name); + void update(@Id Long id, String name); + +} +""") + when: + def updateReturningCustomMethod = repository.findPossibleMethods("updateReturning").findFirst().get() + then: + getQuery(updateReturningCustomMethod) == 'UPDATE "account" SET "name"=?,"tenancy"=? WHERE ("id" = ? AND "tenancy" = ?) RETURNING "id","name","tenancy"' + getParameterPropertyPaths(updateReturningCustomMethod) == ["name", "tenancy", "id", "tenancy"] as String[] + getDataResultType(updateReturningCustomMethod) == "io.micronaut.data.tck.entities.Account" + getDataInterceptor(updateReturningCustomMethod) == "io.micronaut.data.intercept.UpdateAllEntitiesInterceptor" + getResultDataType(updateReturningCustomMethod) == DataType.ENTITY + + when: + def updateByIdAndTenancyMethod = repository.findPossibleMethods("updateByIdAndTenancy").findFirst().get() + then: + getQuery(updateByIdAndTenancyMethod) == 'UPDATE "account" SET "name"=?,"tenancy"=? WHERE ("id" = ? AND "tenancy" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(updateByIdAndTenancyMethod) == ["name", "tenancy", "id", "tenancy", "tenancy"] as String[] + getDataResultType(updateByIdAndTenancyMethod) == "void" + getDataInterceptor(updateByIdAndTenancyMethod) == "io.micronaut.data.intercept.UpdateInterceptor" + getResultDataType(updateByIdAndTenancyMethod) == null + + when: + def updateAccount1 = repository.findPossibleMethods("updateAccount1").findFirst().get() + then: + getQuery(updateAccount1) == 'UPDATE "account" SET "tenancy"=?,"name"=? WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(updateAccount1) == ["tenancy", "name", "id", "tenancy"] as String[] + getDataResultType(updateAccount1) == "void" + getDataInterceptor(updateAccount1) == "io.micronaut.data.intercept.UpdateInterceptor" + getResultDataType(updateAccount1) == null + + when: + def updateAccount2 = repository.findPossibleMethods("updateAccount2").findFirst().get() + then: + getQuery(updateAccount2) == 'UPDATE "account" SET "name"=?,"tenancy"=? WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(updateAccount2) == ["name", "tenancy", "id", "tenancy"] as String[] + getDataResultType(updateAccount2) == "void" + getDataInterceptor(updateAccount2) == "io.micronaut.data.intercept.UpdateInterceptor" + getResultDataType(updateAccount2) == null + + when: + def updateById = repository.findPossibleMethods("updateById").findFirst().get() + then: + getQuery(updateById) == 'UPDATE "account" SET "name"=?,"tenancy"=? WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(updateById) == ["name", "tenancy", "id", "tenancy"] as String[] + getDataResultType(updateById) == "void" + getDataInterceptor(updateById) == "io.micronaut.data.intercept.UpdateInterceptor" + getResultDataType(updateById) == null + + when: + def update = repository.findMethod("update", Long, String).get() + then: + getQuery(update) == 'UPDATE "account" SET "name"=?,"tenancy"=? WHERE ("id" = ? AND "tenancy" = ?)' + getParameterPropertyPaths(update) == ["name", "tenancy", "id", "tenancy"] as String[] + getDataResultType(update) == "void" + getDataInterceptor(update) == "io.micronaut.data.intercept.UpdateInterceptor" + getResultDataType(update) == null + } + // void "ORACLE test build update returning "() { // given: // def repository = buildRepository('test.BookRepository', """ diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy index fd56588825..3be6fd566c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy @@ -19,6 +19,7 @@ import groovy.transform.CompileStatic import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationMetadataProvider import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.convert.ConversionContext import io.micronaut.data.annotation.Join import io.micronaut.data.annotation.Query import io.micronaut.data.intercept.annotation.DataMethod @@ -95,6 +96,10 @@ class TestUtils { return getParameterBindingPaths(metadata.getAnnotation(DataMethod)) } + static Object[] getParameterValues(AnnotationMetadataProvider metadata) { + return getParameterValues(metadata.getAnnotation(DataMethod)) + } + static DataType[] getDataTypes(AnnotationMetadataProvider metadata) { return getDataTypes(metadata.getAnnotation(DataMethod)) } @@ -175,6 +180,15 @@ class TestUtils { .toArray(Boolean[]::new) } + static Object[] getParameterValues(AnnotationValue annotationValue) { + return annotationValue.getAnnotations(DataMethod.META_MEMBER_PARAMETERS, DataMethodQueryParameter) + .stream() + .map(p -> { + p.get(AnnotationMetadata.VALUE_MEMBER, Object).orElse(null) + }) + .toArray(Object[]::new) + } + private static String getPropertyPath(AnnotationValue p) { def propertyPath def prop = p.stringValue(DataMethodQueryParameter.META_MEMBER_PROPERTY) 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 cf59b542f6..7d701e7013 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 @@ -647,14 +647,22 @@ private Flux withConnectionFlux(@NonNull PreparedDataOperation operati .getParameterInRole(R2dbcRepository.PARAMETER_TX_STATUS_ROLE, ReactiveTransactionStatus.class).orElse(null); if (tx != null) { try { - return Flux.from(callback.apply(tx.getConnection())); + return Flux.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + return callback.apply(tx.getConnection()); + } + }); } catch (Exception e) { return Flux.error(new TransactionSystemException("Error invoking doInTransaction handler: " + e.getMessage(), e)); } } return connectionOperations.withConnectionFlux( isWrite ? ConnectionDefinition.DEFAULT : ConnectionDefinition.READ_ONLY, - status -> callback.apply(status.getConnection()) + status -> Flux.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + return callback.apply(status.getConnection()); + } + }) ); } @@ -666,14 +674,22 @@ private Mono withConnectionMono(@NonNull PreparedDataOperation operati .getParameterInRole(R2dbcRepository.PARAMETER_TX_STATUS_ROLE, ReactiveTransactionStatus.class).orElse(null); if (tx != null) { try { - return Mono.fromDirect(callback.apply(tx.getConnection())); + return Mono.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + return callback.apply(tx.getConnection()); + } + }); } catch (Exception e) { return Mono.error(new TransactionSystemException("Error invoking doInTransaction handler: " + e.getMessage(), e)); } } return connectionOperations.withConnectionMono( isWrite ? ConnectionDefinition.DEFAULT : ConnectionDefinition.READ_ONLY, - status -> callback.apply(status.getConnection()) + status -> Mono.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + return callback.apply(status.getConnection()); + } + }) ); } @@ -894,12 +910,16 @@ private Statement prepare(Connection connection) throws RuntimeException { } private void setParameters(Statement stmt, SqlStoredQuery storedQuery) { - data = data.map(d -> { + data = data.flatMap(d -> { if (d.vetoed) { - return d; + return Mono.just(d); } - storedQuery.bindParameters(new R2dbcParameterBinder(ctx, stmt, storedQuery), ctx.invocationContext, d.entity, d.previousValues); - return d; + return Mono.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + storedQuery.bindParameters(new R2dbcParameterBinder(ctx, stmt, storedQuery), ctx.invocationContext, d.entity, d.previousValues); + } + return Mono.just(d); + }); }); } diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2DiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2DiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..b9053abada --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2DiscriminatorMultitenancySpec.groovy @@ -0,0 +1,12 @@ +package io.micronaut.data.r2dbc.h2 + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class H2DiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements H2TestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: H2AccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mariadb/MariaDbDiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mariadb/MariaDbDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..b093f66fae --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mariadb/MariaDbDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,14 @@ +package io.micronaut.data.r2dbc.mariadb + + +import io.micronaut.data.r2dbc.mysql.MySqlAccountRepository +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class MariaDbDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements MariaDbTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MySqlAccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..b9ba41c3b5 --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.r2dbc.mysql + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class MySqlDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements MySqlTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MySqlAccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..ab49fdf48f --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXEDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.r2dbc.oraclexe + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class OracleXEDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements OracleXETestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: OracleXEAccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..72ecca8b32 --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.r2dbc.postgres + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class PostgresDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements PostgresTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: PostgresAccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..b2df9fdae7 --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/sqlserver/SqlServerDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.r2dbc.sqlserver + + +import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec + +class SqlServerDiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements SqlServerTestPropertyProvider { + + @Override + Map getExtraProperties() { + return [accountRepositoryClass: MSAccountRepository.name] + } + +} diff --git a/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/h2/H2AccountRepository.java b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/h2/H2AccountRepository.java new file mode 100644 index 0000000000..bd9eb25168 --- /dev/null +++ b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/h2/H2AccountRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.h2; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.Account; +import io.micronaut.data.tck.repositories.AccountRepository; + +@R2dbcRepository(dialect = Dialect.H2) +public interface H2AccountRepository extends AccountRepository { +} diff --git a/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/mysql/MySqlAccountRepository.java b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/mysql/MySqlAccountRepository.java new file mode 100644 index 0000000000..650c4c105c --- /dev/null +++ b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/mysql/MySqlAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.mysql; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.tck.repositories.AccountRepository; + +@R2dbcRepository(dialect = Dialect.MYSQL) +public interface MySqlAccountRepository extends AccountRepository { +} diff --git a/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/oraclexe/OracleXEAccountRepository.java b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/oraclexe/OracleXEAccountRepository.java new file mode 100644 index 0000000000..1852288f76 --- /dev/null +++ b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/oraclexe/OracleXEAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.oraclexe; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.tck.repositories.AccountRepository; + +@R2dbcRepository(dialect = Dialect.ORACLE) +public interface OracleXEAccountRepository extends AccountRepository { +} diff --git a/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/postgres/PostgresAccountRepository.java b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/postgres/PostgresAccountRepository.java new file mode 100644 index 0000000000..cd731f5a38 --- /dev/null +++ b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/postgres/PostgresAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.postgres; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.tck.repositories.AccountRepository; + +@R2dbcRepository(dialect = Dialect.POSTGRES) +public interface PostgresAccountRepository extends AccountRepository { +} diff --git a/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/sqlserver/MSAccountRepository.java b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/sqlserver/MSAccountRepository.java new file mode 100644 index 0000000000..96b76cea91 --- /dev/null +++ b/data-r2dbc/src/test/java/io/micronaut/data/r2dbc/sqlserver/MSAccountRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.sqlserver; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.tck.repositories.AccountRepository; + +@R2dbcRepository(dialect = Dialect.SQL_SERVER) +public interface MSAccountRepository extends AccountRepository { +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/event/listeners/TenantIdEntityEventListener.java b/data-runtime/src/main/java/io/micronaut/data/runtime/event/listeners/TenantIdEntityEventListener.java new file mode 100644 index 0000000000..74a7a6111e --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/event/listeners/TenantIdEntityEventListener.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.runtime.event.listeners; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.data.annotation.TenantId; +import io.micronaut.data.annotation.event.PrePersist; +import io.micronaut.data.event.EntityEventContext; +import io.micronaut.data.model.runtime.PropertyAutoPopulator; +import io.micronaut.data.model.runtime.RuntimePersistentProperty; +import io.micronaut.data.runtime.convert.DataConversionService; +import io.micronaut.data.runtime.multitenancy.TenantResolver; +import jakarta.inject.Singleton; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.function.Predicate; + +/** + * An event listener that handles {@link TenantId}. + * + * @author Denis Stepanov + * @since 4.8.0 + */ +@Requires(beans = TenantResolver.class) +@Singleton +public class TenantIdEntityEventListener extends AutoPopulatedEntityEventListener implements PropertyAutoPopulator { + + private final TenantResolver tenantResolver; + private final DataConversionService conversionService; + + /** + * Default constructor. + * + * @param conversionService The conversion service + * @param tenantResolver The tenant resolver + */ + public TenantIdEntityEventListener(TenantResolver tenantResolver, DataConversionService conversionService) { + this.tenantResolver = tenantResolver; + this.conversionService = conversionService; + } + + @NonNull + @Override + protected List> getEventTypes() { + return List.of(PrePersist.class); + } + + @NonNull + @Override + protected Predicate> getPropertyPredicate() { + return (prop) -> { + final AnnotationMetadata annotationMetadata = prop.getAnnotationMetadata(); + return annotationMetadata.hasStereotype(TenantId.class); + }; + } + + @Override + public boolean prePersist(@NonNull EntityEventContext context) { + for (RuntimePersistentProperty property : getApplicableProperties(context.getPersistentEntity())) { + if (property.getAnnotationMetadata().hasStereotype(TenantId.class)) { + Argument argument = property.getArgument(); + Object newValue = tenantResolver.resolveTenantIdentifier(); + if (!argument.isInstance(newValue)) { + newValue = conversionService.convert(newValue, argument.getType()); + } + context.setProperty(property.getProperty(), newValue); + break; + } + } + return true; + } + + @Override + @NonNull + public Object populate(RuntimePersistentProperty property, @Nullable Object previousValue) { + return conversionService.convertRequired(tenantResolver.resolveTenantIdentifier(), property.getArgument()); + } + +} 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 4cca0c7451..e6ae806096 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 @@ -64,7 +64,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractReactiveEntityOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractReactiveEntityOperations.java index 4680377aaa..e185be965f 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractReactiveEntityOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractReactiveEntityOperations.java @@ -16,7 +16,9 @@ package io.micronaut.data.runtime.operations.internal; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.async.propagation.ReactorPropagation; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.propagation.PropagatedContext; import io.micronaut.data.annotation.Relation; import io.micronaut.data.event.EntityEventContext; import io.micronaut.data.event.EntityEventListener; @@ -101,27 +103,35 @@ private void doCascade(boolean isPost, Relation.Cascade cascadeType) { @Override protected boolean triggerPre(Function, Boolean> fn) { - data = data.map(d -> { + data = data.flatMap(d -> { if (d.vetoed) { - return d; + return Mono.just(d); } - final DefaultEntityEventContext event = new DefaultEntityEventContext<>(persistentEntity, d.entity); - d.vetoed = !fn.apply((EntityEventContext) event); - d.entity = event.getEntity(); - return d; + return Mono.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + final DefaultEntityEventContext event = new DefaultEntityEventContext<>(persistentEntity, d.entity); + d.vetoed = !fn.apply((EntityEventContext) event); + d.entity = event.getEntity(); + return Mono.just(d); + } + }); }); return false; } @Override protected void triggerPost(Consumer> fn) { - data = data.map(d -> { + data = data.flatMap(d -> { if (d.vetoed) { - return d; + return Mono.just(d); } - final DefaultEntityEventContext event = new DefaultEntityEventContext<>(persistentEntity, d.entity); - fn.accept((EntityEventContext) event); - return d; + return Mono.deferContextual(contextView -> { + try (PropagatedContext.Scope ignore = ReactorPropagation.findPropagatedContext(contextView).orElse(PropagatedContext.empty()).propagate()) { + final DefaultEntityEventContext event = new DefaultEntityEventContext<>(persistentEntity, d.entity); + fn.accept((EntityEventContext) event); + return Mono.just(d); + } + }); }); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java index d5238a288b..aa7ff20ea8 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java @@ -16,6 +16,7 @@ package io.micronaut.data.runtime.operations.internal.query; import io.micronaut.aop.InvocationContext; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; @@ -172,6 +173,11 @@ protected final void bindParameter(Binder binder, } } } + } else if (value instanceof EvaluatedAnnotationValue evaluatedAnnotationValue) { + value = evaluatedAnnotationValue.withArguments( + invocationContext.getTarget(), + invocationContext.getParameterValues() + ).get(AnnotationMetadata.VALUE_MEMBER, Object.class).orElse(null); } if (persistentProperty != null) { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java index dcecbdf48f..0af1351a73 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java @@ -164,6 +164,10 @@ public DefaultStoredQuery( List queryParameters = new ArrayList<>(params.size()); for (AnnotationValue av : params) { String[] propertyPath = av.stringValues(DataMethodQueryParameter.META_MEMBER_PROPERTY_PATH); + Object value = null; + if (av.getValues().containsKey(AnnotationMetadata.VALUE_MEMBER)) { + value = av; + } if (propertyPath.length == 0) { propertyPath = av.stringValue(DataMethodQueryParameter.META_MEMBER_PROPERTY) .map(property -> new String[]{property}) @@ -188,6 +192,7 @@ public DefaultStoredQuery( av.classValue(DataMethodQueryParameter.META_MEMBER_CONVERTER).orElse(null), av.booleanValue(DataMethodQueryParameter.META_MEMBER_EXPANDABLE).orElse(false), av.booleanValue(DataMethodQueryParameter.META_MEMBER_EXPRESSION).orElse(false), + value, queryParameters )); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/StoredQueryParameter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/StoredQueryParameter.java index 769e61d14f..a8928c2342 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/StoredQueryParameter.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/StoredQueryParameter.java @@ -44,6 +44,7 @@ public final class StoredQueryParameter implements QueryParameterBinding { private final boolean expandable; private final List all; private final boolean expression; + private final Object value; private boolean previousInitialized; private QueryParameterBinding previousPopulatedValueParameter; @@ -59,6 +60,7 @@ public final class StoredQueryParameter implements QueryParameterBinding { Class parameterConverterClass, boolean expandable, final boolean expression, + Object value, List all) { this.name = name; this.dataType = dataType; @@ -71,6 +73,7 @@ public final class StoredQueryParameter implements QueryParameterBinding { this.parameterConverterClass = parameterConverterClass; this.expandable = expandable; this.expression = expression; + this.value = value; this.all = all; } @@ -143,6 +146,11 @@ public boolean isExpression() { return expression; } + @Override + public Object getValue() { + return value; + } + @Override public String toString() { return "StoredQueryParameter{" + @@ -156,6 +164,7 @@ public String toString() { ", previousPopulatedValueParameter=" + previousPopulatedValueParameter + ", expandable=" + expandable + ", expression=" + expression + + ", value=" + value + '}'; } } diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractDiscriminatorMultitenancySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractDiscriminatorMultitenancySpec.groovy new file mode 100644 index 0000000000..80b6692714 --- /dev/null +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractDiscriminatorMultitenancySpec.groovy @@ -0,0 +1,285 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.tests + +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.annotation.Introspected +import io.micronaut.data.connection.ConnectionDefinition +import io.micronaut.data.connection.annotation.Connectable +import io.micronaut.data.tck.entities.Account +import io.micronaut.data.tck.repositories.AccountRepository +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Delete +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Put +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import jakarta.transaction.Transactional +import spock.lang.Specification + +abstract class AbstractDiscriminatorMultitenancySpec extends Specification { + + abstract Map getExtraProperties() + + Map getDataSourceProperties() { + return [:] + } + + def "test discriminator multitenancy"() { + setup: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, getExtraProperties() + getProperties() + [ + 'spec.name' : 'discriminator-multitenancy', + 'micronaut.data.multi-tenancy.mode' : 'DISCRIMINATOR', + 'micronaut.multitenancy.tenantresolver.httpheader.enabled': 'true', + 'datasource.default.schema-generate' : 'create-drop' + ], Environment.TEST) + def context = embeddedServer.applicationContext + FooAccountClient fooAccountClient = context.getBean(FooAccountClient) + BarAccountClient barAccountClient = context.getBean(BarAccountClient) + fooAccountClient.deleteAll() + barAccountClient.deleteAll() + when: 'An account created in FOO tenant' + AccountDto fooAccount = fooAccountClient.save("The Stand") + then: 'The account exists in FOO tenant' + fooAccount.id + when: + fooAccount = fooAccountClient.findOne(fooAccount.getId()).orElse(null) + then: + fooAccount + fooAccount.name == "The Stand" + fooAccount.tenancy == "foo" + and: 'There is one account' + fooAccountClient.findAll().size() == 1 + and: 'There is no accounts in BAR tenant' + barAccountClient.findAll().size() == 0 + + when: "Update the tenancy" + fooAccountClient.updateTenancy(fooAccount.getId(), "bar") + then: + fooAccountClient.findAll().size() == 0 + barAccountClient.findAll().size() == 1 + fooAccountClient.findOne(fooAccount.getId()).isEmpty() + barAccountClient.findOne(fooAccount.getId()).isPresent() + + when: "Update the tenancy" + barAccountClient.updateTenancy(fooAccount.getId(), "foo") + then: + fooAccountClient.findAll().size() == 1 + barAccountClient.findAll().size() == 0 + fooAccountClient.findOne(fooAccount.getId()).isPresent() + barAccountClient.findOne(fooAccount.getId()).isEmpty() + + when: + AccountDto barAccount = barAccountClient.save("The Bar") + def allAccounts = barAccountClient.findAllTenants() + then: + barAccount.tenancy == "bar" + allAccounts.size() == 2 + allAccounts.find { it.id == barAccount.id }.tenancy == "bar" + allAccounts.find { it.id == fooAccount.id }.tenancy == "foo" + allAccounts == fooAccountClient.findAllTenants() + + when: + def barAccounts = barAccountClient.findAllBarTenants() + then: + barAccounts.size() == 1 + barAccounts[0].id == barAccount.id + barAccounts[0].tenancy == "bar" + barAccounts == fooAccountClient.findAllBarTenants() + + when: + def fooAccounts = barAccountClient.findAllFooTenants() + then: + fooAccounts.size() == 1 + fooAccounts[0].id == fooAccount.id + fooAccounts[0].tenancy == "foo" + fooAccounts == fooAccountClient.findAllFooTenants() + + when: + def exp = barAccountClient.findTenantExpression() + then: + exp.size() == 1 + exp[0].tenancy == "bar" + exp == fooAccountClient.findTenantExpression() + + when: 'Delete all BARs' + barAccountClient.deleteAll() + then: "FOOs aren't deletes" + fooAccountClient.findAll().size() == 1 + + when: 'Delete all FOOs' + fooAccountClient.deleteAll() + then: "All FOOs are deleted" + fooAccountClient.findAll().size() == 0 + cleanup: + embeddedServer?.stop() + } + +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy") +@ExecuteOn(TaskExecutors.IO) +@Controller("/accounts") +class AccountController { + + private final AccountRepository accountRepository + + AccountController(ApplicationContext beanContext) { + def className = beanContext.getProperty("accountRepositoryClass", String).get() + this.accountRepository = beanContext.getBean(Class.forName(className)) as AccountRepository + } + + @Post + AccountDto save(String name) { + def newAccount = new Account() + newAccount.name = name + def account = accountRepository.save(newAccount) + return new AccountDto(account) + } + + @Put("/{id}/tenancy") + void updateTenancy(Long id, String tenancy) { + def account = accountRepository.findById(id).orElseThrow() + account.tenancy = tenancy + accountRepository.update(account) + } + + @Get("/{id}") + Optional findOne(Long id) { + return accountRepository.findById(id).map(AccountDto::new) + } + + @Get + List findAll() { + return findAll0() + } + + @Get("/alltenants") + List findAllTenants() { + return accountRepository.findAll$withAllTenants().stream().map(AccountDto::new).toList() + } + + @Get("/foo") + List findAllFooTenants() { + return accountRepository.findAll$withTenantFoo().stream().map(AccountDto::new).toList() + } + + @Get("/bar") + List findAllBarTenants() { + return accountRepository.findAll$withTenantBar().stream().map(AccountDto::new).toList() + } + + @Get("/expression") + List findTenantExpression() { + return accountRepository.findAll$withTenantExpression().stream().map(AccountDto::new).toList() + } + + @Connectable + protected List findAll0() { + findAll1() + } + + @Connectable(propagation = ConnectionDefinition.Propagation.MANDATORY) + protected List findAll1() { + accountRepository.findAll().stream().map(AccountDto::new).toList() + } + + @Delete + void deleteAll() { + deleteAll0() + } + + @Transactional + protected deleteAll0() { + deleteAll1() + } + + @Transactional(Transactional.TxType.MANDATORY) + protected deleteAll1() { + accountRepository.deleteAll() + } + +} + +@Introspected +@EqualsAndHashCode +class AccountDto { + Long id + String name + String tenancy + + AccountDto() { + } + + AccountDto(Account account) { + id = account.id + name = account.name + tenancy = account.tenancy + } + +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy") +@Client("/accounts") +interface AccountClient { + + @Post + AccountDto save(String name); + + @Put("/{id}/tenancy") + void updateTenancy(Long id, String tenancy) + + @Get("/{id}") + Optional findOne(Long id); + + @Get + List findAll(); + + @Get("/alltenants") + List findAllTenants(); + + @Get("/foo") + List findAllFooTenants(); + + @Get("/bar") + List findAllBarTenants(); + + @Get("/expression") + List findTenantExpression(); + + @Delete + void deleteAll(); +} + + +@Requires(property = "spec.name", value = "discriminator-multitenancy") +@Header(name = "tenantId", value = "foo") +@Client("/accounts") +interface FooAccountClient extends AccountClient { +} + +@Requires(property = "spec.name", value = "discriminator-multitenancy") +@Header(name = "tenantId", value = "bar") +@Client("/accounts") +interface BarAccountClient extends AccountClient { +} diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy index c25027aaa3..6c1a1ba57b 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy @@ -60,6 +60,7 @@ import jakarta.persistence.criteria.CriteriaBuilder import jakarta.persistence.criteria.CriteriaUpdate import jakarta.persistence.criteria.Predicate import jakarta.persistence.criteria.Root +import reactor.core.publisher.Mono import spock.lang.AutoCleanup import spock.lang.IgnoreIf import spock.lang.Shared @@ -2670,6 +2671,7 @@ abstract class AbstractRepositorySpec extends Specification { void "entity with id class"() { given: + entityWithIdClassRepository.deleteAll() EntityWithIdClass e = new EntityWithIdClass() e.id1 = 11 e.id2 = 22 @@ -2739,6 +2741,7 @@ abstract class AbstractRepositorySpec extends Specification { void "entity with id class 2"() { given: + entityWithIdClass2Repository.deleteAll() EntityWithIdClass2 e = new EntityWithIdClass2(11, 22, "Xyz") EntityWithIdClass2 f = new EntityWithIdClass2(33, e.id2(), "Xyz") EntityWithIdClass2 g = new EntityWithIdClass2(e.id1(), 44, "Xyz") diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/Account.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/Account.java new file mode 100644 index 0000000000..24334cec68 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/Account.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.entities; + +import io.micronaut.data.annotation.TenantId; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import java.util.HashSet; +import java.util.Set; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + private String name; + @TenantId + private String tenancy; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTenancy() { + return tenancy; + } + + public void setTenancy(String tenancy) { + this.tenancy = tenancy; + } +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/AccountRecord.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/AccountRecord.java new file mode 100644 index 0000000000..6195c6f436 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/AccountRecord.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.entities; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TenantId; + +@MappedEntity +public record AccountRecord(@Id + @GeneratedValue + Long id, + String name, + @TenantId + String tenancy) { + +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRecordRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRecordRepository.java new file mode 100644 index 0000000000..a127239fab --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRecordRepository.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.repositories; + +import io.micronaut.data.annotation.WithTenantId; +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.Account; +import io.micronaut.data.tck.entities.AccountRecord; + +import java.util.List; + +public interface AccountRecordRepository extends CrudRepository { + + @WithoutTenantId + List findAll$withAllTenants(); + + @WithTenantId("bar") + List findAll$withTenantBar(); + + @WithTenantId("foo") + List findAll$withTenantFoo(); + + @WithTenantId("#{this.barTenant()}") + List findAll$withTenantExpression(); + + default String barTenant() { + return "bar"; + } +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRepository.java new file mode 100644 index 0000000000..efa2907b6a --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AccountRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.repositories; + +import io.micronaut.data.annotation.WithTenantId; +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.Account; + +import java.util.List; + +public interface AccountRepository extends CrudRepository { + + @WithoutTenantId + List findAll$withAllTenants(); + + @WithTenantId("bar") + List findAll$withTenantBar(); + + @WithTenantId("foo") + List findAll$withTenantFoo(); + + @WithTenantId("#{this.barTenant()}") + List findAll$withTenantExpression(); + + default String barTenant() { + return "bar"; + } +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/build.gradle b/doc-examples/hibernate-multitenancy-discriminator-example-java/build.gradle new file mode 100644 index 0000000000..487a4d4046 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/build.gradle @@ -0,0 +1,31 @@ +plugins { + id "io.micronaut.build.internal.data-native-example" +} + +application { + mainClass = "example.Application" +} + +micronaut { + version libs.versions.micronaut.platform.get() + runtime "netty" + testRuntime "junit5" + testResources { + clientTimeout = 300 + version = libs.versions.micronaut.testresources.get() + } +} + +dependencies { + annotationProcessor projects.micronautDataProcessor + + implementation mnMultitenancy.micronaut.multitenancy + implementation mn.micronaut.http.client + implementation mnSql.micronaut.hibernate.jpa + implementation projects.micronautDataHibernateJpa + implementation mnSerde.micronaut.serde.jackson + + runtimeOnly mnSql.micronaut.jdbc.tomcat + runtimeOnly mnSql.h2 + runtimeOnly mnLogging.logback.classic +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/gradle.properties b/doc-examples/hibernate-multitenancy-discriminator-example-java/gradle.properties new file mode 100644 index 0000000000..2f7c48213f --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/gradle.properties @@ -0,0 +1 @@ +skipDocumentation=true diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Application.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Application.java new file mode 100644 index 0000000000..fc99e7c05b --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Application.java @@ -0,0 +1,11 @@ + +package example; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Book.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Book.java new file mode 100644 index 0000000000..a6d29a09cb --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/Book.java @@ -0,0 +1,58 @@ + +package example; + +import io.micronaut.data.annotation.TenantId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Book { + @Id + @GeneratedValue + private Long id; + private String title; + private int pages; + @TenantId + private String tenant; + + public Book() { + } + + public Book(String title, int pages) { + this.title = title; + this.pages = pages; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookController.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookController.java new file mode 100644 index 0000000000..37b544c3f3 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookController.java @@ -0,0 +1,49 @@ +package example; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@ExecuteOn(TaskExecutors.IO) +@Controller("/books") +public class BookController { + + private final BookRepository bookRepository; + + public BookController(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @Post + BookDto save(String title, int pages) { + return new BookDto(bookRepository.save(new Book(title, pages))); + } + + @Get("/{id}") + Optional findOne(Long id) { + return bookRepository.findById(id).map(BookDto::new); + } + + @Get + List findAll() { + return bookRepository.findAll().stream().map(BookDto::new).toList(); + } + + @Get("/withoutTenancy") + List findAllWithoutTenancy() { + return bookRepository.findAll$WithoutTenancy().stream().map(BookDto::new).toList(); + } + + @Delete + void deleteAll() { + bookRepository.deleteAll(); + } + +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java new file mode 100644 index 0000000000..4e93f43bf6 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java @@ -0,0 +1,35 @@ + +package example; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class BookDto { + private final String id; + private final String title; + private final int pages; + + public BookDto(Book book) { + this(book.getId().toString(), book.getTitle(), book.getPages()); + } + + @Creator + public BookDto(String id, String title, int pages) { + this.id = id; + this.title = title; + this.pages = pages; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java new file mode 100644 index 0000000000..22bcd8ca69 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java @@ -0,0 +1,16 @@ + +package example; + +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.repository.CrudRepository; + +import java.util.List; + +@Repository +interface BookRepository extends CrudRepository { + + @WithoutTenantId + List findAll$WithoutTenancy(); + +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/application.yml b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/application.yml new file mode 100644 index 0000000000..101dd93d78 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/application.yml @@ -0,0 +1,23 @@ +micronaut: + data: + multi-tenancy: + mode: DISCRIMINATOR + multitenancy: + tenantresolver: + httpheader: + enabled: true + +datasources: + default: + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=USER + driverClassName: org.h2.Driver + username: sa + password: '' + +jpa: + default: + properties: + hibernate: + hbm2ddl: + auto: update + compileTimeHibernateProxies: true diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/logback.xml b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/logback.xml new file mode 100644 index 0000000000..cad3f56572 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + + + diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java new file mode 100644 index 0000000000..bc802ce3d7 --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java @@ -0,0 +1,28 @@ +package example; + +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.annotation.Client; + +import java.util.List; +import java.util.Optional; + +@Client("/books") +public interface BookClient { + + @Post + BookDto save(String title, int pages); + + @Get("/{id}") + Optional findOne(String id); + + @Get + List findAll(); + + @Get("/withoutTenancy") + List findAllWithoutTenancy(); + + @Delete + void deleteAll(); +} diff --git a/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookHibernateDiscriminatorMultiTenancySpec.java b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookHibernateDiscriminatorMultiTenancySpec.java new file mode 100644 index 0000000000..7f2ffced0c --- /dev/null +++ b/doc-examples/hibernate-multitenancy-discriminator-example-java/src/test/java/example/BookHibernateDiscriminatorMultiTenancySpec.java @@ -0,0 +1,77 @@ +package example; + +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@MicronautTest(transactional = false) +class BookHibernateDiscriminatorMultiTenancySpec { + + @Inject + FooBookClient fooBookClient; + + @Inject + BarBookClient barBookClient; + + @AfterEach + public void cleanup() { + fooBookClient.deleteAll(); + barBookClient.deleteAll(); + } + + @Test + void testRest() throws SQLException { + // When: A book created in FOO tenant + BookDto book = fooBookClient.save("The Stand", 1000); + assertNotNull(book.getId()); + // Then: The book exists in FOO tenant + book = fooBookClient.findOne(book.getId()).orElse(null); + assertNotNull(book); + assertEquals("The Stand", book.getTitle()); + // And: There is one book + assertEquals(1, fooBookClient.findAll().size()); + assertTrue(fooBookClient.findAll().iterator().hasNext()); + // And: There is no books in BAR tenant + assertEquals(0, barBookClient.findAll().size()); + // No tenancy repository methods returns all books + assertEquals(1, barBookClient.findAllWithoutTenancy().size()); + assertEquals(1, fooBookClient.findAllWithoutTenancy().size()); + + // When: Delete all BARs + barBookClient.deleteAll(); + // Then: FOOs aren't deletes + assertEquals(1, fooBookClient.findAll().size()); + + // When: Delete all FOOs + fooBookClient.deleteAll(); + // Then: BARs aren deletes + assertEquals(0, fooBookClient.findAll().size()); + } + +} + +// tag::clients[] + +@Header(name = "tenantId", value = "foo") +@Client("/books") +interface FooBookClient extends BookClient { +} + +@Header(name = "tenantId", value = "bar") +@Client("/books") +interface BarBookClient extends BookClient { +} + +// end::clients[] + diff --git a/doc-examples/hibernate-reactive-example-kotlin/src/main/kotlin/example/AccountRepository.kt b/doc-examples/hibernate-reactive-example-kotlin/src/main/kotlin/example/AccountRepository.kt index 0b23f49470..e2e410a244 100644 --- a/doc-examples/hibernate-reactive-example-kotlin/src/main/kotlin/example/AccountRepository.kt +++ b/doc-examples/hibernate-reactive-example-kotlin/src/main/kotlin/example/AccountRepository.kt @@ -1,7 +1,6 @@ package example import io.micronaut.data.annotation.Repository -import io.micronaut.data.repository.CrudRepository import io.micronaut.data.repository.kotlin.CoroutineCrudRepository @Repository diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/build.gradle b/doc-examples/jdbc-multitenancy-discriminator-example-java/build.gradle new file mode 100644 index 0000000000..a2793553e6 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/build.gradle @@ -0,0 +1,33 @@ +plugins { + id "io.micronaut.build.internal.data-native-example" +} + +application { + mainClass = "example.Application" +} + +micronaut { + version libs.versions.micronaut.platform.get() + runtime "netty" + testRuntime "junit5" + testResources { + clientTimeout = 300 + version = libs.versions.micronaut.testresources.get() + } +} + +dependencies { + annotationProcessor projects.micronautDataDocumentProcessor + + implementation mnMultitenancy.micronaut.multitenancy + implementation mnReactor.micronaut.reactor + implementation mn.micronaut.http.client + implementation projects.micronautDataJdbc + implementation mnSerde.micronaut.serde.jackson + implementation(libs.managed.jakarta.persistence.api) + implementation(libs.managed.jakarta.transaction.api) + + runtimeOnly mnSql.micronaut.jdbc.tomcat + runtimeOnly mnSql.h2 + runtimeOnly mnLogging.logback.classic +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/gradle.properties b/doc-examples/jdbc-multitenancy-discriminator-example-java/gradle.properties new file mode 100644 index 0000000000..2f7c48213f --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/gradle.properties @@ -0,0 +1 @@ +skipDocumentation=true diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java new file mode 100644 index 0000000000..fc99e7c05b --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java @@ -0,0 +1,11 @@ + +package example; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java new file mode 100644 index 0000000000..66d4d42ef3 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java @@ -0,0 +1,61 @@ + +package example; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TenantId; + +// tag::book[] +@MappedEntity +public class Book { + @Id + @GeneratedValue + private Long id; + private String title; + private int pages; + @TenantId + private String tenant; +// end::book[] + + public Book(String title, int pages) { + this.title = title; + this.pages = pages; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + +// tag::book[] +// ... +} +// end::book[] diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java new file mode 100644 index 0000000000..37b544c3f3 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java @@ -0,0 +1,49 @@ +package example; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@ExecuteOn(TaskExecutors.IO) +@Controller("/books") +public class BookController { + + private final BookRepository bookRepository; + + public BookController(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @Post + BookDto save(String title, int pages) { + return new BookDto(bookRepository.save(new Book(title, pages))); + } + + @Get("/{id}") + Optional findOne(Long id) { + return bookRepository.findById(id).map(BookDto::new); + } + + @Get + List findAll() { + return bookRepository.findAll().stream().map(BookDto::new).toList(); + } + + @Get("/withoutTenancy") + List findAllWithoutTenancy() { + return bookRepository.findAll$WithoutTenancy().stream().map(BookDto::new).toList(); + } + + @Delete + void deleteAll() { + bookRepository.deleteAll(); + } + +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java new file mode 100644 index 0000000000..4e93f43bf6 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java @@ -0,0 +1,35 @@ + +package example; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class BookDto { + private final String id; + private final String title; + private final int pages; + + public BookDto(Book book) { + this(book.getId().toString(), book.getTitle(), book.getPages()); + } + + @Creator + public BookDto(String id, String title, int pages) { + this.id = id; + this.title = title; + this.pages = pages; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java new file mode 100644 index 0000000000..af0017f0f2 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java @@ -0,0 +1,17 @@ + +package example; + +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +import java.util.List; + +@JdbcRepository(dialect = Dialect.H2) +interface BookRepository extends CrudRepository { + + @WithoutTenantId + List findAll$WithoutTenancy(); + +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/application.yml b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/application.yml new file mode 100644 index 0000000000..5144adb1ae --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/application.yml @@ -0,0 +1,17 @@ +micronaut: + data: + multi-tenancy: + mode: DISCRIMINATOR + multitenancy: + tenantresolver: + httpheader: + enabled: true + +datasources: + default: + url: jdbc:h2:mem:db + driverClassName: org.h2.Driver + username: sa + password: '' + dialect: H2 + schema-generate: CREATE_DROP diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml new file mode 100644 index 0000000000..cad3f56572 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + + + diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java new file mode 100644 index 0000000000..bc802ce3d7 --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java @@ -0,0 +1,28 @@ +package example; + +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.annotation.Client; + +import java.util.List; +import java.util.Optional; + +@Client("/books") +public interface BookClient { + + @Post + BookDto save(String title, int pages); + + @Get("/{id}") + Optional findOne(String id); + + @Get + List findAll(); + + @Get("/withoutTenancy") + List findAllWithoutTenancy(); + + @Delete + void deleteAll(); +} diff --git a/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookJdbcDiscriminatorMultiTenancySpec.java b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookJdbcDiscriminatorMultiTenancySpec.java new file mode 100644 index 0000000000..c2648efa8d --- /dev/null +++ b/doc-examples/jdbc-multitenancy-discriminator-example-java/src/test/java/example/BookJdbcDiscriminatorMultiTenancySpec.java @@ -0,0 +1,79 @@ +package example; + +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@MicronautTest(transactional = false) +class BookJdbcDiscriminatorMultiTenancySpec { + + @Inject + FooBookClient fooBookClient; + + @Inject + BarBookClient barBookClient; + + @AfterEach + public void cleanup() { + fooBookClient.deleteAll(); + barBookClient.deleteAll(); + } + + @Test + void testRest(BookRepository bookRepository) throws SQLException { + // When: A book created in FOO tenant + Book b = new Book("The Stand", 1000); + b.setTenant("foo"); + BookDto book = new BookDto(bookRepository.save(b)); + assertNotNull(book.getId()); + // Then: The book exists in FOO tenant + book = fooBookClient.findOne(book.getId()).orElse(null); + assertNotNull(book); + assertEquals("The Stand", book.getTitle()); + // And: There is one book + assertEquals(1, fooBookClient.findAll().size()); + assertTrue(fooBookClient.findAll().iterator().hasNext()); + // And: There is no books in BAR tenant + assertEquals(0, barBookClient.findAll().size()); + // No tenancy repository methods returns all books + assertEquals(1, barBookClient.findAllWithoutTenancy().size()); + assertEquals(1, fooBookClient.findAllWithoutTenancy().size()); + + // When: Delete all BARs + barBookClient.deleteAll(); + // Then: FOOs aren't deletes + assertEquals(1, fooBookClient.findAll().size()); + + // When: Delete all FOOs + fooBookClient.deleteAll(); + // Then: BARs aren deletes + assertEquals(0, fooBookClient.findAll().size()); + } + +} + +// tag::clients[] + +@Header(name = "tenantId", value = "foo") +@Client("/books") +interface FooBookClient extends BookClient { +} + +@Header(name = "tenantId", value = "bar") +@Client("/books") +interface BarBookClient extends BookClient { +} + +// end::clients[] + diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/build.gradle b/doc-examples/mongo-multitenancy-discriminator-example-java/build.gradle new file mode 100644 index 0000000000..168b03f27f --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/build.gradle @@ -0,0 +1,40 @@ +import io.micronaut.testresources.buildtools.KnownModules + +plugins { + id "io.micronaut.build.internal.data-native-example" +} + +application { + mainClass = "example.Application" +} + +micronaut { + version libs.versions.micronaut.platform.get() + runtime "netty" + testRuntime "junit5" + testResources { + enabled = true + inferClasspath = false + additionalModules.add(KnownModules.MONGODB) + clientTimeout = 300 + version = libs.versions.micronaut.testresources.get() + } +} + +dependencies { + implementation mnMultitenancy.micronaut.multitenancy + annotationProcessor projects.micronautDataDocumentProcessor + + implementation mnRxjava2.micronaut.rxjava2 + implementation mnReactor.micronaut.reactor + implementation mn.micronaut.http.client + implementation projects.micronautDataMongodb + implementation mnSerde.micronaut.serde.jackson + + implementation mnMongo.mongo.driver + + implementation(libs.managed.jakarta.persistence.api) + implementation(libs.managed.jakarta.transaction.api) + + runtimeOnly mnLogging.logback.classic +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/gradle.properties b/doc-examples/mongo-multitenancy-discriminator-example-java/gradle.properties new file mode 100644 index 0000000000..2f7c48213f --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/gradle.properties @@ -0,0 +1 @@ +skipDocumentation=true diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Application.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Application.java new file mode 100644 index 0000000000..fc99e7c05b --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Application.java @@ -0,0 +1,11 @@ + +package example; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Book.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Book.java new file mode 100644 index 0000000000..8b74d644db --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/Book.java @@ -0,0 +1,48 @@ + +package example; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TenantId; +import org.bson.types.ObjectId; + +@MappedEntity +public class Book { + @Id + @GeneratedValue + private ObjectId id; + private String title; + private int pages; + @TenantId + private String tenancyId; + + public Book(String title, int pages) { + this.title = title; + this.pages = pages; + } + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } + + public String getTenancyId() { + return tenancyId; + } + + public void setTenancyId(String tenancyId) { + this.tenancyId = tenancyId; + } +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookController.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookController.java new file mode 100644 index 0000000000..ad5f42254e --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookController.java @@ -0,0 +1,50 @@ +package example; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import org.bson.types.ObjectId; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@ExecuteOn(TaskExecutors.IO) +@Controller("/books") +public class BookController { + + private final BookRepository bookRepository; + + public BookController(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @Post + BookDto save(String title, int pages) { + return new BookDto(bookRepository.save(new Book(title, pages))); + } + + @Get("/{id}") + Optional findOne(String id) { + return bookRepository.findById(new ObjectId(id)).map(BookDto::new); + } + + @Get + List findAll() { + return bookRepository.findAll().stream().map(BookDto::new).toList(); + } + + @Get("/all") + List findAllNoTenancy() { + return bookRepository.findAll$NoTenancy().stream().map(BookDto::new).toList(); + } + + @Delete + void deleteAll() { + bookRepository.deleteAll(); + } + +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java new file mode 100644 index 0000000000..b96ba54de1 --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java @@ -0,0 +1,35 @@ + +package example; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class BookDto { + private final String id; + private final String title; + private final int pages; + + public BookDto(Book book) { + this(book.getId().toHexString(), book.getTitle(), book.getPages()); + } + + @Creator + public BookDto(String id, String title, int pages) { + this.id = id; + this.title = title; + this.pages = pages; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java new file mode 100644 index 0000000000..dbd83b87ab --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java @@ -0,0 +1,17 @@ + +package example; + +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.mongodb.annotation.MongoRepository; +import io.micronaut.data.repository.CrudRepository; +import org.bson.types.ObjectId; + +import java.util.List; + +@MongoRepository +interface BookRepository extends CrudRepository { + + @WithoutTenantId + List findAll$NoTenancy(); + +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/application.yml b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/application.yml new file mode 100644 index 0000000000..80468c3dab --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/application.yml @@ -0,0 +1,8 @@ +micronaut: + data: + multi-tenancy: + mode: DISCRIMINATOR + multitenancy: + tenantresolver: + httpheader: + enabled: true diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/logback.xml b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/logback.xml new file mode 100644 index 0000000000..9eabe72238 --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + + + + diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java new file mode 100644 index 0000000000..7acee7af1e --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java @@ -0,0 +1,28 @@ +package example; + +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.annotation.Client; + +import java.util.List; +import java.util.Optional; + +@Client("/books") +public interface BookClient { + + @Post + BookDto save(String title, int pages); + + @Get("/{id}") + Optional findOne(String id); + + @Get + List findAll(); + + @Get("/all") + List findAllNoTenancy(); + + @Delete + void deleteAll(); +} diff --git a/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookDiscriminatorMultitenancySpec.java b/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookDiscriminatorMultitenancySpec.java new file mode 100644 index 0000000000..8e28213ea9 --- /dev/null +++ b/doc-examples/mongo-multitenancy-discriminator-example-java/src/test/java/example/BookDiscriminatorMultitenancySpec.java @@ -0,0 +1,69 @@ +package example; + +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@MicronautTest +class BookDiscriminatorMultitenancySpec { + + @Inject + FooBookClient fooBookClient; + + @Inject + BarBookClient barBookClient; + + @AfterEach + public void cleanup() { + fooBookClient.deleteAll(); + barBookClient.deleteAll(); + } + + @Test + void testRest() { + // When: A book created in FOO tenant + BookDto book = fooBookClient.save("The Stand", 1000); + assertNotNull(book.getId()); + // Then: The book exists in FOO tenant + book = fooBookClient.findOne(book.getId()).orElse(null); + assertNotNull(book); + assertEquals("The Stand", book.getTitle()); + // And: There is one book + assertEquals(1, fooBookClient.findAll().size()); + assertTrue(fooBookClient.findAllNoTenancy().iterator().hasNext()); + // And: There is no books in BAR tenant + assertEquals(0, barBookClient.findAll().size()); + + assertEquals(1, barBookClient.findAllNoTenancy().size()); + assertEquals(1, fooBookClient.findAllNoTenancy().size()); + + // When: Delete all BARs + barBookClient.deleteAll(); + // Then: FOOs aren't deletes + assertEquals(1, fooBookClient.findAll().size()); + + // When: Delete all FOOs + fooBookClient.deleteAll(); + // Then: BARs aren deletes + assertEquals(0, fooBookClient.findAll().size()); + } +} + +@Header(name = "tenantId", value = "foo") +@Client("/books") +interface FooBookClient extends BookClient { +} + +@Header(name = "tenantId", value = "bar") +@Client("/books") +interface BarBookClient extends BookClient { +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/build.gradle b/doc-examples/r2dbc-multitenancy-discriminator-example-java/build.gradle new file mode 100644 index 0000000000..af8acc8192 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/build.gradle @@ -0,0 +1,38 @@ +import io.micronaut.testresources.buildtools.KnownModules + +plugins { + id "io.micronaut.build.internal.data-native-example" +} + +application { + mainClass = "example.Application" +} + +micronaut { + version libs.versions.micronaut.platform.get() + runtime "netty" + testRuntime "junit5" + testResources { + additionalModules.add(KnownModules.R2DBC_MARIADB) + clientTimeout = 300 + version = libs.versions.micronaut.testresources.get() + } +} + +dependencies { + annotationProcessor projects.micronautDataDocumentProcessor + + implementation mnMultitenancy.micronaut.multitenancy + implementation mnReactor.micronaut.reactor + implementation mn.micronaut.http.client + implementation projects.micronautDataR2dbc + implementation mnSerde.micronaut.serde.jackson + + implementation(libs.managed.jakarta.persistence.api) + implementation(libs.managed.jakarta.transaction.api) + + runtimeOnly mnR2dbc.r2dbc.mariadb + runtimeOnly mnLogging.logback.classic + + testResourcesService mnSql.mariadb.java.client +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/gradle.properties b/doc-examples/r2dbc-multitenancy-discriminator-example-java/gradle.properties new file mode 100644 index 0000000000..2f7c48213f --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/gradle.properties @@ -0,0 +1 @@ +skipDocumentation=true diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java new file mode 100644 index 0000000000..fc99e7c05b --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Application.java @@ -0,0 +1,11 @@ + +package example; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java new file mode 100644 index 0000000000..eeb52c1c41 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java @@ -0,0 +1,55 @@ + +package example; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TenantId; + +@MappedEntity +public class Book { + @Id + @GeneratedValue + private Long id; + private String title; + private int pages; + @TenantId + private String tenancy; + + public Book(String title, int pages) { + this.title = title; + this.pages = pages; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public String getTenancy() { + return tenancy; + } + + public void setTenancy(String tenancy) { + this.tenancy = tenancy; + } +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java new file mode 100644 index 0000000000..8e63f8c962 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookController.java @@ -0,0 +1,49 @@ +package example; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@ExecuteOn(TaskExecutors.IO) +@Controller("/books") +public class BookController { + + private final BookRepository bookRepository; + + public BookController(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @Post + Mono save(String title, int pages) { + return bookRepository.save(new Book(title, pages)).map(BookDto::new); + } + + @Get("/{id}") + Mono findOne(Long id) { + return bookRepository.findById(id).map(BookDto::new); + } + + @Get + Flux findAll() { + return bookRepository.findAll().map(BookDto::new); + } + + @Get("/withoutTenancy") + Flux findAllWithoutTenancy() { + return bookRepository.findAll$NoTenancy().map(BookDto::new); + } + + @Delete + Mono deleteAll() { + return bookRepository.deleteAll().then(); + } + +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java new file mode 100644 index 0000000000..4e93f43bf6 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookDto.java @@ -0,0 +1,35 @@ + +package example; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class BookDto { + private final String id; + private final String title; + private final int pages; + + public BookDto(Book book) { + this(book.getId().toString(), book.getTitle(), book.getPages()); + } + + @Creator + public BookDto(String id, String title, int pages) { + this.id = id; + this.title = title; + this.pages = pages; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public int getPages() { + return pages; + } +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java new file mode 100644 index 0000000000..40eace8d94 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/java/example/BookRepository.java @@ -0,0 +1,16 @@ + +package example; + +import io.micronaut.data.annotation.WithoutTenantId; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.reactive.ReactorCrudRepository; +import reactor.core.publisher.Flux; + +@R2dbcRepository(dialect = Dialect.MYSQL) +interface BookRepository extends ReactorCrudRepository { + + @WithoutTenantId + Flux findAll$NoTenancy(); + +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/application.yml b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/application.yml new file mode 100644 index 0000000000..56e2fdeefc --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/application.yml @@ -0,0 +1,15 @@ +micronaut: + data: + multi-tenancy: + mode: DISCRIMINATOR + multitenancy: + tenantresolver: + httpheader: + enabled: true + +r2dbc: + datasources: + default: + db-type: mariadb + schema-generate: CREATE_DROP + dialect: MYSQL diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml new file mode 100644 index 0000000000..9eabe72238 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + + + + diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java new file mode 100644 index 0000000000..1d761cf733 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookClient.java @@ -0,0 +1,29 @@ +package example; + +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.annotation.Client; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Optional; + +@Client("/books") +public interface BookClient { + + @Post + BookDto save(String title, int pages); + + @Get("/{id}") + Optional findOne(String id); + + @Get + List findAll(); + + @Get("/withoutTenancy") + List findAllWithoutTenancy(); + + @Delete + void deleteAll(); +} diff --git a/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookR2dbcDiscriminatorMultiTenancySpec.java b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookR2dbcDiscriminatorMultiTenancySpec.java new file mode 100644 index 0000000000..ed50f4adc6 --- /dev/null +++ b/doc-examples/r2dbc-multitenancy-discriminator-example-java/src/test/java/example/BookR2dbcDiscriminatorMultiTenancySpec.java @@ -0,0 +1,71 @@ +package example; + +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@MicronautTest(transactional = false) +class BookR2dbcDiscriminatorMultiTenancySpec { + + @Inject + FooBookClient fooBookClient; + + @Inject + BarBookClient barBookClient; + + @AfterEach + public void cleanup() { + fooBookClient.deleteAll(); + barBookClient.deleteAll(); + } + + @Test + void testRest() { + // When: A book created in FOO tenant + BookDto book = fooBookClient.save("The Stand", 1000); + assertNotNull(book.getId()); + // Then: The book exists in FOO tenant + book = fooBookClient.findOne(book.getId()).orElse(null); + assertNotNull(book); + assertEquals("The Stand", book.getTitle()); + // And: There is one book + assertEquals(1, fooBookClient.findAll().size()); + assertTrue(fooBookClient.findAll().iterator().hasNext()); + // And: There is no books in BAR tenant + assertEquals(0, barBookClient.findAll().size()); + + // Find all without tenancy returns all books + assertEquals(1, barBookClient.findAllWithoutTenancy().size()); + assertEquals(1, fooBookClient.findAllWithoutTenancy().size()); + + // When: Delete all BARs + barBookClient.deleteAll(); + // Then: FOOs aren't deletes + assertEquals(1, fooBookClient.findAll().size()); + + // When: Delete all FOOs + fooBookClient.deleteAll(); + // Then: BARs aren deletes + assertEquals(0, fooBookClient.findAll().size()); + } + +} + +@Header(name = "tenantId", value = "foo") +@Client("/books") +interface FooBookClient extends BookClient { +} + +@Header(name = "tenantId", value = "bar") +@Client("/books") +interface BarBookClient extends BookClient { +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7ba09eeba..f3ef83f959 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.4.1" +micronaut = "4.4.2" micronaut-platform = "4.3.7" micronaut-docs = "2.0.0" micronaut-gradle-plugin = "4.3.6" diff --git a/settings.gradle b/settings.gradle index 0978842f1a..6e18c48bbf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -85,6 +85,8 @@ include 'doc-examples:hibernate-reactive-example-kotlin' include 'doc-examples:hibernate-reactive-example-groovy' include 'doc-examples:hibernate-sync-and-reactive-example-java' +include 'doc-examples:hibernate-multitenancy-discriminator-example-java' + include 'doc-examples:example-hibernate-and-jdbc' include 'doc-examples:jdbc-example-java' @@ -94,14 +96,17 @@ include 'doc-examples:jdbc-example-records-java' include 'doc-examples:jdbc-multitenancy-datasource-example-java' include 'doc-examples:jdbc-multitenancy-schema-example-java' +include 'doc-examples:jdbc-multitenancy-discriminator-example-java' include 'doc-examples:jdbc-and-r2dbc-example-java' include 'doc-examples:r2dbc-example-java' include 'doc-examples:r2dbc-example-groovy' include 'doc-examples:r2dbc-example-kotlin' + include 'doc-examples:r2dbc-multitenancy-datasource-example-java' include 'doc-examples:r2dbc-multitenancy-schema-example-java' +include 'doc-examples:r2dbc-multitenancy-discriminator-example-java' include 'doc-examples:mongo-example-java' include 'doc-examples:mongo-example-groovy' @@ -113,6 +118,7 @@ include 'doc-examples:mongo-jackson-example-java' include 'doc-examples:mongo-multitenancy-database-example-java' include 'doc-examples:mongo-multitenancy-datasource-example-java' +include 'doc-examples:mongo-multitenancy-discriminator-example-java' include 'doc-examples:azure-cosmos-example-java' include 'doc-examples:azure-cosmos-example-groovy' diff --git a/src/main/docs/guide/multitenancy.adoc b/src/main/docs/guide/multitenancy.adoc index 780404b5f7..42b8cb114c 100644 --- a/src/main/docs/guide/multitenancy.adoc +++ b/src/main/docs/guide/multitenancy.adoc @@ -9,6 +9,7 @@ Micronaut Data supports multi-tenancy to allow the use of multiple databases or The DATASOURCE mode is used in combination with the micronaut-multitenancy library in order to resolve the tenant name. In the below example, the tenant resolver is set to use a http header. See https://micronaut-projects.github.io/micronaut-multitenancy/latest/guide/[Micronaut Multitenancy] for more information. +.Example of the configuration with two data sources to be chosen based on the tenancy [configuration] ---- include::doc-examples/jdbc-multitenancy-datasource-example-java/src/main/resources/application.yml[] @@ -24,9 +25,43 @@ include::doc-examples/jdbc-multitenancy-schema-example-java/src/test/java/exampl === Schema Mode The SCHEMA mode uses a single datasource and set the active schema based on the tenant resolved. +.Example of the configuration with one datasource that will be used to switch schema based on the tenancy [configuration] ---- include::doc-examples/jdbc-multitenancy-schema-example-java/src/main/resources/application.yml[] ---- NOTE: You can use property `schema-generate-names` to specify multiple schemas to be created and initialized for testing. + +=== Discriminator Mode +The DISCRIMINATOR mode uses a single entity's property to store the tenant id. + +.Example of the configuration with one data source +[configuration] +---- +include::doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/resources/application.yml[] +---- + +The entity with multitenancy enabled requires a tenant property to be annotated with api:data.annotation.TenantId[]: + +[source,java] +---- +include::doc-examples/jdbc-multitenancy-discriminator-example-java/src/main/java/example/Book.java[tags="book"] +---- + +There are specific annotations to alter the behaviour of repositories with api:data.annotation.TenantId[] property and its methods: + +[cols=2*] +|=== +|*Annotation* +|*Description* + +|api:data.annotation.WithoutTenantId[] +|The method's query will not have implicit predicate to include the tenant id + +|api:data.annotation.WithTenantId[] +|Modify the tenant id of the query + +|=== + +NOTE: The tenancy annotations are only supported for the discriminator multitenancy