I am doing a work on arango db. Dose arangodb-spring-boot-starter has the transition and rollback support
I have tried #Transition annotation in the custom repo layer. added a error by custom error, the service has a functionality to create multiple document. I was expecting the rollback which is not happened.
This is the arango repository code.
public interface RelationRepository extends ArangoRepository<Relation, String> {
#Transactional
#Query("insert { _from: #from, _to: #to } into #collection return NEW")
Set<Relation> createEdge(#Param("from") String from,#Param("to") String to;}
This is the code snippet for the service
#Service
public class RelationService {
#Autowired
private RelationRepository relationRepository;
private final Logger log = LoggerFactory.getLogger(RelationService.class);
#Transactional(rollbackFor = SQLException.class)
public HashMap<String,String> demoRelation() {
relationRepository.createEdge("vertex1/121286","vertex2/167744","relation",
Instant.now().toEpochMilli(),Long.MAX_VALUE);
if(true)
throw new SQLException("custom exception to check rollback");
return null;
}
}
I was expecting the rollback, instead it is creating records
Related
I am working on a spring-batch, where after reader and processor, writer is responsible to populate data to DB. Writer is calling Service which internally calls DAO layer. In method insertToDB() if some exception occurs the transaction is not being rolled back. PSB my code.
public class MyWriter{
#Autowired
private MyService myService;
#Override
public void write(List<? extends MyBO> list) {
try{
for(MyBO bo: list){
myService.insert(bo);
}
}
catch(Exception e){
log.error("Cant write to DB")
}
}
public class MyService{
#Autowired
private TableOneDAO tableOneDao;
#Autowired
private TableTwoDAO tableTwoDAO;
#Autowired
private TableThreeDAO tableThreeDAO;
public void insert(MyBO bo){
try{
// do other stuff of processing bo and create entity
MyEntityTableOne myentity1 = getEntityT1(bo);
MyEntityTableTwo myentity2 = getEntityT2(bo);
MyEntityTableThree myentity3 = getEntityT3(bo);
insertToDB(myEntity1,myEntity2,myEntity3);
}
catch(Exception e){
log.error("Error occured.");
throw new MyException("Error Blah blah occured");
}
}
#Transactional(value = "txn1")
public void insertToDB(MyEntityTableOne entity1, MyEntityTableTwo entity2, MyEntityTableThree entity3) {
try{
tableOneDao.insert(entity1);
tableTwoDAO.insert(entity2);
tableThreeDAO.insert(entity3);
}
catch(Exception e){
log.error("Error occured during insert to DB");
throw new MyException("Error Blah blah occured during DB insert");
}
}
The code goes to the catch block, but doesn't rollback records. If some error occurs during insert of Table2 then entry for Table1 is not rolled-back. And if occurs during table3 insertion then table1 and table2 records are not rolled-back.
If I move the #Transactional annotation to insert() method it works fine. What is root cause of this issue. What I have to do if I want to have transaction only on insertToDB() method.
I am trying to make it simple: To support #Transactional spring wraps the implementing class into a so called proxy and surrounds the method call / class with the transactional logic.
Now you are calling the #Transactional annotated method within the same class. Therefore the proxy is not invoked and the transactional does not work. When moving the annotation to your insert method you are invoking the method from outside of the class which means you invoke the method against the proxy.
Thats a limitation of Spring AOP (?) I think.
You can do something like following to achieve what you want:
public class MyService{
#Ressource
private MyService self;
...
self.insertToDB(myEntity1,myEntity2,myEntity3)
Your item writer will be already called in a transaction driven by Spring Batch and that you can configure at the step level by providing the transaction manager and transaction attributes. So there is no need to use #Transactional in the downstream service used by the writer.
You need to remove that annotation from MyService and it should work as expected.
I have a ProcessRecon usecase class with a single method named execute. It saves an entity Reconciliation using paymentRepository.saveRecon and calls a web service as part of acknowledgement using paymentRepository.sendReconAck.
Now there's a chance that this external web service might fail in which case I want to rollback the changes i.e. the saved entity. Since I am using Unirest, it throws UnirestException which is a checked exception.
There are no errors on the console but this will probably be helpful [UPDATED].
2020-08-20 17:21:42,035 DEBUG [http-nio-7012-exec-6] org.springframework.transaction.support.AbstractPlatformTransactionManager: Creating new transaction with name [com.eyantra.payment.features.payment.domain.usecases.ProcessRecon.execute]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-com.mashape.unirest.http.exceptions.UnirestException
...
2020-08-20 17:21:44,041 DEBUG [http-nio-7012-exec-2] org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction rollback
2020-08-20 17:21:44,044 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Rolling back JPA transaction on EntityManager [SessionImpl(621663440<open>)]
2020-08-20 17:21:44,059 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Not closing pre-bound JPA EntityManager after transaction
2020-08-20 17:22:40,020 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor: Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
What I see at the moment is that entity gets pushed to database even if there's a UnirestException. But I expect no data be saved to database.
I am using Spring Boot 2.3.3 with MySQL 5.7. This is the code I have for it.
ProcessRecon.java
#Usecase // this custom annotation is derived from #service
public class ProcessRecon {
private final PaymentRepository paymentRepository;
#Autowired
public ProcessRecon(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
#Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
PaymentDetails paymentDetails = paymentRepository.getByReqId(reconciliation.getReqId());
if (paymentDetails == null)
throw new EntityNotFoundException(ExceptionMessages.PAYMENT_DETAILS_NOT_FOUND);
reconciliation.setPaymentDetails(paymentDetails);
Long transId = null;
if (paymentDetails.getImmediateResponse() != null)
transId = paymentDetails.getImmediateResponse().getTransId();
if (transId != null)
reconciliation.setTransId(transId);
if (reconciliation.getTransId() == null)
throw new ValidationException("transId should be provided in Reconciliation if there is no immediate" +
" response for a particular reqId!");
// THIS GETS SAVED
Reconciliation savedRecon = paymentRepository.saveRecon(reconciliation);
paymentDetails.setReconciliation(savedRecon);
// IF THROWS SOME ERROR, ROLLBACK
paymentRepository.sendReconAck(reconciliation);
return savedRecon;
}
}
PaymentRepositoryImpl.java
#CleanRepository
public class PaymentRepositoryImpl implements PaymentRepository {
#Override
public String sendReconAck(final Reconciliation recon) throws UnirestException {
// Acknowledge OP
return sendAck(recon.getRequestType(), recon.getTransId());
}
String sendAck(final String requestType, final Long transId) throws UnirestException {
// TODO: Check if restTemplate can work with characters (requestType)
final Map<String, Object> queryParams = new HashMap<String, Object>();
queryParams.put("transId", transId);
queryParams.put("requestType", requestType);
logger.debug("{}", queryParams);
final HttpResponse<String> result = Unirest.get(makeAckUrl()).queryString(queryParams).asString();
logger.debug("Output of ack with queryParams {} is {}", queryParams, result.getBody());
return result.getBody();
}
#Override
public Reconciliation saveRecon(final Reconciliation recon) {
try {
return reconDS.save(recon);
}
catch (DataIntegrityViolationException ex) {
throw new EntityExistsException(ExceptionMessages.CONSTRAINT_VIOLATION);
}
}
}
ReconciliationDatasource.java
#Datasource // extends from #Repository
public interface ReconciliationDatasource extends JpaRepository<Reconciliation, Long> {
List<Reconciliation> findByPaymentDetails_User_Id(Long userId);
}
To make annotations work you have to use interfaces instead of classes for dependency injection.
interface ProcessRecon {
Reconciliation execute(final Reconciliation reconciliation)
throws UnirestException;
}
Then
#Usecase
public class ProcessReconImpl implements ProcessRecon {
private final PaymentRepository paymentRepository;
#Autowired
public ProcessReconImpl(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
#Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
//method implementation...
}
}
Usage
#Autowired
ProcessRecon processRecon;
public void executeServiceMethod(Reconciliation reconciliation) {
processRecon.execute(reconciliation)
}
This way you have got proxy of ProcessReconImpl with provided by annotations additional functionality.
I assumed the default engine for the tables would be InnoDB but to my surpise, the tables were created using MyISAM engine which doesn't support transactions.
I resolved the problem by using the below property as suggested here
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
instead of
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
That was the only change required. Thanks!
In a spring boot application, I have multiple implementations of an interface, every one of which accesses to the database using a JPA repository.
#Autowired
List<? extends CrawlerService> crawlers;
This is the repository:
#Repository
public interface HotelRepository extends JpaRepository<Hotel, Long> {
public Optional<Hotel> findByTextAndSource(String text, String source);
}
And the implementations look like this:
#Service
public class MyService implements CrawlerService {
#Autowired
DocumentService documentService;
#Autowired
HotelRepository hotelRepository;
private Logger logger = LoggerFactory.getLogger("MyService");
private Set<String> visitedLinks = new HashSet<>();
#Override
public void start() {
execute("https://www.example.com/");
}
public void execute(String url) {
if(visitedLinks.contains(url))
return;
try {
logger.info("Connecting '{}' ...", url);
visitedLinks.add(url);
Document document = Jsoup.connect(url).get();
String text = documentService.getText(document);
// this is where I access to the database
if(hotelRepository.findByTextAndSource(text, "EXAMPLE").isPresent()) {
return;
}
Hotel hotel = new Hotel();
hotel.setUrl(url);
hotel.setText(text);
hotel.setSource("EXAMPLE");
hotelRepository.save(hotel);
// find other links
findLinks(document, url).forEach(this::execute);
} catch (IOException e) {
logger.error("Connection error for {}", url);
}
}
}
Now I'm using parallel stream to run each one of them in parallel:
crawlers.stream().parallel().forEach(CrawlerService::start);
They continue to work until the point that they want to access the database through the JPA repository. Then all of them stop, except the one assigned to the main thread. This is observed using the output of my logger:
2018-11-01 21:42:38.408 INFO 7641 --- [ main] MyService ...
Only this one continues to run.
I want all of them to continue to work in parallel. How can I solve this problem?
It is worth mentioning that I'm using MySQL and Spring Boot 2 with default settings for connection pool.
I have a question about xml-free configuration of Spring. Unfortunately it does not roll back my DB changes even if I mark a corresponding method with #Transactional annotation.
First of all I have a controller, which calls a class marked with #Transactional.
#RequestMapping(value="/Device", method=RequestMethod.POST, produces={"application/json"})
#ResponseStatus(HttpStatus.CREATED)
public #ResponseBody List<Device> addDevice(
#RequestBody Device deviceToAdd) {
List<Device> devices= deviceManager.addDevice(deviceToAdd);
return devices;
}
All controllers have an ExceptionResolver that catches DuplicateKeyException:
#ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<Result> handleBindException(DuplicateKeyException e) {
log.error(e.getMessage(), e);
Result result = new Result("this object already exists");
return new Response(result);
}
Here is my main transactional class which calls two DAO classes. The second one causes DuplicateKeyException, but the results of the first one are not deleted from DB.
#Transactional(propagation = Propagation.REQUIRED, rollbackFor=Exception.class)
public class DeviceManager {
#Autowired
DaoClass1 daoClass1;
#Autowired
DaoClass2 daoClass2;
public List<Device> addDevice(Device device){
Pocket pocket = daoClass1.addPocket(new Pocket());<--is not rolled back after Exception
device.setPocket(pocket);
List<Devices> addedDevices = daoClass2.addDevice(device); <--- causes exception
return devices;
}
I tried also to use #Transactional annotation both for the classes daoClass1 and daoClass2, but it did not change anything.
What could I have done wrong?
I have two routes in Apache Camel. I am using Spring 3 and JPA annotations through Hibernate.
I have multiple data sources and have configured Apache Camel to use one default TransactionManager through SpringTransactionPolicy.
I save the data using DAO and can see the data within the test class by actually finding data. I then use the same DAO within Apache Camel Processor to find the same data, but the data is not found. I don't see why within the test class the data can be found and as soon as I try to look in the Apache Camel custom processor the data cannot be found.
Below is the sample code:
public class Config {
#Autowired
private TransactionManagerConfig transactionManagerConfig;
#Bean
public SpringTransactionPolicy defaultTransactionPolicy() throws Exception {
SpringTransactionPolicy transactionPolicy = new SpringTransactionPolicy();
transactionPolicy.setTransactionManager((PlatformTransactionManager) transactionManagerConfig.transactionManager());
return transactionPolicy;
}
}
public class CustomRouter extends SpringRouteBuilder {
public void configure() throws Exception {
ApplicationContext applicationContext = getApplicationContext();
from("someSource")).transacted().unmarshal().jaxb("com.sample.processor.entities").convertBodyTo(Entity.class).to("direct:x")
.end();
from("direct:x").transacted().process((CustomProcessor) applicationContext.getBean("customProcessor")).to(someSource);
}
}
public class CustomProcessor implements org.apache.camel.Processor {
#Resource
private StatusDAO statusDAO;
public CustomProcessor(StatusDAO statusDAO) {
this.statusDAO = statusDAO;
}
public void process(Exchange exchange) throws Exception {
Status status = null;
Message message = exchange.getIn();
Entity entity = (Entity) message.getBody();
if(entity.getId() != null) {
// find by id
status = statusDAO.findById(id);
}
status.setName("someName");
messageStatusDAO.update(status);
message.setBody(messageStatus);
exchange.setOut(message);
}
}
Data is received within the system as XML and changed to a Entity class. I find data using the statusDAO.findById() at this point the DAO returns no data even though the Entity exists.
I setup the test as below:
#Transactional
#TransactionalConfiguration(defaultRollback = true)
public class CustomTest {
#Transactional
#Before
public void setup throws Exception {
// setup the Status entity and save it using the DAO
}
#Test
public void testSomething() {
Status status = statusDAO.findById("id"); // returns the Status that was saved
MockEndpoint mockEndpoint = (MockEndpoint) this.context.getEndpoint(someDestination);
mockEndpoint.expectedMessageCount(1);
Status status = producerTemplate.requestBody(source, someXML, Status.class);
assertNotNull(status);
mockEndpoint.assertIsSatisfied();
}
}
Using the same transaction manager and the same statusDAO within the Custom Processor does not return the status.
I don't understand why.