I'm dumping some data into my MongoDb and generate a UUID on the way. In the collection, this UUID field is stored as LUUID (legacy UUID - type 3) and I don't know how to avoid this, because I would want the format to be the standard UUID (type 4).
Entity:
#Document(collection = "sms")
public class SmsEntity {
...
private UUID ubmMessageSid; // <- this field gets stored as LUUID
...
public static class Builder {
...
private UUID ubmMessageSid;
...
public Builder ubmMessageSid(UUID ubmMessageSid) {
this.ubmMessageSid = ubmMessageSid;
return this;
}
public SmsEntity build() {return new SmsEntity(this);}
}
}
Repo:
#Repository
public interface SmsRepository extends CrudRepository<SmsEntity, String> {
}
Service storing this entity:
...
var ubmId = UUID.randomUUID();
var smsEntity = SmsEntity.builder()
.ubmMessageSid(ubmId)
...
.build();
repository.save(smsEntity);
Anything I have to annotate or configure to store the UUID as Binary/type4?
In Spring Boot 2, for Spring MongoDB 3.x, you can set this using the autoconfiguration properties:
# Options: unspecified, standard, c_sharp_legacy, java_legacy, python_legacy
spring.data.mongodb.uuid-representation=standard
On version 3.x of Spring Data Mongo MongoClient has been replaced by MongoClientSettings:
CodecRegistry codecRegistry =
CodecRegistries.fromRegistries(CodecRegistries.fromCodecs(new UuidCodec(UuidRepresentation.JAVA_LEGACY)),
MongoClientSettings.getDefaultCodecRegistry());
return new MongoClient(new ServerAddress(address, port), MongoClientSettings.builder().codecRegistry(codecRegistry).build());
You can set the UUID codec in your Mongo config. That will persist your UUIDs with type 4 codec. The code you need to do that is the following:
CodecRegistries.fromRegistries(CodecRegistries.fromCodecs(new UuidCodec(UuidRepresentation.STANDARD)),
MongoClient.getDefaultCodecRegistry());
return new MongoClient(new ServerAddress(address, port), MongoClientOptions.builder().codecRegistry(codecRegistry).build());
Here is the complete class just in case:
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.ServerAddress;
import org.bson.UuidRepresentation;
import org.bson.codecs.UuidCodec;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
#Configuration
#EnableAutoConfiguration
#EnableMongoRepositories(basePackages = "com.yourpackage.repositories")
public class MongoConfig extends AbstractMongoConfiguration {
#Value("${mongo.database}")
String database;
#Value("${mongo.address}")
String address;
#Value("${mongo.port}")
Integer port;
#Override
protected String getDatabaseName() {
return database;
}
#Override
public MongoClient mongoClient() {
CodecRegistry codecRegistry =
CodecRegistries.fromRegistries(CodecRegistries.fromCodecs(new UuidCodec(UuidRepresentation.STANDARD)),
MongoClient.getDefaultCodecRegistry());
return new MongoClient(new ServerAddress(address, port), MongoClientOptions.builder().codecRegistry(codecRegistry).build());
}
#Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
#Bean
public ValidatingMongoEventListener validatingMongoEventListener() {
return new ValidatingMongoEventListener(localValidatorFactoryBean());
}
}
Attention: With the new Spring Boot Version 2.2.0.RELEASE AbstractMongoConfiguration becomes deprecated and this is not working anymore. I made a post for that, maybe it's a bug or maybe someone knows the answer: Spring Boot Standard UUID codec not working with AbstractMongoClientConfiguration
In my case the property in the application.yml didn't work as well as the one using CodecRegistries. My working solution is:
MongoConfig.java:
#Bean
public MongoClient mongoClient() {
MongoClientSettings.Builder builder = MongoClientSettings.builder();
builder.uuidRepresentation(UuidRepresentation.JAVA_LEGACY);
return MongoClients.create(builder.build());
}
Related
For my Springboot application, I have a requirement that MongoDB URI should be specified with "app1.mongodb.uri" in application.properties. Yes we don't want to use "spring.data.mongodb.uri" because I was told that it's misleading (what!?). Does anyone know what is the simplest way to do that ? My application is all running fine, and I'm so reluctant to make any big change because of this "requirement".
Figured it out how to do it. The trick is to override the beam MongoClient and Mongotemplate.
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
#SuppressWarnings("deprecation")
#Configuration
#EnableMongoRepositories
#PropertySource("classpath:application.properties")
public class MongoDBConfig extends AbstractMongoConfiguration {
#Value("${app1.mongodb.db}")
private String database;
#Value("${app1.mongodb.uri}")
private String uri;
#Override
#Bean
public MongoClient mongoClient() {
MongoClientURI mongoURI = new MongoClientURI(uri);
MongoClient client = new MongoClient(mongoURI);
return client;
}
#Override
protected String getDatabaseName() {
return database;
}
#Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongoClient(), getDatabaseName());
}
}
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!
I was looking to change WriteResultChecking property of mongoTemplate whilst working on Spring boot app (2.0.5). I found out a way via extending AbstractMongoConfiguration as below.
I got the same working, however i found this approach a bit risky.
Saying this because this approach forced me to write a implementation for
public MongoClient mongoClient() {
return new MongoClient(host, port);
}
Now MongoClient is the central class to maintain connections with MongoDB and if i am forced to write implementation for the same, then i may be possibly missing out on optimizations that spring framework does.
Can someone please suggest any other optimal way of overriding some properties/behaviours without having to tinker too much ?
#Configuration
public class MyMongoConfigs extends AbstractMongoConfiguration {
#Value("${spring.data.mongodb.database}")
private String databaseName;
#Value("${spring.data.mongodb.host}")
private String host;
#Value("${spring.data.mongodb.port}")
private int port;
#Override
public MongoClient mongoClient() {
return new MongoClient(host, port);
}
#Override
protected String getDatabaseName() {
return databaseName;
}
#Bean
public MongoTemplate mongoTemplate() throws Exception {
MongoTemplate myTemp = new MongoTemplate(mongoDbFactory(), mappingMongoConverter());
**myTemp.setWriteResultChecking(WriteResultChecking.EXCEPTION);**
return myTemp;
}
You are in right direction. Using AbstractMongoConfiguration you override the configs that you need to customize and it's the right way to do it. AbstractMongoConfiguration is still Spring Provided class, so the you don't have to worry about optimization, unless you mess with your configuration.
This is my approach:
package app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.WriteResultChecking;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import com.mongodb.WriteConcern;
#Configuration
class ApplicationConfig {
#Bean
MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter converter) {
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);
mongoTemplate.setWriteResultChecking(WriteResultChecking.EXCEPTION);
return mongoTemplate;
}
}
I have figured this out by inspecting the source code of org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration.
I wrote CustomMongoTemplate which override method find() with added criteria to check if document has 'deletedAt' field;
Custom MongoTemplate:
public class CustomMongoTemplate extends MongoTemplate {
public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) {
super(mongoDbFactory, mongoConverter);
}
#Override
public <T> List<T> find(Query query, Class<T> entityClass, String collectionName) {
query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));
return super.find(query, entityClass, collectionName);
}
Then create Bean in configuration class:
#Configuration
public class MyConfiguration {
//...
#Bean(name = "mongoTemplate")
CustomMongoTemplate customMongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
return new CustomMongoTemplate(databaseFactory, converter);
}
//...
}
And last thing - allow Spring to override default MongoTemplate bean. Add next thing to your application.properties file:
spring.main.allow-bean-definition-overriding=true
When inserting data into MongoDB Spring Data is adding a custom "_class" column, is there a way to eliminate the "class" column when using Spring Boot & MongoDB?
Or do i need to create a custom type mapper?
Any hints or advice?
Dave's answer is correct. However, we generally recommend not do this (that's why it's enabled by default in the first place) as you effectively throw away to persist type hierarchies or even a simple property set to e.g. Object. Assume the following type:
#Document
class MyDocument {
private Object object;
}
If you now set object to a value, it will be happily persisted but there's no way you can read the value back into it's original type.
A more up to date answer to that question, working with embedded mongo db for test cases:
I quote from http://mwakram.blogspot.fr/2017/01/remove-class-from-mongo-documents.html
Spring Data MongoDB adds _class in the mongo documents to handle
polymorphic behavior of java inheritance. If you want to remove _class
just drop following Config class in your code.
package com.waseem.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
#Configuration
public class MongoConfig {
#Autowired MongoDbFactory mongoDbFactory;
#Autowired MongoMappingContext mongoMappingContext;
#Bean
public MappingMongoConverter mappingMongoConverter() {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return converter;
}
}
Here is a slightly simpler approach:
#Configuration
public class MongoDBConfig implements InitializingBean {
#Autowired
#Lazy
private MappingMongoConverter mappingMongoConverter;
#Override
public void afterPropertiesSet() throws Exception {
mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
}
}
You can remove the _class by following code. You can use this in your mongo configuration class.
#Bean
public MongoTemplate mongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return new MongoTemplate(databaseFactory, converter);
}
I think you need to create a #Bean of type MongoTemplate and set the type converter explicitly. Details (non-Boot but just extract the template config): http://www.mkyong.com/mongodb/spring-data-mongodb-remove-_class-column/
Similar to RZet but avoids inheritance:
#Configuration
public class MongoConfiguration {
#Autowired
private MappingMongoConverter mappingMongoConverter;
// remove _class
#PostConstruct
public void setUpMongoEscapeCharacterConversion() {
mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
}
}
A simple way (+ for ReactiveMongoTemplate):
#Configuration
public class MongoDBConfig {
#Autowired
private MongoClient mongoClient;
#Value("${spring.data.mongodb.database}")
private String dbName;
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate() {
ReactiveMongoTemplate template = new ReactiveMongoTemplate(mongoClient, dbName);
MappingMongoConverter converter = (MappingMongoConverter) template.getConverter();
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
converter.afterPropertiesSet();
return template;
}
}
Add a converter to remove class.
MappingMongoConverter converter =
new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext());
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(), converter);
return mongoTemplate;
`
The correct answer above seems to be using a number of deprecated dependencies. For example if you check the code, it mentions MongoDbFactory which is deprecated in the latest Spring release. If you happen to be using MongoDB with Spring-Data in 2020, this solution seems to be older. For instant results, check this snippet of code. Works 100%.
Just Create a new AppConfig.java file and paste this block of code. You'll see the "_class" property disappearing from the MongoDB document.
package com.reddit.redditmain.Configuration;
import org.apache.naming.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
#Configuration
public class AppConfig {
#Autowired
MongoDatabaseFactory mongoDbFactory;
#Autowired
MongoMappingContext mongoMappingContext;
#Bean
public MappingMongoConverter mappingMongoConverter() {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return converter;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import
org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import
org.springframework.data.mongodb.core.convert.MappingMongoConverter;
#Configuration
public class MongoConfigWithAuditing {
#Bean
#Primary
public MongoTemplate mongoTemplate(MongoDatabaseFactory
mongoDatabaseFactory, MappingMongoConverter mappingMongoConverter) {
// this is to avoid saving _class to db
mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDatabaseFactory, mappingMongoConverter);
return mongoTemplate;
}
}
Spring Boot 3 with reactive mongo.
package es.dmunozfer.trading.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
import lombok.extern.slf4j.Slf4j;
#Slf4j
#Configuration
#EnableReactiveMongoRepositories("es.dmunozfer.trading.repository")
public class MongoConfig extends AbstractReactiveMongoConfiguration {
#Autowired
private MongoProperties mongoProperties;
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Bean
#Override
public MappingMongoConverter mappingMongoConverter(ReactiveMongoDatabaseFactory databaseFactory, MongoCustomConversions customConversions,
MongoMappingContext mappingContext) {
MappingMongoConverter converter = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return converter;
}
}
I'm leaving this answer here in case someone wants to remove the _class from kotlin and update it a bit since the previous answers have several deprecated dependencies.
import org.springframework.beans.factory.BeanFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.MongoDatabaseFactory
import org.springframework.data.mongodb.core.convert.DbRefResolver
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper
import org.springframework.data.mongodb.core.convert.MappingMongoConverter
import org.springframework.data.mongodb.core.mapping.MongoMappingContext
#Configuration
internal class SpringMongoConfig {
#Bean
fun mappingMongoConverter(
factory: MongoDatabaseFactory, context: MongoMappingContext,
beanFactory: BeanFactory
): MappingMongoConverter {
val dbRefResolver: DbRefResolver = DefaultDbRefResolver(factory)
val mappingConverter = MappingMongoConverter(dbRefResolver, context)
mappingConverter.setTypeMapper(DefaultMongoTypeMapper(null))
return mappingConverter
}
}
The default MappingMongoConverter adds a custom type key ("_class") to each object in the database. So, if I create a Person:
package my.dto;
public class Person {
String name;
public Person(String name) {
this.name = name;
}
}
and save it to the db:
MongoOperations ops = new MongoTemplate(new Mongo(), "users");
ops.insert(new Person("Joe"));
the resulting object in the mongo will be:
{ "_id" : ObjectId("4e2ca049744e664eba9d1e11"), "_class" : "my.dto.Person", "name" : "Joe" }
Questions:
What are the implications of moving the Person class into a different namespace?
Is it possible not to pollute the object with the "_class" key; without writing a unique converter just for the Person class?
So here's the story: we add the type by default as some kind of hint what class to instantiate actually. As you have to pipe in a type to read the document into via MongoTemplate anyway there are two possible options:
You hand in a type the actual stored type can be assigned to. In that case we consider the stored type, use that for object creation. Classical example here is doing polymorphic queries. Suppose you have an abstract class Contact and your Person. You could then query for Contacts and we essentially have to determine a type to instantiate.
If you - on the other hand - pass in a completely different type we'd simply marshal into that given type, not into the one stored in the document actually. That would cover your question what happens if you move the type.
You might be interested in watching this ticket which covers some kind of pluggable type mapping strategy to turn the type information into an actual type. This can serve simply space saving purposes as you might want to reduce a long qualified class name to a hash of a few letters. It would also allow more complex migration scenarios where you might find completely arbitrary type keys produced by another datastore client and bind those to Java types.
Here's my annotation, and it works.
#Configuration
public class AppMongoConfig {
public #Bean
MongoDbFactory mongoDbFactory() throws Exception {
return new SimpleMongoDbFactory(new Mongo(), "databasename");
}
public #Bean
MongoTemplate mongoTemplate() throws Exception {
//remove _class
MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext());
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(), converter);
return mongoTemplate;
}
}
If you want to disable _class attribute by default, but preserve polymorfism for specified classes, you can explictly define the type of _class (optional) field by configuing:
#Bean
public MongoTemplate mongoTemplate() throws Exception {
Map<Class<?>, String> typeMapperMap = new HashMap<>();
typeMapperMap.put(com.acme.domain.SomeDocument.class, "role");
TypeInformationMapper typeMapper1 = new ConfigurableTypeInformationMapper(typeMapperMap);
MongoTypeMapper typeMapper = new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, Arrays.asList(typeMapper1));
MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext());
converter.setTypeMapper(typeMapper);
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(), converter);
return mongoTemplate;
}
This will preserve _class field (or whatever you want to name in construtor) for only specified entities.
You can also write own TypeInformationMapper for example based on annotations. If you annotate your document by #DocumentType("aliasName") you will keep polymorphism by keeping alias of class.
I have explained briefly it on my blog, but here is some piece of quick code:
https://gist.github.com/athlan/6497c74cc515131e1336
<mongo:mongo host="hostname" port="27017">
<mongo:options
...options...
</mongo:mongo>
<mongo:db-factory dbname="databasename" username="user" password="pass" mongo-ref="mongo"/>
<bean id="mongoTypeMapper" class="org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper">
<constructor-arg name="typeKey"><null/></constructor-arg>
</bean>
<bean id="mongoMappingContext" class="org.springframework.data.mongodb.core.mapping.MongoMappingContext" />
<bean id="mongoConverter" class="org.springframework.data.mongodb.core.convert.MappingMongoConverter">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
<constructor-arg name="mappingContext" ref="mongoMappingContext" />
<property name="typeMapper" ref="mongoTypeMapper"></property>
</bean>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>
<constructor-arg name="mongoConverter" ref="mongoConverter" />
<property name="writeResultChecking" value="EXCEPTION" />
</bean>
While, Mkyong's answer still works, I would like to add my version of solution as few bits are deprecated and may be in the verge of cleanup.
For example : MappingMongoConverter(mongoDbFactory(), new MongoMappingContext()) is deprecated in favor of new MappingMongoConverter(dbRefResolver, new MongoMappingContext()); and SimpleMongoDbFactory(new Mongo(), "databasename"); in favor of new SimpleMongoDbFactory(new MongoClient(), database);.
So, my final working answer without deprecation warnings is :
#Configuration
public class SpringMongoConfig {
#Value("${spring.data.mongodb.database}")
private String database;
#Autowired
private MongoDbFactory mongoDbFactory;
public #Bean MongoDbFactory mongoDBFactory() throws Exception {
return new SimpleMongoDbFactory(new MongoClient(), database);
}
public #Bean MongoTemplate mongoTemplate() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
// Remove _class
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return new MongoTemplate(mongoDBFactory(), converter);
}
}
Hope this helps people who would like to have a clean class with no deprecation warnings.
For Spring Boot 2.3.0.RELEASE it's more easy, just override the method mongoTemplate, it's already has all things you need to set type mapper. See the following example:
#Configuration
#EnableMongoRepositories(
// your package ...
)
public class MongoConfig extends AbstractMongoClientConfiguration {
// .....
#Override
public MongoTemplate mongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
// remove __class field from mongo
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return super.mongoTemplate(databaseFactory, converter);
}
// .....
}
This is my one line solution:
#Bean
public MongoTemplate mongoTemplateFraud() throws UnknownHostException {
MongoTemplate mongoTemplate = new MongoTemplate(getMongoClient(), dbName);
((MappingMongoConverter)mongoTemplate.getConverter()).setTypeMapper(new DefaultMongoTypeMapper(null));//removes _class
return mongoTemplate;
}
I struggled a long time with this problem. I followed the approach from mkyong but when I introduced a LocalDate attribute (any JSR310 class from Java 8) I received the following exception:
org.springframework.core.convert.ConverterNotFoundException:
No converter found capable of converting from type [java.time.LocalDate] to type [java.util.Date]
The corresponding converter org.springframework.format.datetime.standard.DateTimeConverters is part of Spring 4.1 and is referenced in Spring Data MongoDB 1.7. Even if I used newer versions the converter didn't jump in.
The solution was to use the existing MappingMongoConverter and only provide a new DefaultMongoTypeMapper (the code from mkyong is under comment):
#Configuration
#EnableMongoRepositories
class BatchInfrastructureConfig extends AbstractMongoConfiguration
{
#Override
protected String getDatabaseName() {
return "yourdb"
}
#Override
Mongo mongo() throws Exception {
new Mongo()
}
#Bean MongoTemplate mongoTemplate()
{
// overwrite type mapper to get rid of the _class column
// get the converter from the base class instead of creating it
// def converter = new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext())
def converter = mappingMongoConverter()
converter.typeMapper = new DefaultMongoTypeMapper(null)
// create & return template
new MongoTemplate(mongoDbFactory(), converter)
}
To summarize:
extend AbstractMongoConfiguration
annotate with EnableMongoRepositories
in mongoTemplate get converter from base class, this ensures that the type conversion classes are registered
#Configuration
public class MongoConfig {
#Value("${spring.data.mongodb.database}")
private String database;
#Value("${spring.data.mongodb.host}")
private String host;
public #Bean MongoDbFactory mongoDbFactory() throws Exception {
return new SimpleMongoDbFactory(new MongoClient(host), database);
}
public #Bean MongoTemplate mongoTemplate() throws Exception {
MappingMongoConverter converter = new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory()),
new MongoMappingContext());
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(), converter);
return mongoTemplate;
}
}
The correct answer above seems to be using a number of deprecated dependencies. For example if you check the code, it mentions MongoDbFactory which is deprecated in the latest Spring release. If you happen to be using MongoDB with Spring-Data in 2020, this solution seems to be older. For instant results, check this snippet of code. Works 100%.
Just Create a new AppConfig.java file and paste this block of code. You'll see the "_class" property disappearing from the MongoDB document.
package "Your Package Name";
import org.apache.naming.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
#Configuration
public class AppConfig {
#Autowired
MongoDatabaseFactory mongoDbFactory;
#Autowired
MongoMappingContext mongoMappingContext;
#Bean
public MappingMongoConverter mappingMongoConverter() {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return converter;
}
}
I'm using:
package YOUR_PACKAGE;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
#Configuration
public class MongoConfiguration {
#Autowired
private MappingMongoConverter mongoConverter;
#PostConstruct
public void setUpMongoEscapeCharacterAndTypeMapperConversion() {
mongoConverter.setMapKeyDotReplacement("_");
// This will remove _class: key
mongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
}
}
Btw: It is also replacing "." with "_"
you just need to add the #TypeAlias annotation to the class defintion over changing the type mapper
I've tried the solutions above, some of them don't work in combination with auditing, and none seems to set correctly the MongoCustomConversions
A solution that works for me is the following
#Configuration
public class MongoConfig {
#Bean
public MappingMongoConverter mappingMongoConverterWithCustomTypeMapper(
MongoDatabaseFactory factory,
MongoMappingContext context,
MongoCustomConversions conversions) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
mappingConverter.setCustomConversions(conversions);
/**
* replicate the way that Spring
* instantiates a {#link DefaultMongoTypeMapper}
* in {#link MappingMongoConverter#MappingMongoConverter(DbRefResolver, MappingContext)}
*/
CustomMongoTypeMapper customTypeMapper = new CustomMongoTypeMapper(
context,
mappingConverter::getWriteTarget);
mappingConverter.setTypeMapper(customTypeMapper);
return mappingConverter;
}
}
public class CustomMongoTypeMapper extends DefaultMongoTypeMapper {
public CustomMongoTypeMapper(
MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
UnaryOperator<Class<?>> writeTarget) {
super(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext, writeTarget);
}
#Override
public TypeInformation<?> readType(Bson source) {
/**
* do your conversion here, and eventually return
*/
return super.readType(source);
}
}
As an alternative, you could use a BeanPostProcessor to detect the creation of a mappingMongoConverter, and add your converter there.
Something like
public class MappingMongoConverterHook implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("mappingMongoConverter" == beanName) {
((MappingMongoConverter) bean).setTypeMapper(new CustomMongoTypeMapper());
}
return bean;
}
}