I am using spring data to fetch data for my application.
The repository class uses a mongo entity class which is being added as an upstream dependency to my project which means I don't have any control to change the source code of the class. As a result of this, I cannot use #Document annotation from org.springframework.data.mongodb.core.mapping to my mongo entity class.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface DummyRepository extends MongoRepository<Dummy, String> {
Page<Dummy> findAll(Pageable pageable);
}
Here, I don't have any control over source code of Dummy class so I can't add #Document to specify collection name for this class
How can I specify the collection name while using DummyRepository to query mongo collection?
One way would be to use #EnableMongoRepositories#repositoryFactoryBeanClass with your own flavor of MongoRepsoitoryFactoryBean overriding getEntityInformation(Class).
Unfortunately there's a bug (DATAMONGO-2297) in the code and for the time being you also need to customize getTargetRepsoitory(RepositoryInformation) as shown in the snippet below.
#Configuration
#EnableMongoRepositories(repositoryFactoryBeanClass = CustomRepoFactory.class)
class config extends AbstractMongoConfiguration {
// ...
}
class CustomRepoFactory extends MongoRepositoryFactoryBean {
public CustomRepoFactory(Class repositoryInterface) {
super(repositoryInterface);
}
#Override
protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
return new MongoRepositoryFactory(operations) {
#Override
public <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return new MappingMongoEntityInformation(
operations.getConverter().getMappingContext().getPersistentEntity(domainClass)) {
#Override
public String getCollectionName() {
return "customize-as-you-wish";
}
};
}
#Override // you should not need this when DATAMONGO-2297 is resolved
protected Object getTargetRepository(RepositoryInformation information) {
MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType());
return getTargetRepositoryViaReflection(information, entityInformation, operations);
}
};
}
}
Related
I have a repository base class as defined below.
#NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
}
public class BaseRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements BaseRepository<T, ID> {
public BaseRepositoryImpl(JpaEntityInformation<T, ?> entityInfo, EntityManager entityMgr) {
super(entityInfo, entityMgr);
}
// ...
}
#Configuration
#EnableJpaRepositories(basePackages = "org.example",
repositoryBaseClass = BaseRepositoryImpl.class)
public class BaseConfig {
// additional JPA Configuration
}
I have defined a business repository class and a query method as seen below.
#Repository
public interface CarRepository extends BaseRepository<Car, Long> {
#Query("SELECT c FROM Car c Where active = 1")
List<Car> findAllActiveCars();
}
I have a test class which invokes the findAllActiveCars(). I am getting the expected results. But, that query method is not invoking any of the methods in BaseRepository class. How to customize the return values of the query methods?
You didn't show the methods that you did implement, so it is not clear why they don't get called, but since you want to decrypt entity fields, consider listening to JPAs entity lifecycle events. #PostLoad should be able to do the trick.
https://docs.jboss.org/hibernate/core/4.0/hem/en-US/html/listeners.html
How can we create a multi-tenant application in spring webflux using Mongodb-reactive repository?
I cannot find any complete resources on the web for reactive applications. all the resources available are for non-reactive applications.
UPDATE:
In a non-reactive application, we used to store contextual data in ThreadLocal but this cannot be done with reactive applications as there is thread switching. There is a way to store contextual info in reactor Context inside a WebFilter, But I don't how get hold of that data in ReactiveMongoDatabaseFactory class.
Thanks.
I was able to Implement Multi-Tenancy in Spring Reactive application using mangodb. Main classes responsible for realizing were: Custom MongoDbFactory class, WebFilter class (instead of Servlet Filter) for capturing tenant info and a ThreadLocal class for storing tenant info. Flow is very simple:
Capture Tenant related info from the request in WebFilter and set it in ThreadLocal. Here I am sending Tenant info using header: X-Tenant
Implement Custom MondoDbFactory class and override getMongoDatabase() method to return database based on current tenant available in ThreadLocal class.
Source code is:
CurrentTenantHolder.java
package com.jazasoft.demo;
public class CurrentTenantHolder {
private static final ThreadLocal<String> currentTenant = new InheritableThreadLocal<>();
public static String get() {
return currentTenant.get();
}
public static void set(String tenant) {
currentTenant.set(tenant);
}
public static String remove() {
synchronized (currentTenant) {
String tenant = currentTenant.get();
currentTenant.remove();
return tenant;
}
}
}
TenantContextWebFilter.java
package com.example.demo;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Component
public class TenantContextWebFilter implements WebFilter {
public static final String TENANT_HTTP_HEADER = "X-Tenant";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().containsKey(TENANT_HTTP_HEADER)) {
String tenant = request.getHeaders().getFirst(TENANT_HTTP_HEADER);
CurrentTenantHolder.set(tenant);
}
return chain.filter(exchange).doOnSuccessOrError((Void v, Throwable throwable) -> CurrentTenantHolder.remove());
}
}
MultiTenantMongoDbFactory.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoDatabase;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
public class MultiTenantMongoDbFactory extends SimpleReactiveMongoDatabaseFactory {
private final String defaultDatabase;
public MultiTenantMongoDbFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDatabase = databaseName;
}
#Override
public MongoDatabase getMongoDatabase() throws DataAccessException {
final String tlName = CurrentTenantHolder.get();
final String dbToUse = (tlName != null ? tlName : this.defaultDatabase);
return super.getMongoDatabase(dbToUse);
}
}
MongoDbConfig.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoClientFactoryBean;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
#Configuration
public class MongoDbConfig {
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate(MultiTenantMongoDbFactory multiTenantMongoDbFactory) {
return new ReactiveMongoTemplate(multiTenantMongoDbFactory);
}
#Bean
public MultiTenantMongoDbFactory multiTenantMangoDbFactory(MongoClient mongoClient) {
return new MultiTenantMongoDbFactory(mongoClient, "test1");
}
#Bean
public ReactiveMongoClientFactoryBean mongoClient() {
ReactiveMongoClientFactoryBean clientFactory = new ReactiveMongoClientFactoryBean();
clientFactory.setHost("localhost");
return clientFactory;
}
}
UPDATE:
In reactive-stream we cannot store contextual information in ThreadLocal any more as the request is not tied to a single thread, So, This is not the correct solution.
However, Contextual information can be stored reactor Context in WebFilter like this. chain.filter(exchange).subscriberContext(context -> context.put("tenant", tenant));. Problem is how do get hold of this contextual info in ReactiveMongoDatabaseFactory implementation class.
Here is my very rough working solution for Spring WebFlux - they have since updated the ReactiveMongoDatabaseFactory - getMongoDatabase to return a Mono
Create web filter
public class TenantContextFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantContextFilter.class);
#Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
ServerHttpRequest request = swe.getRequest();
HttpHeaders headers = request.getHeaders();
if(headers.getFirst("X-TENANT-ID") == null){
LOGGER.info(String.format("Missing X-TENANT-ID header"));
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String tenantId = headers.getFirst("X-TENANT-ID");
LOGGER.info(String.format("Processing request with tenant identifier [%s]", tenantId));
return wfc.filter(swe)
.contextWrite(TenantContextHolder.setTenantId(tenantId));
}
}
Create class to get context (credit to somewhere I found this)
public class TenantContextHolder {
public static final String TENANT_ID = TenantContextHolder.class.getName() + ".TENANT_ID";
public static Context setTenantId(String id) {
return Context.of(TENANT_ID, Mono.just(id));
}
public static Mono<String> getTenantId() {
return Mono.deferContextual(contextView -> {
if (contextView.hasKey(TENANT_ID)) {
return contextView.get(TENANT_ID);
}
return Mono.empty();
}
);
}
public static Function<Context, Context> clearContext() {
return (context) -> context.delete(TENANT_ID);
}
}
My spring security setup (all requests allowed for testing)
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain WebFilterChain(ServerHttpSecurity http) {
return http
.formLogin(it -> it.disable())
.cors(it -> it.disable()) //fix this
.httpBasic(it -> it.disable())
.csrf(it -> it.disable())
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange(it -> it.anyExchange().permitAll()) //allow anonymous
.addFilterAt(new TenantContextFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
.build();
}
}
Create Tenant Mongo DB Factory
I still have some clean-up work for defaults etc...
public class MultiTenantMongoDBFactory extends SimpleReactiveMongoDatabaseFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(MultiTenantMongoDBFactory.class);
private final String defaultDb;
public MultiTenantMongoDBFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDb = databaseName;
}
#Override
public Mono<MongoDatabase> getMongoDatabase() throws DataAccessException {
return TenantContextHolder.getTenantId()
.map(id -> {
LOGGER.info(String.format("Database trying to retrieved is [%s]", id));
return super.getMongoDatabase(id);
})
.flatMap(db -> {
return db;
})
.log();
}
}
Configuration Class
#Configuration
#EnableReactiveMongoAuditing
#EnableReactiveMongoRepositories(basePackages = {"com.order.repository"})
class MongoDbConfiguration {
#Bean
public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
return new MultiTenantMongoDBFactory(MongoClients.create("mongodb://user:password#localhost:27017"), "tenant_catalog");
}
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate() {
ReactiveMongoTemplate template = new ReactiveMongoTemplate(reactiveMongoDatabaseFactory());
template.setWriteResultChecking(WriteResultChecking.EXCEPTION);
return template;
}
}
Entity Class
#Document(collection = "order")
//getters
//setters
Testing
Create two mongo db's with same collection, put different documents in both
In Postman I just did a get request with the "X-TENANT-ID" header and database name as the value (e.g. tenant-12343 or tenant-34383) and good to go!
This is my param converter
import org.springframework.data.domain.Sort;
public class MyParamConverter implements ParamConverter<Sort> {
#Override
public Sort fromString(String s){
return new Sort(new Sort.Order(Sort.Direction.ASC, "ds"));
}
#Override
public String toString(Sort mo){
return mo.toString();
}
}
this is my paramconverter provider
#Provider
public class MyParamConverterProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if(rawType.equals(Sort.class)){
return (ParamConverter<T>) new MyParamConverter();
}
return null;
}
I am trying to use in my API as
#GET
#Path("/")
Response read(#QueryParam("sort") Sort order);
I am expecting the jax to map string that I pass in my url e.g. &sort="asc" to Sort object. But I am getting an compile time error that have a registered implementation of paramconverter provider. I need to find a way when I pass a query param as &sort="somethung" it gets convert to SORT automatically either by using custom annotation or by using Param Converter.
with reference to your comment, try registering your provider like:
#ApplicationPath("/")
public class MyApplication extends ResourceConfig {
#Override
public Set<Class<?>> getClasses() {
final Set<Class<?>> classes = new HashSet<Class<?>>();
classes.add(MyParamConverterProvider.class);
return classes;
}
}
or, if you are using Jersey
#ApplicationPath("/")
public class MyApplication extends ResourceConfig {
public MyApplication() {
packages("my.package");
// or without package scanning
register(MyParamConverterProvider.class);
}
}
I need to run inner class test cases from eclipse using Junit4. I understand that there is org.junit.runners.Enclosed that is intended to serve this purpose. It works well for "plain" unit test i.e. without the need for spring context configuration.
For my case, give sample code below, Adding another annotation of Enclosed does not work since there is a conflict of both SpringJUnit4ClassRunner and Enclosed test runners. How can I solve this problem ?
Note: Kindly ignore any basic spelling mistake/basic import issues in the below example since I tried to cook up from my actual use-case.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = { "/unit-test-context.xml"})
public class FooUnitTest {
// Mocked dependency through spring context
#Inject
protected DependentService dependentService;
public static class FooBasicScenarios extends FooUnitTest{
#Test
public void testCase1 {
.....
List<Data> data = dependentService.getData();
.....
}
}
public static class FooNeagativeScenarios extends FooUnitTest{
#Test
public void testCase1 {
.....
List<Data> data = dependentService.getData();
.....
}
}
}
}
FooUnitTest is a container, you cannot use it as a superclass.
You need to move all your spring-code to Scenario-classes. And use #RunWith(Enclosed.class). For example, with abstract superclass
#RunWith(Enclosed.class)
public class FooUnitTest {
#ContextConfiguration(locations = { "/unit-test-context.xml"})
protected abstract static class BasicTestSuit {
// Mocked dependency through spring context
#Inject
protected DependentService dependentService;
}
#RunWith(SpringJUnit4ClassRunner.class)
public static class FooBasicScenarios extends BasicTestSuit {
#Test
public void testCase1 {
.....
List<Data> data = dependentService.getData();
.....
}
}
#RunWith(SpringJUnit4ClassRunner.class)
public static class FooNeagativeScenarios extends BasicTestSuit {
#Test
public void testCase1 {
.....
List<Data> data = dependentService.getData();
.....
}
}
}
Of course you can declare all dependencies in each Scenario-class, in that case there is no necessary in abstract superclass.
Use Case
I am trying to use Adding custom behaviour to all repositories functionality of Spring Data MongoDB.
The documentation unhelpfully describes how to connect using JPA. Anyways got the config setup with Mongo equivalent.
I want to add a findByCategoryName(String categoryName) method to all entities as all my entities will have a Category . Category is a DBRef object so have to use custom query.
Below is relevant part of the config
<!-- Activate Spring Data MongoDB repository support -->
<mongo:repositories base-package="com.domain.*.repo" repository-impl-postfix="CustomImpl"
factory-class="com.domain.commonrepo.CommonMongoRepoFactoryBean"/>
<bean id="mappingContext" class="org.springframework.data.mongodb.core.mapping.MongoMappingContext" />
<mongo:mapping-converter mapping-context-ref="mappingContext">
<mongo:custom-converters base-package="com.domain.mongo.converter" />
</mongo:mapping-converter>
<bean id="entityInformationCreator" class="org.springframework.data.mongodb.repository.support.DefaultEntityInformationCreator">
<constructor-arg name="mappingContext" ref="mappingContext" />
</bean>
.
.
The FactoryBean
#NoRepositoryBean
public class CommonMongoRepoFactoryBean<T extends MongoRepository<?,?>, ID extends
Serializable> extends MongoRepositoryFactoryBean{
#Autowired
private static MongoTemplate mongoTemplate;
protected MongoRepositoryFactory getRepositoryFactory(Class<T> clazz) {
return new CommonMongoRepoFactory(clazz);
}
private static class CommonMongoRepoFactory extends MongoRepositoryFactory {
private Class clazz;
public CommonMongoRepoFactory(Class clazz) {
super(mongoTemplate);
this.clazz = clazz;
}
public CommonMongoRepoImpl getTargetRepository() {
return new CommonMongoRepoImpl(clazz);
}
public Class<?> getRepositoryClass() {
return CommonMongoRepoImpl.class;
}
}
I know it's a bit of a hack but with no documentation it is a pain. If anyone knows better PLEASE give me a github link :-)
Common Repo interface
#NoRepositoryBean
public interface CommonMongoRepo<T, ID extends Serializable> extends MongoRepository<T,ID> {
public List<T> findByCategoryName(String categoryName);
Implementation
#NoRepositoryBean
public class CommonMongoRepoImpl<T, ID extends Serializable> extends SimpleMongoRepository<T,
ID> implements CommonMongoRepo<T, ID> {
private Class<T> type;
#Autowired
private static MongoTemplate mongoOperations;
#Autowired
private static EntityInformationCreator entityInformationCreator;
#Autowired
private CategoryRepo categoryRepo;
public CommonMongoRepoImpl(Class<T> type) {
super((MongoEntityInformation<T, ID>) entityInformationCreator.getEntityInformation(type), mongoOperations);
}
#Override
public List<T> findByCategoryName(String categoryName) {
Category category = categoryRepo.findByName(categoryName);
return mongoOperations.find(query(where("categories.$id").is(category.getId())), type);
}
PROBLEM
Now when I am trying to use the common method I get an exception
No Property category found in "Entity". Which is I guess when mongo repo is trying to auto implement the method. This is inspite of me declaring the bean as #NoRepositoryBean
PLEASE HELP!!! Dont want to add the same custom method to all the entities
Here is the best solution!
Step One:
Add a custom method to interface!
增加一个自定义的方法
#custom interface
/**
* Basic Repository for common custom methods
* #author liangping
*/
import java.io.Serializable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
#NoRepositoryBean
public interface WootideRepositoryCustom <T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID>, MongoRepository<T, ID> {
public Page<T> search(Query query, Pageable pageable);
}
Implementation
Step Two:
Add implement for your custom method!
实现你的自定义方法
/**
* implement for wootide basic repository
* #author liangping
*/
import java.io.Serializable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
public class WootideRepositoryImpl<T, ID extends Serializable> extends
SimpleMongoRepository<T, ID> implements WootideRepositoryCustom<T, ID> {
public WootideRepositoryImpl(MongoEntityInformation<T, ID> metadata,
MongoOperations mongoOperations) {
super(metadata, mongoOperations);
}
#Override
public Page<T> search(Query query, Pageable pageable) {
long total = this.getMongoOperations().count(query, this.getEntityInformation().getJavaType() );
return new PageImpl<T>(this.getMongoOperations().find(query.with(pageable), this.getEntityInformation().getJavaType()), pageable, total);
}
}
Create a new factory for custom repository
/**
* Repository Factory for all Subrepository
* #author liangping
*/
import java.io.Serializable;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.repository.support.MappingMongoEntityInformation;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
public class WootideRepositoryFactoryBean<R extends MongoRepository<T, I>, T, I extends Serializable>
extends MongoRepositoryFactoryBean<R, T, I> {
#Override
protected RepositoryFactorySupport getFactoryInstance(
MongoOperations operations) {
return new WootideMongoRepositoryFactory<T,I>( operations );
}
private static class WootideMongoRepositoryFactory<T, ID extends Serializable>
extends MongoRepositoryFactory {
private MongoOperations mongo;
public WootideMongoRepositoryFactory(MongoOperations mongoOperations) {
super(mongoOperations);
this.mongo = mongoOperations;
}
#SuppressWarnings("unchecked")
protected Object getTargetRepository(RepositoryMetadata metadata) {
TypeInformation<T> information = ClassTypeInformation.from((Class<T>)metadata.getDomainType());
MongoPersistentEntity<T> pe = new BasicMongoPersistentEntity<T>(information);
MongoEntityInformation<T,ID> mongometa = new MappingMongoEntityInformation<T, ID>(pe);
return new WootideRepositoryImpl<T, ID>( mongometa, mongo);
}
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return WootideRepositoryCustom.class;
}
}
}
Make it works
<mongo:repositories base-package="com.***.mongodb"
factory-class="com.***.mongodb.custom.WootideRepositoryFactoryBean"/>
Good Luck! 祝你好运!
Somewhat delayed but here is sample code that does this for a Spring web app project. The salient points are:
Interface used in Controller
Implementation done in a separate class that inherits from a base
The base implementation provides common methods that any other Controller can use with just a quick inheritance