I have a Spring Batch application(Spring Boot 2.3.5.RELEASE) that uses a JpaRepository to insert some custom log messages into a database as Spring Batch is processing. This is separate from the out of the box Spring Batch tables. Seems that when I throw an exception from my ItemProcessorAdapter, it is caught by the ItemProcessListener onProcessError() method. In this method I am performing a JpaRepository save() and flush(). No errors are logged, but once I leave this method the JpaRepository does a rollback.
Is this normal behavior? How can I get around it?
When using JpaRepository, is there a way to set a #Transactional(noRollbackFor = {xxxException.class})? I tried this and it seemed to have no effect.
Sample code snippet is below.
#Configuration
public class BatchJobConfiguration {
//Omitted for clarity....
#Bean
#StepScope
public CompositeItemProcessor<Decision,Decision> itemProcessor() {
CompositeItemProcessor<Decision,Decision> itemProcessor = new CompositeItemProcessor<>();
itemProcessor.setDelegates(Arrays.asList(
decisionValidatingItemProcessor(),
myItemProcessor(null)
));
return itemProcessor;
} // end itemProcessor()
#Bean
public BeanValidatingItemProcessor<Decision> decisionValidatingItemProcessor() {
BeanValidatingItemProcessor<Decision> beanValidatingItemProcessor = new BeanValidatingItemProcessor<>();
beanValidatingItemProcessor.setFilter(true);
return beanValidatingItemProcessor;
} // end decisionValidatingItemProcessor()
#Bean
public ItemProcessorAdapter<Decision,Decision> myItemProcessor(DecisionProcessingService service) {
ItemProcessorAdapter<Decision,Decision> adapter = new ItemProcessorAdapter<>();
adapter.setTargetObject(service);
adapter.setTargetMethod("processDecision");
return adapter;
}
#Bean
#StepScope
public DecisionItemProcessListener decisionItemProcessListener() {
return new DecisionItemProcessListener(mpJpaRepository);
}
}
#Service
public class DecisionProcessingService {
public Decision processDecision(Decision decision) throws BatchException {
....
throw new BatchException("An error occurred");
}
}
public class DecisionItemProcessListener implements ItemProcessListener<Decision,Decision> {
private MyJpaRepository mpJpaRepository;
public DecisionItemProcessListener(MyJpaRepository mpJpaRepository) {
this.mpJpaRepository = mpJpaRepository;
}
....
#Override
public void onProcessError(Decision decision, Exception e) {
MyEntityObject obj = MyEntityObject.builder()
.msg(e.getMessage())
.build();
mpJpaRepository.save(obj);
mpJpaRepository.flush();
// after this, the insert above is rolled back.
} // end onProcessError()
}
The callback you are using here ItemProcessListener#onProcessError is called with-in a transaction (driven by Spring Batch) that is going to be rolled-back due to the exception thrown by the item processor.
If you want to save data in that method, you need to use a new transaction (use the REQUIRES_NEW propagation).
EDIT: I shared a minimal complete example here: https://github.com/benas/spring-batch-lab/tree/master/issues/so64913980.
Related
I tryng to perform an an unpdate within a UnitTest method with org.springframework.transaction.annotation.Transactional annotation, but it seems that the update doesn't work.
If I remove #Transactional annotation on the method, the update works succesfully but it is visible for all others tests too.
Could you please indicate to me where I'm wrong?
I need the update effective only within the method with the #Transactional annotation and not visible for all others methods.
I'm using
Srping boot v 2.6.6. to start the application. I use JPA and Oracle Data Base.
This is my repository class where I use native query.
#Repository
#Transactional
public interface EsercentiRepository extends JpaRepository<EsercentiEntity, Long> {
// Update
#Modifying
#Query(value="update esercenti set sslfl=:sslfl where id_conv=:idConv", nativeQuery=true)
public void updateSslFlagByIdViaQuery(#Param("idConv") long idConv, #Param("sslfl") String sslfl);
// select
#Query(value="select id_conv,c_code,vendor_id,pos_id,abi_code,funzioni,ds,pos_id_sia,rifmer3d,sslfl "
+ " from esercenti where id_conv=:idConv", nativeQuery=true)
public EsercentiEntity getEsercentiByIdConvViaQuery(#Param("idConv") long idConv);
}
This is my Service class.
#Service
#Transactional
public class EsercentiServices implements IEsercenti {
#Autowired
private EsercentiRepository esercentiRepository;
#Override
public void updateSslFlagByIdViaQuery(long idConv, String sslfl) throws Exception {
esercentiRepository.updateSslFlagByIdViaQuery(idConv, sslfl);
}
#Override
public EsercentiEntity getEsercentiByIdConvViaQuery(long idConv) throws Exception {
return esercentiRepository.getEsercentiByIdConvViaQuery(idConv);
}
}
And this is my SpringBootTest class located in the 'test' directory where I use Junit 5 to perform Functional tests.
#EnableTransactionManagement
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#TestMethodOrder(MethodOrderer.OrderAnnotation.class)
#ExtendWith(SpringExtension.class)
#SpringBootTest
class TestGpay extends BaseServiceTester {
#Autowired
private IEsercenti esercentiServices;
[..]
#Test
#Transactional
public void TestDirectAuthGpayWithPayload(TestInfo testInfo) {
try {
// [..... Some codes]
EsercentiEntity ese = esercentiServices.getEsercentiByIdConvViaQuery(7826L);
esercentiServices.updateSslFlagByIdViaQuery(7826L, "Y");
EsercentiEntity ese2 = esercentiServices.getEsercentiByIdConvViaQuery(7826L);
// Send the http request. I expect to find the data changed on DB as per above update, but it is not.
WebUtils.HttpResponse response = netsJsonClient(endPoint, "POST", jsonObjectRequest.toString(), merId, merIdKsig);
// If I cancel the #Transactoinal annotation on the method level, the update is ok,
// but it is a global update and not only related to this database session.
} catch (Exception e) {
e.printStackTrace();
closeDriver(driver);
fail("Exception on test case " + testInfo.getDisplayName() + " Full Error:" + e);
}
}
Thanks in advance.
consider following model:
#Service
public class TripServiceImpl implements TripService {
#Autowired
private EventService eventService;
#Autowired
private CalendarService calendarService;
#Transactional
public void processTrip(TripDto dto) {
EventDto event = eventService.findByTripId(dto).orElseGet(() -> eventService.createByTrip(dto));
dto.getMembers().forEach(memberCode -> {
try {
calendarService.createReminder(event.getId(), memberCode);
} catch (Exception ex) {}
});
// rest logic
}
}
This is how createReminder method looks like:
#Transactional(propagation = Propagation.REQUIRES_NEW)
public void createReminder(Long eventId, String memberCode) {
Optiona<Event> eventRepository.findById(eventId); // this returns Optional.empty()
...
}
In our trip service we find or create event for given trip if no exists, and add reminders to calendars of members from that trip. Method for creating reminders we need as new transaction to ensure, if that method failed with exception, don't rollback all changes. This transaction propagation unfortunately can't find entity which was saved in parent transactional method. Can you tell me how to fix it? Thank you.
I want to read text data fixtures (CSV files) at the start on my application and put it in my database.
For that, I have created a PopulationService with an initialization method (#PostConstruct annotation).
I also want them to be executed in a single transaction, and hence I added #Transactional on the same method.
However, the #Transactional seems to be ignored :
The transaction is started / stopped at my low level DAO methods.
Do I need to manage the transaction manually then ?
Quote from legacy (closed) Spring forum:
In the #PostConstruct (as with the afterPropertiesSet from the InitializingBean interface) there is no way to ensure that all the post processing is already done, so (indeed) there can be no Transactions. The only way to ensure that that is working is by using a TransactionTemplate.
So if you would like something in your #PostConstruct to be executed within transaction you have to do something like this:
#Service("something")
public class Something {
#Autowired
#Qualifier("transactionManager")
protected PlatformTransactionManager txManager;
#PostConstruct
private void init(){
TransactionTemplate tmpl = new TransactionTemplate(txManager);
tmpl.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
//PUT YOUR CALL TO SERVICE HERE
}
});
}
}
I think #PostConstruct only ensures the preprocessing/injection of your current class is finished. It does not mean that the initialization of the whole application context is finished.
However you can use the spring event system to receive an event when the initialization of the application context is finished:
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public void onApplicationEvent(ContextRefreshedEvent event) {
// do startup code ..
}
}
See the documentation section Standard and Custom Events for more details.
As an update, from Spring 4.2 the #EventListener annotation allows a cleaner implementation:
#Service
public class InitService {
#Autowired
MyDAO myDAO;
#EventListener(ContextRefreshedEvent.class)
public void onApplicationEvent(ContextRefreshedEvent event) {
event.getApplicationContext().getBean(InitService.class).initialize();
}
#Transactional
public void initialize() {
// use the DAO
}
}
Inject self and call through it the #Transactional method
public class AccountService {
#Autowired
private AccountService self;
#Transactional
public void resetAllAccounts(){
//...
}
#PostConstruct
private void init(){
self.resetAllAccounts();
}
}
For older Spring versions which do not support self-injection, inject BeanFactory and get self as beanFactory.getBean(AccountService.class)
EDIT
It looks like that since this solution has been posted 1.5 years ago developers are still under impression that if a method,
annotated with #Transactional, is called from a #PostContruct-annotated method invoked upon the Bean initialization, it won't be actually executed inside of Spring Transaction, and awkward (obsolete?) solutions get discussed and accepted instead of this very simple and straightforward one and the latter even gets downvoted.
The Doubting Thomases :) are welcome to check out an example Spring Boot application at GitHub which implements the described above solution.
What actually causes, IMHO, the confusion: the call to #Transactional method should be done through a proxied version of a Bean where such method is defined.
When a #Transactional method is called from another Bean, that another Bean usually injects this one and invokes its proxied (e.g. through #Autowired) version of it, and everything is fine.
When a #Transactional method is called from the same Bean directly, through usual Java call, the Spring AOP/Proxy machinery is not involved and the method is not executed inside of Transaction.
When, as in the suggested solution, a #Transactional method is called from the same Bean through self-injected proxy (self field), the situation is basically equivalent to a case 1.
#Platon Serbin's answer didn't work for me. So I kept searching and found the following answer that saved my life. :D
The answer is here No Session Hibernate in #PostConstruct, which I took the liberty to transcribe:
#Service("myService")
#Transactional(readOnly = true)
public class MyServiceImpl implements MyService {
#Autowired
private MyDao myDao;
private CacheList cacheList;
#Autowired
public void MyServiceImpl(PlatformTransactionManager transactionManager) {
this.cacheList = (CacheList) new TransactionTemplate(transactionManager).execute(new TransactionCallback(){
#Override
public Object doInTransaction(TransactionStatus transactionStatus) {
CacheList cacheList = new CacheList();
cacheList.reloadCache(MyServiceImpl.this.myDao.getAllFromServer());
return cacheList;
}
});
}
The transaction part of spring might not be initialized completely at #PostConstruct.
Use a listener to the ContextRefreshedEvent event to ensure, that transactions are available:
#Component
public class YourService
implements ApplicationListener<ContextRefreshedEvent> // <= ensure correct timing!
{
private final YourRepo repo;
public YourService (YourRepo repo) {this.repo = repo;}
#Transactional // <= ensure transaction!
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
repo.doSomethingWithinTransaction();
}
}
Using transactionOperations.execute() in #PostConstruct or in #NoTransaction method both works
#Service
public class ConfigurationService implements ApplicationContextAware {
private static final Logger LOG = LoggerFactory.getLogger(ConfigurationService.class);
private ConfigDAO dao;
private TransactionOperations transactionOperations;
#Autowired
public void setTransactionOperations(TransactionOperations transactionOperations) {
this.transactionOperations = transactionOperations;
}
#Autowired
public void setConfigurationDAO(ConfigDAO dao) {
this.dao = dao;
}
#PostConstruct
public void postConstruct() {
try { transactionOperations.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
ResultSet<Config> configs = dao.queryAll();
}
});
}
catch (Exception ex)
{
LOG.trace(ex.getMessage(), ex);
}
}
#NoTransaction
public void saveConfiguration(final Configuration configuration, final boolean applicationSpecific) {
String name = configuration.getName();
Configuration original = transactionOperations.execute((TransactionCallback<Configuration>) status ->
getConfiguration(configuration.getName(), applicationSpecific, null));
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
}
}
trying to find what went wrong with my code which worked fine until i moved to JTAtransactionManager it is having issue saving the record in database but fetching the record working fine, below is my sample TransactionConfig class and service class method.
#Configuration
#ComponentScan
#EnableTransactionManagement
public class TransactionConfig {
#Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
//userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
#Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
#Bean(name = "transactionManager")
#DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
TransactionManager atomikosTransactionManager = atomikosTransactionManager();
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
}
}
---Employee Service Class Method---
#Transactional
public void appExample() {
try {
Employee emp = new Employee();
emp.setFirstName("Veer");
emp.setLastName("kumar");
empRepo.save(emp);
} catch (Exception e) {
log.error(e);
}
}
I think issue is with empRepo.save() method call. This call is not committing any changes to database as you are using #Transactional for transaction management.
Please try with empRepo.saveAndFlush() which will immediately flush the data into database. You can refer answer Difference between save and saveAndFlush in Spring data jpa
As of now, I'm able to connect to Cassandra via the following code:
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Session;
public static Session connection() {
Cluster cluster = Cluster.builder()
.addContactPoints("IP1", "IP2")
.withCredentials("user", "password")
.withSSL()
.build();
Session session = null;
try {
session = cluster.connect("database_name");
session.execute("CQL Statement");
} finally {
IOUtils.closeQuietly(session);
IOUtils.closeQuietly(cluster);
}
return session;
}
The problem is that I need to write to Cassandra in a Spring Batch project. Most of the starter kits seem to use a JdbcBatchItemWriter to write to a mySQL database from a chunk. Is this possible? It seems that a JdbcBatchItemWriter cannot connect to a Cassandra database.
The current itemwriter code is below:
#Bean
public JdbcBatchItemWriter<Person> writer() {
JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>();
writer.setItemSqlParameterSourceProvider(new
BeanPropertyItemSqlParameterSourceProvider<Person>());
writer.setSql("INSERT INTO people (first_name, last_name) VALUES
(:firstName, :lastName)");
writer.setDataSource(dataSource);
return writer;
}
Spring Data Cassandra provides repository abstractions for Cassandra that you should be able to use in conjunction with the RepositoryItemWriter to write to Cassandra from Spring Batch.
It is possible to extend Spring Batch to support Cassandra by customising ItemReader and ItemWriter.
ItemWriter example:
public class CassandraBatchItemWriter<Company> implements ItemWriter<Company>, InitializingBean {
protected static final Log logger = LogFactory.getLog(CassandraBatchItemWriter.class);
private final Class<Company> aClass;
#Autowired
private CassandraTemplate cassandraTemplate;
#Override
public void afterPropertiesSet() throws Exception { }
public CassandraBatchItemWriter(final Class<Company> aClass) {
this.aClass = aClass;
}
#Override
public void write(final List<? extends Company> items) throws Exception {
logger.debug("Write operations is performing, the size is {}" + items.size());
if (!items.isEmpty()) {
logger.info("Deleting in a batch performing...");
cassandraTemplate.deleteAll(aClass);
logger.info("Inserting in a batch performing...");
cassandraTemplate.insert(items);
}
logger.debug("Items is null...");
}
}
Then you can inject it as a #Bean through #Configuration
#Bean
public ItemWriter<Company> writer(final DataSource dataSource) {
final CassandraBatchItemWriter<Company> writer = new CassandraBatchItemWriter<Company>(Company.class);
return writer;
}
Full source code can be found in Github repo: Spring-Batch-with-Cassandra