Skip to content

Commit

Permalink
feat: Adding MongoDatabaseResolver
Browse files Browse the repository at this point in the history
It provide ways to implement multi-tenancy using a database per tenant approch

It'll select dynamically the database to be used
Example:
- MongoDatabaseResolver.resolve() returns "customerA"
The selected mongo database to be used will be "customerA"
  • Loading branch information
pedroh-pereira committed Nov 11, 2022
1 parent 16adf2c commit 1e642a9
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.mongodb.panache.common;

/*
* This interface can be used to resolve the mongo database name in runtime, it'll help to implement multi-tenancy using tenant per database approach
*/
public interface MongoDatabaseResolver {
String resolve();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.beanName;
import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.clientFromArc;
import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.getDatabaseName;
import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.getDatabaseNameFromResolver;

import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -26,6 +27,7 @@
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.WriteModel;
import com.mongodb.client.result.DeleteResult;

import io.quarkus.mongodb.panache.common.MongoEntity;
import io.quarkus.mongodb.panache.common.binder.NativeQueryBinder;
Expand Down Expand Up @@ -74,7 +76,7 @@ public Uni<Void> persist(Iterable<?> entities) {
objects.add(entity);
}

if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -99,7 +101,7 @@ public Uni<Void> persist(Object firstEntity, Object... entities) {
public Uni<Void> persist(Stream<?> entities) {
return Uni.createFrom().deferred(() -> {
List<Object> objects = entities.collect(Collectors.toList());
if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -122,7 +124,7 @@ public Uni<Void> update(Iterable<?> entities) {
objects.add(entity);
}

if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -147,7 +149,7 @@ public Uni<Void> update(Object firstEntity, Object... entities) {
public Uni<Void> update(Stream<?> entities) {
return Uni.createFrom().deferred(() -> {
List<Object> objects = entities.collect(Collectors.toList());
if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -170,7 +172,7 @@ public Uni<Void> persistOrUpdate(Iterable<?> entities) {
objects.add(entity);
}

if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -195,7 +197,7 @@ public Uni<Void> persistOrUpdate(Object firstEntity, Object... entities) {
public Uni<Void> persistOrUpdate(Stream<?> entities) {
return Uni.createFrom().deferred(() -> {
List<Object> objects = entities.collect(Collectors.toList());
if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
ReactiveMongoCollection collection = mongoCollection(firstEntity);
Expand Down Expand Up @@ -314,7 +316,7 @@ private ReactiveMongoDatabase mongoDatabase(MongoEntity mongoEntity) {
if (mongoEntity != null && !mongoEntity.database().isEmpty()) {
return mongoClient.getDatabase(mongoEntity.database());
}
String databaseName = getDefaultDatabaseName(mongoEntity);
String databaseName = getDatabaseNameFromResolver().orElse(getDefaultDatabaseName(mongoEntity));
return mongoClient.getDatabase(databaseName);
}

Expand Down Expand Up @@ -621,7 +623,7 @@ public Uni<Long> count(Class<?> entityClass, Document query) {

public Uni<Long> deleteAll(Class<?> entityClass) {
ReactiveMongoCollection<?> collection = mongoCollection(entityClass);
return collection.deleteMany(new Document()).map(deleteResult -> deleteResult.getDeletedCount());
return collection.deleteMany(new Document()).map(DeleteResult::getDeletedCount);
}

public Uni<Boolean> deleteById(Class<?> entityClass, Object id) {
Expand All @@ -634,14 +636,14 @@ public Uni<Long> delete(Class<?> entityClass, String query, Object... params) {
String bindQuery = bindFilter(entityClass, query, params);
BsonDocument docQuery = BsonDocument.parse(bindQuery);
ReactiveMongoCollection<?> collection = mongoCollection(entityClass);
return collection.deleteMany(docQuery).map(deleteResult -> deleteResult.getDeletedCount());
return collection.deleteMany(docQuery).map(DeleteResult::getDeletedCount);
}

public Uni<Long> delete(Class<?> entityClass, String query, Map<String, Object> params) {
String bindQuery = bindFilter(entityClass, query, params);
BsonDocument docQuery = BsonDocument.parse(bindQuery);
ReactiveMongoCollection<?> collection = mongoCollection(entityClass);
return collection.deleteMany(docQuery).map(deleteResult -> deleteResult.getDeletedCount());
return collection.deleteMany(docQuery).map(DeleteResult::getDeletedCount);
}

public Uni<Long> delete(Class<?> entityClass, String query, Parameters params) {
Expand All @@ -651,7 +653,7 @@ public Uni<Long> delete(Class<?> entityClass, String query, Parameters params) {
//specific Mongo query
public Uni<Long> delete(Class<?> entityClass, Document query) {
ReactiveMongoCollection<?> collection = mongoCollection(entityClass);
return collection.deleteMany(query).map(deleteResult -> deleteResult.getDeletedCount());
return collection.deleteMany(query).map(DeleteResult::getDeletedCount);
}

public UpdateType update(Class<?> entityClass, String update, Map<String, Object> params) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.quarkus.mongodb.panache.common.runtime;

import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.function.Function;

import javax.inject.Named;

import io.quarkus.arc.Arc;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.quarkus.mongodb.panache.common.MongoEntity;
import io.quarkus.mongodb.runtime.MongoClientBeanUtil;
import io.quarkus.mongodb.runtime.MongoClientConfig;
Expand All @@ -17,6 +20,13 @@ public final class BeanUtils {
private BeanUtils() {
}

private static <T, R> Optional<R> getValueFromArcInstance(Class<T> instanceClass, Function<T, R> mapFn) {
return Optional.of(Arc.container().instance(instanceClass))
.filter(InstanceHandle::isAvailable)
.map(InstanceHandle::get)
.map(mapFn);
}

public static String beanName(MongoEntity entity) {
if (entity != null && !entity.clientName().isEmpty()) {
return entity.clientName();
Expand Down Expand Up @@ -82,4 +92,9 @@ public static String getDatabaseName(MongoEntity mongoEntity, String clientBeanN
"The database attribute was not set for the @MongoEntity annotation neither was the database property configured for the named Mongo Client (via 'quarkus.mongodb.%s.database')",
mongoEntity.clientName()));
}

public static Optional<String> getDatabaseNameFromResolver() {
return getValueFromArcInstance(MongoDatabaseResolver.class, MongoDatabaseResolver::resolve);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void update(Iterable<?> entities) {
objects.add(entity);
}

if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
MongoCollection collection = mongoCollection(firstEntity);
Expand All @@ -134,7 +134,7 @@ public void update(Object firstEntity, Object... entities) {

public void update(Stream<?> entities) {
List<Object> objects = entities.collect(Collectors.toList());
if (objects.size() > 0) {
if (!objects.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = objects.get(0);
MongoCollection collection = mongoCollection(firstEntity);
Expand Down Expand Up @@ -214,7 +214,7 @@ private void persist(MongoCollection collection, Object entity) {
}

private void persist(List<Object> entities) {
if (entities.size() > 0) {
if (!entities.isEmpty()) {
// get the first entity to be able to retrieve the collection with it
Object firstEntity = entities.get(0);
MongoCollection collection = mongoCollection(firstEntity);
Expand Down Expand Up @@ -383,7 +383,7 @@ private MongoDatabase mongoDatabase(MongoEntity mongoEntity) {
if (mongoEntity != null && !mongoEntity.database().isEmpty()) {
return mongoClient.getDatabase(mongoEntity.database());
}
String databaseName = getDefaultDatabaseName(mongoEntity);
String databaseName = BeanUtils.getDatabaseNameFromResolver().orElse(getDefaultDatabaseName(mongoEntity));
return mongoClient.getDatabase(databaseName);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.it.mongodb.panache;

public class ResolversPropertiesConstants {
public static final String MONGO_DATABASE_RESOLVER_ENABLED_PROPERTY = "quarkus.mongodb.database-resolver.enabled";

private ResolversPropertiesConstants() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.it.mongodb.panache.profiles;

import io.quarkus.test.junit.QuarkusTestProfile;

public class MongoDatabaseResolverTestProfile implements QuarkusTestProfile {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkus.it.mongodb.panache.reactive.resolvers;

import io.quarkus.it.mongodb.panache.resolvers.BaseMongodbPanacheResolversTest;
import io.quarkus.mongodb.panache.reactive.runtime.JavaReactiveMongoOperations;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
class ReactiveMongodbPanacheResourceMongoDatabaseResolverTest extends BaseMongodbPanacheResolversTest {

public ReactiveMongodbPanacheResourceMongoDatabaseResolverTest() {
super(JavaReactiveMongoOperations.INSTANCE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.quarkus.it.mongodb.panache.resolvers;

import static org.junit.jupiter.api.Assertions.assertEquals;

import javax.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.mockito.Mockito;

import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.quarkus.mongodb.panache.common.reactive.runtime.ReactiveMongoOperations;
import io.quarkus.mongodb.panache.common.runtime.MongoOperations;
import io.quarkus.test.Mock;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.mongodb.MongoReplicaSetTestResource;

@QuarkusTestResource(MongoReplicaSetTestResource.class)
@DisabledOnOs(OS.WINDOWS)
@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class BaseMongodbPanacheResolversTest {

private static final String EXPECTED_DATABASE_NAME_FROM_RESOLVER = "tenant-a";

@ConfigProperty(name = "quarkus.mongodb.database")
String databaseNameFromProperty;

@InjectMock
private MongoDatabaseResolver mongoDatabaseResolver;

private Object mongoOperations;

public BaseMongodbPanacheResolversTest(Object mongoOperations) {
this.mongoOperations = mongoOperations;
}

@Test
public void testMongoDatabaseResolverUsingDatabaseFromResolve() throws Exception {
testMongoDatabaseResolver(ResolverEntity.class, EXPECTED_DATABASE_NAME_FROM_RESOLVER, true, false);
}

@Test
public void testMongoDatabaseResolverKeepingDatabaseFromAnnotation() throws Exception {
testMongoDatabaseResolver(ResolverEntityWithDatabase.Entity.class, ResolverEntityWithDatabase.DATABASE_NAME, true,
true);
}

@Test
public void testMongoDatabaseResolverUsingDefaultDatabaseFromProperties() throws Exception {
testMongoDatabaseResolver(ResolverEntity.class, databaseNameFromProperty, false, false);
}

private void testMongoDatabaseResolver(Class<?> entityClass, String expectedDatabaseName, boolean shouldMockResolveReturn,
boolean expectedNoCallInResolveMethod)
throws Exception {
//arrange
if (shouldMockResolveReturn) {
Mockito.when(mongoDatabaseResolver.resolve()).thenReturn(expectedDatabaseName);
}

//act
String databaseName = null;
if (mongoOperations instanceof MongoOperations) {
MongoOperations auxMongoOperations = (MongoOperations) mongoOperations;
databaseName = auxMongoOperations.mongoDatabase(entityClass).getName();
}

if (mongoOperations instanceof ReactiveMongoOperations) {
ReactiveMongoOperations auxReactiveMongoOperations = (ReactiveMongoOperations) mongoOperations;
databaseName = auxReactiveMongoOperations.mongoDatabase(entityClass).getName();
}

//assert
Mockito.verify(mongoDatabaseResolver, Mockito.times(expectedNoCallInResolveMethod ? 0 : 1)).resolve();
assertEquals(expectedDatabaseName, databaseName);
}

@Mock
@ApplicationScoped
public static class TestMongoDatabaseResolver implements MongoDatabaseResolver {

@Override
public String resolve() {
return null;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.it.mongodb.panache.resolvers;

import io.quarkus.mongodb.panache.runtime.JavaMongoOperations;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
class MongodbPanacheResolversTest extends BaseMongodbPanacheResolversTest {

public MongodbPanacheResolversTest() {
super(JavaMongoOperations.INSTANCE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.it.mongodb.panache.resolvers;

import io.quarkus.mongodb.panache.PanacheMongoEntityBase;

public class ResolverEntity extends PanacheMongoEntityBase {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.it.mongodb.panache.resolvers;

import io.quarkus.mongodb.panache.common.MongoEntity;

public class ResolverEntityWithDatabase {
public static final String DATABASE_NAME = "database-from-anotation";

@MongoEntity(database = DATABASE_NAME)
public static class Entity {

}
}

0 comments on commit 1e642a9

Please sign in to comment.