Does WebFlux Spring Boot #Transactional annotation work with reactive MongoDB?
I use WebFlux Spring Boot with reactive MongoDB like:
id 'org.springframework.boot' version '2.6.7'
...
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
...
I marked one of my method #Transactional to test. But it seems the annotation does not work for me. If an error occurs inside this method, then it still adds a raw to my mongoDB database.
import org.springframework.transaction.annotation.Transactional;
...
#Transactional
public Mono<Chat> createChat(Chat chat) {
return chatRepository
.save(chat)
.map(
c-> {
if (true) {
throw new RuntimeException();
}
return c;
});
}
Do I miss something or Spring Boot #Transactional annotation does not work with reactive MongoDB?
I use MongoDB v5.0.8
It seems like that Spring Data for reactive MongoDB requires to set explicitly a special bean transactionManager. As soon as I have added this bean to my configuration for the reactive MongoDB, the #Transactional annotation started working. So the example method posted in my question does not add a new raw to the database anymore if an error occurs inside the method.
Here is my configuration with transactionManager bean:
#Configuration
#EnableReactiveMongoRepositories
#AllArgsConstructor
public class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {
private final MongoProperties mongoProperties;
#Override
public MongoClient reactiveMongoClient() {
return MongoClients.create();
}
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory) {
return new ReactiveMongoTransactionManager(reactiveMongoDatabaseFactory);
}
P.S.
It turns out the defining of transactionManager bean is not enough to enable transactions in reactive MongoDB. The very server of MongoDB should be also configured with replication. I followed these steps and it worked for me.
Related
Before all, I'm testing in Postman with this URL: http://localhost:8080/skiing/getSkiing, response is:
[
{}
]
I don't know is this all that I need for Mongo configuration with Spring, if it's no, can someone link me an example with good way how to connect Spring Boot with Mongo. And also, if this is all what I need for mongo configuration, how Spring read this? Where is this called or where Spring Boot actually use this?
spring.data.mongodb.database=tripadvisor
spring.data.mongodb.port=27017
spring.data.mongodb.host=localhost
spring.servlet.multipart.max-file-size=256MB
spring.servlet.multipart.max-request-size=256MB
spring.servlet.multipart.enabled=true
Anyway, my response after trying to read all elements from documents is empty. This is my code for that:
Repo:
#Repository
public interface SkiingRepository extends MongoRepository<Skiing, String> {
}
Service
#Service
public class SkiingServiceImpl implements SkiingService {
#Autowired
private SkiingRepository skiingRepository;
#Override
public List<Skiing> getAllSkiing() {
return skiingRepository.findAll();
}
}
Controller:
#RestController
#RequestMapping("/skiing")
public class SkiingController {
#Autowired
SkiingService skiingService;
#GetMapping(value = "/getSkiing")
public ResponseEntity<?> getAllSkiing() {
List<Skiing> skiingList = skiingService.getAllSkiing();
return new ResponseEntity<Object>(skiingList, HttpStatus.OK);
}
}
There are some basic configurations when you work with Spring and MongoDB:
spring.data.mongodb.uri=
spring.data.mongodb.database=
spring.data.mongodb.username=
spring.data.mongodb.password=
spring.data.mongodb.port=
spring.data.mongodb.host=
The answer for question "Where is this called or where Spring Boot actually use this?" is Spring Data MongoDB - a project of Spring Framework
Please refer Spring Data MongoDB for more information
I recently migrated my spring boot/batch Java application from spring-boot/spring-framework (respectively) 1.x.x/4.x.x to => 2.x.x/5.x.x (2.2.4/5.2.3 to be specific). The problem is something is definitely wrong (in my opinion) with the transaction/entity manager, as when the .saveAll() method is called from the JpaRepository class of my database persistance layer, it jumps into the SpringAOP framework/libarary code and into a infinite loop. I see it returning a "DefaulTransaction" object from a method (invoke()). My application on 1.x.x/4.x.x when it worked, would return the actual ArrayList here of my entities. I am using spring-boot-starter, spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-batch, and hibernate/hibernate-envers/hibernate-entitymanager (also of course many other dependencies, let me know if you would like me to list them).
After some research, I'm finding people are saying that Spring Batch #EnableBatchProcessing annotation sets up a default transaction manager, which if I'm using JPA could be causing issues. Reference:
https://github.com/spring-projects/spring-boot/issues/2363
wilkinsona suggested defining this Bean in my #Configuration class:
#Bean
public BatchConfigurer batchConfigurer(DataSource dataSource, EntityManagerFactory entityManagerFactory) {
return new BasicBatchConfigurer(dataSource, entityManagerFactory);
}
I'm getting an error when I do this because its saying the BasicBatchConfigurer() has protected access. What is the best way to instantiate this?
I also saw some people saying removing the #EnableBatchProcessing annotation fixes the persistance to database issue, but when I remove this, I lose the ability to Autowire my JobBuilderFactory and StepBuilderFactory. Is there a way to remove the annotation and get these objects in my code so I can at-least test if this works? Sorry, I'm not completely a master with Spring Batch/Spring.
In my #Configuration class, I am using the PlatformTransactionManager. I am setting up my JobRepository something like this.:
#Bean
public JobRepository jobRepository(PlatformTransactionManager transactionManager,
#Qualifier("dataSource") DataSource dataSource) throws Exception {
JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
jobRepositoryFactoryBean.setDataSource(dataSource);
jobRepositoryFactoryBean.setTransactionManager(transactionManager);
jobRepositoryFactoryBean.setDatabaseType("POSTGRES");
return jobRepositoryFactoryBean.getObject();
}
I can provide any other information if needed. Another question is - if I was using the same code basically, transaction manager, entity manager etc.. how was old my code working on 1.x.x? Could I have a wrong dependency somewhere in my pom.xml such that my new migrated code is using a wrong method or something from the wrong dependency?
By default, #EnableBatchProcessing configures Spring Batch to use a DataSourceTransactionManager if you provide a DataSource. This transaction manager knows nothing about your JPA context. So if you want to use a JPA repository to save data, you need to configure Spring Batch to use a JpaTransactionManager.
Now in order to provide a custom transaction manager, you need to register a BatchConfigurer and override the getTransactionManager() method, something like:
#Bean
public BatchConfigurer batchConfigurer(DataSource dataSource) {
return new DefaultBatchConfigurer(dataSource) {
#Override
public PlatformTransactionManager getTransactionManager() {
return new JpaTransactionManager();
}
};
}
This is explained in the Configuring A Job section and in the Javadoc of #EnableBatchProcessing.
I have a Spring Boot project, version 1.5.4, with a MongoDb configuration class:
#Configuration
public class MongoConfig {
#Value("${spring.data.mongo.client.uri:mongodb://localhost:27017/database}")
private String mongoURI;
#Bean
public MongoDbFactory mongoFactory() throws UnknownHostException{
return new SimpleMongoDbFactory(new MongoClientURI(mongoURI));
}
#Bean
public MongoTemplate mongoTemplate() throws UnknownHostException, MongoException{
return new MongoTemplate(mongoFactory());
}
}
In my integration test i want use Embedded Mongo (https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo).
The problem is that the MongoDb configuration class start before the initialitation of Embedded mongo and try to connect to the database, so my test fail. If i remove the MongoConfig class, all test work well.
How can i exclude it only in my test execution?
Exclude the MongoDB autoconfiguration by using below annotation over your test class.
#EnableAutoConfiguration(exclude={MongoAutoConfiguration.class,
MongoDataAutoConfiguration.class})
Then in the same path as your test class create a configuration class and define your mongo bean over there. This will get picked up during application start up
**#Configuration
public class MockConfigurations {
#Bean
#Primary
public MongoTemplate getMongoTemplate() {
//define your bean
return mongoTemplate;
}
}**
Please refer the answers here. It has two ways of excluding configurations.
Spring boot: apply #Configuration to certain package only
Update 1:
Alternatively, the most efficient way that I can think of is to use Spring profiles and load the profile for the tests
Define your TestConfiguration class and import it your test class.
#RunWith(SpringRunner.class)
#SpringBootTest
#Import(MyTestConfiguration.class)
public class MyTests {
#Test
public void exampleTest() {
...
}
}
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-detecting-config
Update 2:
For EmbeddedMongoAutoConfiguration please refer the detailed answers here.
How do you configure Embedded MongDB for integration testing in a Spring Boot application?
I solved it with this configuration on my test class:
#RunWith(SpringRunner.class)
#ComponentScan({"it.app.server.dal","it.app.server.listener"})
#DataMongoTest() //mongoDB
public class ListenerTests {
...
}
The annotation #DataMongoTest() load my Embbedded MongoDb and with #ComponentScan i can just load the services and repositories wich i need in my test.
I wrote a Test for a MongoRepository in Spring Boot, and the test works fine. The only problem is that when the test is over, I want a rollback, so that there will be no change in the database caused by the test.
// package...
// imports...
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MetistrafficApplication.class)
#Rollback(true)
public class AppRepositoryTests {
#Autowired
private AppRepository appRepository;
#Test
public void insertTest() {
App app = new App("test");
App appInserted = appRepository.save(app);
assertThat(appInserted.getName(), equalTo(app.getName()));
}
}
I put #Transactional before #Rollback, but get this error:
java.lang.illegalstateexception:Failed to retrieve PlatformTransactionManager for #Transactional test for test context
When I searched for the error, I couldn't find any code with MongoRepository. So, how can I solve this?
EDIT: After adding #Transactional("PlatformTransactionManager"), the error I get is changed to this:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'PlatformTransactionManager' is defined: No matching PlatformTransactionManager bean found for qualifier 'PlatformTransactionManager' - neither qualifier match nor bean name match!
As far as I know, there isn't an implementation of Spring's TransactionManager for MongoDB since it is not transactional in the ACID sense. So no, you cannot use #Transactional annotations with MongoDB and you'll have to do all the cleanup manually or else use DBUnit and add your own extensions for MongoDB.
EDIT:
As Petter mentioned in his answer, starting with MongoDB 4.0, MongoDB has support for ACID transactions and you can find the official SpringData examples on GitHub and also have the feature's release post in Spring's developer blog
Now you can use #Transactional with mongo. Take a look at this example: https://www.baeldung.com/spring-data-mongodb-transactions
You'll need mongo 4.0. Also need to enable mongo replication (mongod --replSet rs0)
Then you'll need to add this bean to your spring application
#Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
This is enough to use #Transactional in your code.
I guess you use try catch block. Its better if you can avoid try catch. Anyway if you need to rollback you can do it like this.
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
I writing application using spring-boot-starter-jdbc (v1.3.0).
The problem that I met: Instance of BeanPropertyRowMapper fails as it cannot convert from java.sql.Timestamp to java.time.LocalDateTime.
In order to copy this problem, I implemented
org.springframework.core.convert.converter.Converter for these types.
public class TimeStampToLocalDateTimeConverter implements Converter<Timestamp, LocalDateTime> {
#Override
public LocalDateTime convert(Timestamp s) {
return s.toLocalDateTime();
}
}
My question is: How do I make available TimeStampToLocalDateTimeConverter for BeanPropertyRowMapper.
More general question, how do I register my converters, in order to make them available system wide?
The following code bring us to NullPointerException on initialization stage:
private Set<Converter> getConverters() {
Set<Converter> converters = new HashSet<Converter>();
converters.add(new TimeStampToLocalDateTimeConverter());
converters.add(new LocalDateTimeToTimestampConverter());
return converters;
}
#Bean(name="conversionService")
public ConversionService getConversionService() {
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
bean.setConverters(getConverters());
bean.afterPropertiesSet();
return bean.getObject();
}
Thank you.
All custom conversion service has to be registered with the FormatterRegistry. Try creating a new configuration and register the conversion service by implementing the WebMvcConfigurer
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new TimeStampToLocalDateTimeConverter());
}
}
Hope this works.
I'll copy my answer from https://stackoverflow.com/a/72781591/140707 since I think the two questions are similar (so the answer applies to both).
Existing answers didn't work for me:
Customizing via WebMvcConfigurerAdapter.addFormatters (or simply annotating the converter with #Component) only works in the WebMvc context and I want my custom converter to be available everywhere, including #Value injections on any bean.
Defining a ConversionService bean (via ConversionServiceFactoryBean #Bean or #Component) causes Spring Boot to replace the default ApplicationConversionService on the SpringApplication bean factory with the custom bean you've defined, which will probably be based on DefaultConversionService (in AbstractApplicationContext.finishBeanFactoryInitialization). The problem is that Spring Boot adds some handy converters such as StringToDurationConverter to the standard set in DefaultConversionService, so by replacing it you lose those conversions. This may not be an issue for you if you don't use them, but it means that solution won't work for everyone.
I created the following #Configuration class which did the trick for me. It basically adds custom converters to the ConversionService instance used by Environment (which is then passed on to BeanFactory). This maintains as much backwards compatibility as possible while still adding your custom converter into the conversion services in use.
#Configuration
public class ConversionServiceConfiguration {
#Autowired
private ConfigurableEnvironment environment;
#PostConstruct
public void addCustomConverters() {
ConfigurableConversionService conversionService = environment.getConversionService();
conversionService.addConverter(new MyCustomConverter());
}
}
Obviously you can autowire a list of custom converters into this configuration class and loop over them to add them to the conversion service instead of the hard-coded way of doing it above, if you want the process to be more automatic.
To make sure this configuration class gets run before any beans are instantiated that might require the converter to have been added to the ConversionService, add it as a primary source in your spring application's run() call:
#SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(new Class<?>[] { MySpringBootApplication.class, ConversionServiceConfiguration.class }, args);
}
}
If you don't do this, it might work, or not, depending on the order in which your classes end up in the Spring Boot JAR, which determines the order in which they are scanned. (I found this out the hard way: it worked when compiling locally with an Oracle JDK, but not on our CI server which was using a Azul Zulu JDK.)
Note that for this to work in #WebMvcTests, I had to also combine this configuration class along with my Spring Boot application class into a #ContextConfiguration:
#WebMvcTest(controllers = MyController.class)
#ContextConfiguration(classes = { MySpringBootApplication.class, ConversionServiceConfiguration.class })
#TestPropertySource(properties = { /* ... properties to inject into beans, possibly using your custom converter ... */ })
class MyControllerTest {
// ...
}
I suggest to use #Autowired and the related dependency injection mechanism of spring to use a single ConversionService instance throughout your application. The ConversionService will be instantiated within the configuration.
All Converters to be available application wide receive an annotation (e.g. #AutoRegistered). On application start a #Component FormatterRegistrar (Type name itself is a bit misleading, yes it is "...Registrar" as it does the registering. And #Component as it is fully spring managed and requires dependency injection) will receive #AutoRegistered List of all annotated Converters.
See this thread for concrete implementation details. We use this mechanism within our project and it works out like a charm.
org.springframework.web.servlet.config.annotation.WebMvcConfigurer or any on its implementation is one stop place for any kind of customization in spring boot project. It prvoides various methods, for your Converter requirement.
Just create a new Converter by extending org.springframework.core.convert.converter.Converter<S, T>. Then register it with Spring by your class overriding method org.springframework.web.servlet.config.annotation.WebMvcConfigurer.addFormatters(FormatterRegistry)
Note there are Other types of Converter also which basically starts from ConditionalConverter.
Trying adding
#Converter(autoApply = true)
Its needs to be placed over the convertor class. This works for me in case of Convertor needed for Localdate for interacting to DB.
#Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {
#Override
public Date convertToDatabaseColumn(LocalDate locDate) {
return (locDate == null ? null : Date.valueOf(locDate));
}
#Override
public LocalDate convertToEntityAttribute(Date sqlDate) {
return (sqlDate == null ? null : sqlDate.toLocalDate());
}
}
This is now applied automatically while interacting with DB.