Skip to content

Commit a8d0d97

Browse files
authored
Support discriminator multitenancy (#2876)
1 parent 1f4591d commit a8d0d97

File tree

119 files changed

+3760
-434
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+3760
-434
lines changed

data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import io.micronaut.data.model.runtime.UpdateOperation;
4343
import io.micronaut.data.operations.reactive.ReactorCriteriaRepositoryOperations;
4444
import io.micronaut.data.runtime.convert.DataConversionService;
45-
import io.micronaut.data.runtime.operations.internal.query.BindableParametersPreparedQuery;
4645
import io.micronaut.transaction.reactive.ReactorReactiveTransactionOperations;
4746
import jakarta.persistence.EntityGraph;
4847
import jakarta.persistence.FlushModeType;

data-jdbc/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies {
3636
testImplementation libs.groovy.sql
3737
testImplementation mnValidation.micronaut.validation
3838
testImplementation mnValidation.micronaut.validation.processor
39+
testImplementation mn.micronaut.http.client
3940

4041
testImplementation(mnTestResources.testcontainers.mysql)
4142
testImplementation(mnTestResources.testcontainers.mariadb)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/*
2+
* Copyright 2017-2020 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.data.jdbc.h2
17+
18+
import groovy.transform.EqualsAndHashCode
19+
import io.micronaut.context.ApplicationContext
20+
import io.micronaut.context.annotation.Requires
21+
import io.micronaut.context.env.Environment
22+
import io.micronaut.core.annotation.Introspected
23+
import io.micronaut.data.connection.ConnectionDefinition
24+
import io.micronaut.data.connection.annotation.Connectable
25+
import io.micronaut.data.tck.entities.AccountRecord
26+
import io.micronaut.data.tck.repositories.AccountRecordRepository
27+
import io.micronaut.http.annotation.Controller
28+
import io.micronaut.http.annotation.Delete
29+
import io.micronaut.http.annotation.Get
30+
import io.micronaut.http.annotation.Header
31+
import io.micronaut.http.annotation.Post
32+
import io.micronaut.http.annotation.Put
33+
import io.micronaut.http.client.annotation.Client
34+
import io.micronaut.runtime.server.EmbeddedServer
35+
import io.micronaut.scheduling.TaskExecutors
36+
import io.micronaut.scheduling.annotation.ExecuteOn
37+
import jakarta.transaction.Transactional
38+
import spock.lang.Specification
39+
40+
class H2DiscriminatorMultitenancyRecordSpec extends Specification implements H2TestPropertyProvider {
41+
42+
Map<String, String> getExtraProperties() {
43+
return [accountRepositoryClass: H2AccountRecordRepository.name]
44+
}
45+
46+
def "test discriminator multitenancy"() {
47+
setup:
48+
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, getExtraProperties() + getProperties() + [
49+
'spec.name' : 'discriminator-multitenancy-record',
50+
'micronaut.data.multi-tenancy.mode' : 'DISCRIMINATOR',
51+
'micronaut.multitenancy.tenantresolver.httpheader.enabled': 'true',
52+
'datasource.default.schema-generate' : 'create-drop'
53+
], Environment.TEST)
54+
def context = embeddedServer.applicationContext
55+
FooAccountClient fooAccountClient = context.getBean(FooAccountClient)
56+
BarAccountClient barAccountClient = context.getBean(BarAccountClient)
57+
fooAccountClient.deleteAll()
58+
barAccountClient.deleteAll()
59+
when: 'An account created in FOO tenant'
60+
AccountDto fooAccount = fooAccountClient.save("The Stand")
61+
then: 'The account exists in FOO tenant'
62+
fooAccount.id
63+
when:
64+
fooAccount = fooAccountClient.findOne(fooAccount.getId()).orElse(null)
65+
then:
66+
fooAccount
67+
fooAccount.name == "The Stand"
68+
fooAccount.tenancy == "foo"
69+
and: 'There is one account'
70+
fooAccountClient.findAll().size() == 1
71+
and: 'There is no accounts in BAR tenant'
72+
barAccountClient.findAll().size() == 0
73+
74+
when: "Update the tenancy"
75+
fooAccountClient.updateTenancy(fooAccount.getId(), "bar")
76+
then:
77+
fooAccountClient.findAll().size() == 0
78+
barAccountClient.findAll().size() == 1
79+
fooAccountClient.findOne(fooAccount.getId()).isEmpty()
80+
barAccountClient.findOne(fooAccount.getId()).isPresent()
81+
82+
when: "Update the tenancy"
83+
barAccountClient.updateTenancy(fooAccount.getId(), "foo")
84+
then:
85+
fooAccountClient.findAll().size() == 1
86+
barAccountClient.findAll().size() == 0
87+
fooAccountClient.findOne(fooAccount.getId()).isPresent()
88+
barAccountClient.findOne(fooAccount.getId()).isEmpty()
89+
90+
when:
91+
AccountDto barAccount = barAccountClient.save("The Bar")
92+
def allAccounts = barAccountClient.findAllTenants()
93+
then:
94+
barAccount.tenancy == "bar"
95+
allAccounts.size() == 2
96+
allAccounts.find { it.id == barAccount.id }.tenancy == "bar"
97+
allAccounts.find { it.id == fooAccount.id }.tenancy == "foo"
98+
allAccounts == fooAccountClient.findAllTenants()
99+
100+
when:
101+
def barAccounts = barAccountClient.findAllBarTenants()
102+
then:
103+
barAccounts.size() == 1
104+
barAccounts[0].id == barAccount.id
105+
barAccounts[0].tenancy == "bar"
106+
barAccounts == fooAccountClient.findAllBarTenants()
107+
108+
when:
109+
def fooAccounts = barAccountClient.findAllFooTenants()
110+
then:
111+
fooAccounts.size() == 1
112+
fooAccounts[0].id == fooAccount.id
113+
fooAccounts[0].tenancy == "foo"
114+
fooAccounts == fooAccountClient.findAllFooTenants()
115+
116+
when:
117+
def exp = barAccountClient.findTenantExpression()
118+
then:
119+
exp.size() == 1
120+
exp[0].tenancy == "bar"
121+
exp == fooAccountClient.findTenantExpression()
122+
123+
when: 'Delete all BARs'
124+
barAccountClient.deleteAll()
125+
then: "FOOs aren't deletes"
126+
fooAccountClient.findAll().size() == 1
127+
128+
when: 'Delete all FOOs'
129+
fooAccountClient.deleteAll()
130+
then: "All FOOs are deleted"
131+
fooAccountClient.findAll().size() == 0
132+
cleanup:
133+
embeddedServer?.stop()
134+
}
135+
136+
}
137+
138+
@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
139+
@ExecuteOn(TaskExecutors.IO)
140+
@Controller("/accounts")
141+
class AccountController {
142+
143+
private final AccountRecordRepository accountRepository
144+
145+
AccountController(ApplicationContext beanContext) {
146+
def className = beanContext.getProperty("accountRepositoryClass", String).get()
147+
this.accountRepository = beanContext.getBean(Class.forName(className)) as AccountRecordRepository
148+
}
149+
150+
@Post
151+
AccountDto save(String name) {
152+
def newAccount = new AccountRecord(null, name, null)
153+
def account = accountRepository.save(newAccount)
154+
return new AccountDto(account)
155+
}
156+
157+
@Put("/{id}/tenancy")
158+
void updateTenancy(Long id, String tenancy) {
159+
def account = accountRepository.findById(id).orElseThrow()
160+
accountRepository.update(
161+
new AccountRecord(account.id(), account.name(), tenancy)
162+
)
163+
}
164+
165+
@Get("/{id}")
166+
Optional<AccountDto> findOne(Long id) {
167+
return accountRepository.findById(id).map(AccountDto::new)
168+
}
169+
170+
@Get
171+
List<AccountDto> findAll() {
172+
return findAll0()
173+
}
174+
175+
@Get("/alltenants")
176+
List<AccountDto> findAllTenants() {
177+
return accountRepository.findAll$withAllTenants().stream().map(AccountDto::new).toList()
178+
}
179+
180+
@Get("/foo")
181+
List<AccountDto> findAllFooTenants() {
182+
return accountRepository.findAll$withTenantFoo().stream().map(AccountDto::new).toList()
183+
}
184+
185+
@Get("/bar")
186+
List<AccountDto> findAllBarTenants() {
187+
return accountRepository.findAll$withTenantBar().stream().map(AccountDto::new).toList()
188+
}
189+
190+
@Get("/expression")
191+
List<AccountDto> findTenantExpression() {
192+
return accountRepository.findAll$withTenantExpression().stream().map(AccountDto::new).toList()
193+
}
194+
195+
@Connectable
196+
protected List<AccountDto> findAll0() {
197+
findAll1()
198+
}
199+
200+
@Connectable(propagation = ConnectionDefinition.Propagation.MANDATORY)
201+
protected List<AccountDto> findAll1() {
202+
accountRepository.findAll().stream().map(AccountDto::new).toList()
203+
}
204+
205+
@Delete
206+
void deleteAll() {
207+
deleteAll0()
208+
}
209+
210+
@Transactional
211+
protected deleteAll0() {
212+
deleteAll1()
213+
}
214+
215+
@Transactional(Transactional.TxType.MANDATORY)
216+
protected deleteAll1() {
217+
accountRepository.deleteAll()
218+
}
219+
220+
}
221+
222+
@Introspected
223+
@EqualsAndHashCode
224+
class AccountDto {
225+
Long id
226+
String name
227+
String tenancy
228+
229+
AccountDto() {
230+
}
231+
232+
AccountDto(AccountRecord account) {
233+
id = account.id()
234+
name = account.name()
235+
tenancy = account.tenancy()
236+
}
237+
238+
}
239+
240+
@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
241+
@Client("/accounts")
242+
interface AccountClient {
243+
244+
@Post
245+
AccountDto save(String name);
246+
247+
@Put("/{id}/tenancy")
248+
void updateTenancy(Long id, String tenancy)
249+
250+
@Get("/{id}")
251+
Optional<AccountDto> findOne(Long id);
252+
253+
@Get
254+
List<AccountDto> findAll();
255+
256+
@Get("/alltenants")
257+
List<AccountDto> findAllTenants();
258+
259+
@Get("/foo")
260+
List<AccountDto> findAllFooTenants();
261+
262+
@Get("/bar")
263+
List<AccountDto> findAllBarTenants();
264+
265+
@Get("/expression")
266+
List<AccountDto> findTenantExpression();
267+
268+
@Delete
269+
void deleteAll();
270+
}
271+
272+
273+
@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
274+
@Header(name = "tenantId", value = "foo")
275+
@Client("/accounts")
276+
interface FooAccountClient extends AccountClient {
277+
}
278+
279+
@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
280+
@Header(name = "tenantId", value = "bar")
281+
@Client("/accounts")
282+
interface BarAccountClient extends AccountClient {
283+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.micronaut.data.jdbc.h2
2+
3+
import io.micronaut.data.tck.tests.AbstractDiscriminatorMultitenancySpec
4+
5+
class H2DiscriminatorMultitenancySpec extends AbstractDiscriminatorMultitenancySpec implements H2TestPropertyProvider {
6+
7+
@Override
8+
Map<String, String> getExtraProperties() {
9+
return [accountRepositoryClass: H2AccountRepository.name]
10+
}
11+
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.micronaut.data.jdbc.h2.multitenancy;
2+
3+
import io.micronaut.core.annotation.Nullable;
4+
import io.micronaut.data.annotation.GeneratedValue;
5+
import io.micronaut.data.annotation.Id;
6+
import io.micronaut.data.annotation.MappedEntity;
7+
import io.micronaut.data.annotation.TenantId;
8+
import io.micronaut.serde.annotation.Serdeable;
9+
10+
@Serdeable // <1>
11+
@MappedEntity // <2>
12+
public record TenancyBook(@Nullable
13+
@Id // <3>
14+
@GeneratedValue // <4>
15+
Long id,
16+
String title,
17+
@TenantId // <5>
18+
String framework) {
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.micronaut.data.jdbc.h2.multitenancy;
2+
3+
import io.micronaut.context.annotation.Requires;
4+
import io.micronaut.http.annotation.Controller;
5+
import io.micronaut.http.annotation.Get;
6+
import io.micronaut.scheduling.TaskExecutors;
7+
import io.micronaut.scheduling.annotation.ExecuteOn;
8+
9+
import java.util.List;
10+
11+
@Requires(property = "spec.name", value = "TenancyBookControllerSpec")
12+
@Controller("/books") // <1>
13+
class TenancyBookController {
14+
private final TenancyBookRepository bookRepository;
15+
16+
TenancyBookController(TenancyBookRepository bookRepository) { // <2>
17+
this.bookRepository = bookRepository;
18+
}
19+
20+
@ExecuteOn(TaskExecutors.BLOCKING) // <3>
21+
@Get
22+
// <4>
23+
List<TenancyBook> index() {
24+
return bookRepository.findAll();
25+
}
26+
}

0 commit comments

Comments
 (0)