OptimisticLockException not thrown when version has changed in spring-boot project - spring-boot

Model structure:
#MappedSuperclass
public class BaseModel<K extends Comparable> implements Serializable, Comparable<Object> {
private static final long serialVersionUID = 1L;
#Id
private K id;
#Version
private Integer version;
// getter/setter
}
#Entity
public class MyEntity extends BaseModel<String> {
// some fields and it's getter/setter
}
Record in my database for my_entity:
id: 1
version: 1
...
Below is my update method:
void update(String id, Integer currentVersion, ....) {
MyEntity myEntity = myRepository.findOne(id);
myEntity.setVersion(currentVersion);
// other assignments
myRepository.save(myEntity);
}
Below is the query being fired when this method is invoked.
update my_entity set version=?, x=?, y=?, ...
where id=? and version=?
I am expecting OptimisticLockException when currentVersion passed in above method is other than 1.
Can any body help me why I am not getting OptimisticLockException?
I am using spring-boot for my webmvc project.

Section 11.1.54 of the JPA specification notes that:
In general, fields or properties that are specified with the Version
annotation should not be updated by the application.
From experience, I can advise that some JPA providers (OpenJPA being one) actually throw an exception should you try to manually update the version field.
While not strictly an answer to your question, you can re-factor as below to ensure both portability between JPA providers and strict compliance with the JPA specification:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
//still an issue here however: see below
}
Assuming your update(...) method is running in a transaction however you still have an issue with the above as section 3.4.5 of the JPA specification notes:
3.4.5 OptimisticLockException Provider implementations may defer writing to the database until the end of the transaction, when
consistent with the lock mode and flush mode settings in effect. In
this case, an optimistic lock check may not occur until commit time,
and the OptimisticLockException may be thrown in the "before
completion" phase of the commit. If the OptimisticLockException must
be caught or handled by the application, the flush method should be
used by the application to force the database writes to occur. This
will allow the application to catch and handle optimistic lock
exceptions.
Essentially then, 2 users can submit concurrent modifications for the same Entity. Both threads can pass the initial check however one will fail when the updates are flushed to the database which may be on transaction commit i.e. after your method has completed.
In order that you can catch and handle the OptimisticLock exception, your code should then look something like the below:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
try{
myRepository.flush()
}
catch(OptimisticLockingFailureException ex){
throw new MyWrappedException();
}
}

Use EVICT before updating when using JPA. I did not get the #Version to work either. The property was increased but no exception was thrown when updating an object that had the wrong version-property.
The only thing I have got to work is to first EVICT the object and then save it. Then the HibernateOptimisticLockingException is thrown if the Version properties does not match.
Set the hibernates ShowSQL to 'true' to verify that the actual update sql ends with "where id=? and version=?". If the object is not evicted first, the update statement only has "where id=?", and that will (for obvious reasons) not work.

Optimistic hibernation lock works out of the box (You don't must put a version for Entity):
#Entity
#Table(name = "product")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long quantity;
private Long likes;
#Version
private Long version;
public Product() {
}
//setter and getter
//equals and hashcode
repository
public interface ProductRepository extends JpaRepository<Product, Long> {}
service
#Service
public class ProductOptimisticLockingService {
private final ProductRepository productRepository;
public ProductOptimisticLockingService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
#Transactional(readOnly = true)
public Product findById(Long id, String nameThread){
Product product =
productRepository
.findById(id)
.get();
System.out.printf(
"\n Select (%s) .... " +
"(id:) %d | (likes:) %d | (quantity:) %d | (version:) %d \n",
nameThread,
product.getId(),
product.getLikes(),
product.getQuantity(),
product.getVersion()
);
return product;
}
#Transactional(isolation = Isolation.READ_COMMITTED)
public void updateWithOptimisticLocking(Product product, String nameThread) {
try {
productRepository.save(product);
} catch (ObjectOptimisticLockingFailureException ex) {
System.out.printf(
"\n (%s) Another transaction is already working with a string with an ID: %d \n",
nameThread,
product.getId()
);
}
System.out.printf("\n--- Update has been performed (%s)---\n", nameThread);
}
}
test
#SpringBootTest
class ProductOptimisticLockingServiceTest {
#Autowired
private ProductOptimisticLockingService productService;
#Autowired
private ProductRepository productRepository;
#Test
void saveWithOptimisticLocking() {
/*ID may be - 1 or another. You must put the ID to pass in your methods. You must think how to write right your tests*/
Product product = new Product();
product.setLikes(7L);
product.setQuantity(5L);
productRepository.save(product);
ExecutorService executor = Executors.newFixedThreadPool(2);
Lock lockService = new ReentrantLock();
Runnable taskForAlice = makeTaskForAlice(lockService);
Runnable taskForBob = makeTaskForBob(lockService);
executor.submit(taskForAlice);
executor.submit(taskForBob);
executorServiceMethod(executor);
}
/*------ Alice-----*/
private Runnable makeTaskForAlice(Lock lockService){
return () -> {
System.out.println("Thread-1 - Alice");
Product product;
lockService.lock();
try{
product = productService
.findById(1L, "Thread-1 - Alice");
}finally {
lockService.unlock();
}
setPause(1000L); /*a pause is needed in order for the 2nd transaction to attempt
read the line from which the 1st transaction started working*/
lockService.lock();
try{
product.setQuantity(6L);
product.setLikes(7L);
update(product,"Thread-1 - Alice");
}finally {
lockService.unlock();
}
System.out.println("Thread-1 - Alice - end");
};
}
/*------ Bob-----*/
private Runnable makeTaskForBob(Lock lockService){
return () -> {
/*the pause makes it possible to start the transaction first
from Alice*/
setPause(50L);
System.out.println("Thread-2 - Bob");
Product product;
lockService.lock();
try{
product = findProduct("Thread-2 - Bob");
}finally {
lockService.unlock();
}
setPause(3000L); /*a pause is needed in order for the 1st transaction to update
the string that the 2nd transaction is trying to work with*/
lockService.lock();
try{
product.setQuantity(5L);
product.setLikes(10L);
update(product,"Thread-2 - Bob");
}finally {
lockService.unlock();
}
System.out.println("Thread-2 - Bob - end");
};
}
private void update(Product product, String nameThread){
productService
.updateWithOptimisticLocking(product, nameThread);
}
private Product findProduct(String nameThread){
return productService
.findById(1L, nameThread);
}
private void setPause(long timeOut){
try {
TimeUnit.MILLISECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void executorServiceMethod(ExecutorService executor){
try {
executor.awaitTermination(10L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}

Related

Spring-data JdbcTemplate does not commit

I need to update thousands of records in the database but i would like to commit after a batch of 5000 records.
#Service
#Transactional (rollbackFor=Throwable.class)
public class AttributeProcessorServiceImpl extends DataLoader implements
AttributeProcessorService
{
.....
private final TransactionTemplate transTemplate;
private final JdbcTemplate jdbcTemplate;
#Autowired private PlatformTransactionManager platformTransactionManager;
#Autowired
public BlockAttributeProcessorServiceImpl(
TransactionTemplate transTemplate,
JdbcTemplate jdbcTemplate,
.....)
{
super();
this.transTemplate = transTemplate;
this.jdbcTemplate=jdbcTemplate;
.....
}
#Async
#Transactional (propagation=Propagation.NOT_SUPPORTED)
public void reloadAttrs()
{
loadAttrs();
updateAttrs();
}
private void loadAttrs()
{
...some data fetching and processing, finally call db update.
updateDbInBatches(rowcount, sql);
}
private void updateAttrs()
{
...some data fetching and processing, finally call db update.
updateDbInBatches(rowcount, sql);
}
private void updateDbInBatches(long rowcount, String sql)
{
DefaultTransactionDefinition def;
boolean hasMore=true;
Integer from;
Integer to = 0;
int batchSize=5000; //gets from property
while (hasMore)
{
from = to+1;
to = batchSize;
def = new DefaultTransactionDefinition();
def.setName("backCommitTx");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = platformTransactionManager.getTransaction(def);
int rows = jdbcTemplate.update(sql,paramValues,paramTypes);
logger.debug("Loaded ["+rows+"] records.");
platformTransactionManager.commit(status);
if (to > rowcount)
{
hasMore=false;
logger.debug("All records ["+rowcount+"] updated.");
}
}
}
}
If I put a breakpoint after loadAttrs(), it shows it loaded bunch of records to the database and issued a commit(), but database does not reflect that commit, until after entire public method completes. How do i ensure data is indeed written to the database after each commit. commit neither gives any error as well.
I missed an important piece of information that solved the problem.
I had another public method which is what was called from outside.
public void reloadAttrs(TransDetail trans)
{
reloadAttrs();
}
Above method was infact using default Transaction Propagation as i did not mention it specifically. Since this was the first public method that was called, spring was ignoring transaction demarcation on next public (async) method that was called. I changed above signature to:
#Transactional (propagation=Propagation.NOT_SUPPORTED)
public void reloadAttrs(TransDetail trans)
{
reloadAttrs();
}
It then worked. I was able to see changes in the database after every commit.

Spring #Transactional: Should't the second thread wait until the first thread commit/rollback?

The issue here is that both threads are executing the first SELECT at the same time. Considering that saveUser is a #Transactional method, should't the second thread wait until the first thread commit/rollback?
Code:
#SpringBootApplication
public class TestApp
{
public static void main(String[] args)
{
ConfigurableApplicationContext app = SpringApplication.run(TestApp.class, args);
UserService us = (UserService) app.getBean("userService");
Thread t1 = new Thread(() -> us.saveUser("email#email.com"));
t1.setName("Thread #1");
t1.start();
Thread t2 = new Thread(() -> us.saveUser("email#email.com"));
t2.setName("Thread #2");
t2.start();
}
}
#Repository
public interface UserRepository extends CrudRepository<UserService.User, Long>
{
public UserService.User getByEmail(String email);
}
#AllArgsConstructor
#Service
public class UserService
{
private final UserRepository userRepository;
#Transactional
public boolean saveUser(String email)
{
if (userRepository.getByEmail(email) != null)
{
System.out.println("User already exists");
return false;
}
System.out.println(Thread.currentThread().getName() + ": User doesn't exists, sleeping..");
try
{
Thread.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
User user = new User();
user.email = email;
System.out.println(Thread.currentThread().getName() + ": Saving user..");
user = userRepository.save(user);
return user.id > 0;
}
#Table("user")
public static class User
{
#Id
public long id;
public String email;
}
}
Output:
Thread #1: User doesn't exists, sleeping..
Thread #2: User doesn't exists, sleeping..
Thread #1: Saving user..
Thread #2: Saving user..
Exception in thread "Thread #2" org.springframework.data.relational.core.conversion.DbActionExecutionException: Failed to execute DbAction.InsertRoot(entity=testapp.UserService$User#3ce548a)
[...]
Caused by: org.springframework.dao.DuplicateKeyException: PreparedStatementCallback;
[...]
Table:
create table user (`id` int primary key auto_increment, `email` varchar(50) unique);
#Transactional has isolation parameter that specifies isolation level of your transactions.
The transaction isolation level.
Defaults to Isolation.DEFAULT.
Default means the default isolation selected by your db.
The behaviour you want is described by Serializable isolation level, which, most likely is not the default (Just to name a few: Postgres, MsSql Server, Oracle default to Read Commited)
if you want to control multiple threads in transactional environment, you have to use isolation levels.
one example:
#Transactional(isolation = Isolation.SERIALIZABLE)
You will have to set the Transaction context for each individual thread for that to happen.

Rollback not performed for #Transactional Annotation [duplicate]

This question already has an answer here:
JPA save with multiple entities not rolling back when inside Spring #Transactional and rollback for Exception.class enabled
(1 answer)
Closed 3 years ago.
I am trying to create an API for transferring funds
i.e withdrawal and deposit.
I performed the transaction using #Transactional Annotation.
But there are certain criteria i.e if bank account number doesn't exist that should through an Runtime exception.
I will attach the code within. Now, when transferBalanceMethod is called and if depositor bank Account doesn't exist than amount that is withdrawn should also be rolled back.
But that isn't happening. Means when fund transfer occurs from account A to Account B of 1000 rupees then if an exception occurs in B's deposition then the withdraw in A Account should also be withdrawn.
I tried #Transactional annotation and also rollbackFor property of the Exception class too
I tried to add #Transaction annotation for deposit and withdraw method too but that we use the same transaction because we use propogation Required**
Model Class//This is the Model Class
//All Imports
#Entity
public class BankAccount {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Integer id;
#Column(name = "bankAccountNumber", nullable = false,unique = true)
#NotNull
#Size(min = 5, message = "Bank account number should be greater than 5 characters")
private String bankAccountNumber;
#NotNull
#Column(name = "balance", nullable = false)
#Min(1000)
private Long balance;
//Getter Setter and Constructor
**Controller File**//This is the Controller Class
//All imports and other stuff such as #RestController, #Autowired
#GetMapping("/bankaccount/transfer")
public void transferBalance(#RequestParam("bankAccountNo1") String bankAccountNo1, #RequestParam("bankAccountNo2") String bankAccountNo2,
#RequestParam("balance") Long balance) throws RuntimeException
{
bankService.transferBalance(bankAccountNo1,bankAccountNo2, balance);
}
}
**Service File:-**//This is Service Layer
//All imports
#Service
public class BankService {
#Autowired
private BankRepository bankRepository;
#Autowired
private ModelMapper modelMapper;
public List<BankAccountDTO> getAllBankAccount() {
List<BankAccountDTO> bankAccountDTO = new ArrayList<BankAccountDTO>();
List<BankAccount> bankAccount = bankRepository.findAll();
for (BankAccount b : bankAccount) {
bankAccountDTO.add(modelMapper.map(b, BankAccountDTO.class));
}
return bankAccountDTO;
}
public ResponseEntity<?> getIndividualBankAccount(String bankAccountNumber) {
BankAccount bankAccount = bankRepository.findByBankAccountNumber(bankAccountNumber);
if (bankAccount == null) {
return new ResponseEntity<>("Account not found", HttpStatus.BAD_REQUEST);
} else {
return new ResponseEntity<>(
modelMapper.map(bankRepository.findByBankAccountNumber(bankAccountNumber), BankAccountDTO.class),
HttpStatus.OK);
}
}
public Object addBankAccount(BankAccountDTO bankAccountDTO) {
return bankRepository.save(modelMapper.map(bankAccountDTO, BankAccount.class));
}
#Transactional(propagation = Propagation.REQUIRED)
public void depositBalance(String bankAccountNumber, Long balance) throws RuntimeException {
BankAccount bankAccNo = bankRepository.findByBankAccountNumber(bankAccountNumber);
if (bankAccNo == null) {
throw new RuntimeException("Bank Accout Number is not found : " + bankAccountNumber);
} else {
if (balance <= 0) {
throw new RuntimeException("Please deposit appropriate balance");
} else {
Long amount = bankAccNo.getBalance() + balance;
bankAccNo.setBalance(amount);
bankRepository.save(bankAccNo);
}
}
}
#Transactional(propagation = Propagation.REQUIRED)
public void withdrawBalance(String bankAccountNumber, Long balance) throws RuntimeException {
BankAccount bankAccNo = bankRepository.findByBankAccountNumber(bankAccountNumber);
if (bankAccNo == null) {
throw new RuntimeException("Bank Account not found :" + bankAccountNumber);
} else {
if (balance <= 0) {
throw new RuntimeException("Please withdraw appropriate balance");
} else {
Long amount = bankAccNo.getBalance() - balance;
if (amount < 1000) {
throw new RuntimeException("Sorry Cannot withdraw.Your minimum balance should be thousand rupees!");
} else {
bankAccNo.setBalance(amount);
bankRepository.save(bankAccNo);
}
}
}
}
#Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = RuntimeException.class)
public void transferBalance(String bankAccountNo1, String bankAccountNo2, Long balance) throws RuntimeException {
try {
withdrawBalance(bankAccountNo1, balance);
depositBalance(bankAccountNo2, balance);
} catch (RuntimeException e) {
throw e;
}
}
}
Only runtime exceptions will trigger a rollback operation within a spring transaction annotation, if you are using custom annotations you need to make sure you either made then extend from RuntimeException or that you add an specific rollback clause to your transaction so that it rollback in that specific exception.
maybe this answer will be useful for you:
Spring transaction: rollback on Exception or Throwable
also to go to the official transactional documentation of spring here:
https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html

Transactions and relationship entities mapping problems with Neo4j OGM

Versions used: spring-data-neo4j 4.2.0-BUILD-SNAPSHOT / neo4j-ogm 2.0.6-SNAPSHOT
I'm having problems to correctly fetch relationship entities.
The following fetch calls don't return consistent results (executed in the same transaction):
session.query("MATCH (:A)-[b:HAS_B]-(:C) RETURN count(b) as count") returns 1
session.query("MATCH (:A)-[b:HAS_B]-(:C) RETURN b") correctly returns the relationship entity as a RelationshipModel object
session.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) RETURN b") returns null !
Important remark: When all operations (create, fetch) are done in the same transaction, it seems to be fine.
I have been able to implement a workaround by using session.query(String, Map) to query the relationship entity and map it by myself into my POJO.
#NodeEntity
public class A {
public A () {}
public A (String name) {
this.name = name;
}
#GraphId
private Long graphId;
private String name;
#Relationship(type="HAS_B", direction=Relationship.OUTGOING)
private B b;
}
#RelationshipEntity(type="HAS_B")
public class B {
public B () {}
public B (String name, A a, C c) {
this.name = name;
this.a = a;
this.c = c;
}
#GraphId
private Long graphId;
#StartNode
private A a;
#EndNode
private C c;
private String name;
}
#NodeEntity
public class C {
public C () {}
public C (String name) {
this.name = name;
}
#GraphId
private Long graphId;
private String name;
}
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes={MyTest.TestConfiguration.class})
public class MyTest {
#Autowired
private MyBean myBean;
#Configuration
#EnableAutoConfiguration
#EnableTransactionManagement
#EnableNeo4jRepositories("com.nagra.ml.sp.cpm.core.repositories")
public static class TestConfiguration {
#Bean
public org.neo4j.ogm.config.Configuration configuration() {
org.neo4j.ogm.config.Configuration config = new org.neo4j.ogm.config.Configuration();
config.driverConfiguration().setDriverClassName("org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver");
return config;
}
#Bean
public SessionFactory sessionFactory() {
return new SessionFactory(configuration(), "com.nagra.ml.sp.cpm.model");
}
#Bean
public Neo4jTransactionManager transactionManager() {
return new Neo4jTransactionManager(sessionFactory());
}
#Bean
public MyBean myBean() {
return new MyBean();
}
}
#Test
public void alwaysFails() {
myBean.delete();
myBean.create("1");
try { Thread.sleep(2000); } catch (InterruptedException e) {} //useless
myBean.check("1"); // FAILS HERE !
}
#Test
public void ok() {
myBean.delete();
myBean.createAndCheck("2");
}
}
#Transactional(propagation = Propagation.REQUIRED)
public class MyBean {
#Autowired
private Session neo4jSession;
public void delete() {
neo4jSession.query("MATCH (n) DETACH DELETE n", new HashMap<>());
}
public void create(String suffix) {
C c = new C("c"+suffix);
neo4jSession.save(c);
A a = new A("a"+suffix);
neo4jSession.save(a);
B bRel = new B("b"+suffix, a, c);
neo4jSession.save(bRel);
}
public void check(String suffix) {
//neo4jSession.clear(); //Not working even with this
Number countBRels = (Number) neo4jSession.query("MATCH (:A)-[b:HAS_B]-(:C) WHERE b.name = 'b"+suffix+"' RETURN count(b) as count", new HashMap<>()).iterator().next().get("count");
assertEquals(1, countBRels.intValue()); // OK
Iterable<B> bRels = neo4jSession.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) WHERE b.name = 'b"+suffix+"' RETURN b", new HashMap<>());
boolean relationshipFound = bRels.iterator().hasNext();
assertTrue(relationshipFound); // FAILS HERE !
}
public void createAndCheck(String suffix) {
create(suffix);
check(suffix);
}
}
This query session.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) RETURN b") returns only the relationship but not the start node or end node and so the OGM cannot hydrate this. You need to always return the start and end node along with the relationship like session.query(B.class, "MATCH (a:A)-[b:HAS_B]-(c:C) RETURN a,b,c")
The reason it appears to work when you both create and fetch data in the same transaction is that the session already has a cached copy of a and c and hence b can be hydrated with cached start and end nodes.
Firstly, please upgrade from OGM 2.0.6-SNAPSHOT to 2.1.0-SNAPSHOT. I have noticed some off behaviour in the former which might be one part of the issue.
Now on to your test. There are several things going on here which are worth investigating.
Use of #DirtiesContext: You don't seem to be touching the context and if you are using it to reset the context between tests so you get a new Session/Transaction then that's going about it the wrong way. Just use #Transactional instead. The Spring JUnit runner will treat this in a special manner (see next point).
Being aware that Transactional tests automatically roll back: Jasper is right. Spring Integration Tests will always roll back by default. If you want to make sure your JUnit test commits then you will have to #Commit it. A good example of how to set up your test can be seen here.
Knowing how Spring Transaction proxies work. On top of all this confusion you have to make sure you don't simply call transactional method to transactional method in the same class and expect Spring's Transactional behaviour to apply. A quick write up on why can be seen here.
If you address those issues everything should be fine.

Hibernate: ConstraintViolationException with parallel inserts

I have a simple Hibernate entity:
#Entity
#Table(name = "keyword",
uniqueConstraints = #UniqueConstraint(columnNames = { "keyword" }))
public class KeywordEntity implements Serializable {
private Long id;
private String keyword;
public KeywordEntity() {
}
#Id
#GeneratedValue
#Column(unique = true, updatable=false, nullable = false)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name="keyword")
public String getKeyword() {
return this.keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
DAO for it:
#Component
#Scope("prototype")
public class KeywordDao {
protected SessionFactory sessionFactory;
#Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public KeywordEntity findByKeyword(String keyword) throws NotFoundException {
Criteria criteria = sessionFactory.getCurrentSession()
.createCriteria(KeywordEntity.class)
.add(Restrictions.eq("keyword", keyword));
KeywordEntity entity = (KeywordEntity) criteria.uniqueResult();
if (entity == null) {
throw new NotFoundException("Not found");
}
return entity;
}
public KeywordEntity createKeyword(String keyword) {
KeywordEntity entity = new KeywordEntity(keyword);
save(entity);
return entity;
}
}
and a service, which puts everything under #Transactional:
#Repository
#Scope("prototype")
public class KeywordService {
#Autowired
private KeywordDao dao;
#Transactional(readOnly = true)
public KeywordEntity getKeyword(String keyword) throws NotFoundException {
return dao.findByKeyword(keyword);
}
#Transactional(readOnly = false)
public KeywordEntity createKeyword(String keyword) {
return dao.createKeyword(keyword);
}
#Transactional(readOnly = false)
public KeywordEntity getOrCreateKeyword(String keyword) {
try {
return getKeyword(keyword);
} catch (NotFoundException e) {
return createKeyword(keyword);
}
}
}
In a single-threaded environment this code runs just fine. The problems, when I use it in multi-threaded environment. When there are many parallel threads, working the same keywords, some of them are calling the getOrCreateKeyword with the same keyword at the same time and following scenario occurs:
2 threads at the same time call keyword service with the same keyword, both first tries to fetch the existing keyword, both are not finding, and both try to create new one. The first one succeeds, the second - causes ConstraintViolationException to be thrown.
So I did try to improve the getOrCreateKeyword method a little:
#Transactional(readOnly = false)
public KeywordEntity getOrCreateKeyword(String keyword) {
try {
return getKeyword(keyword);
} catch (NotFoundException e) {
try {
return createKeyword(keyword);
} catch (ConstraintViolationException ce) {
return getKeyword(keyword);
}
}
}
So theoretically it should solve the issues, but in practice, once ConstraintViolationException is thrown, calling the getKeyword(keyword) results in another Hibernate exception:
AssertionFailure - an assertion failure occured (this may indicate a bug in Hibernate,
but is more likely due to unsafe use of the session)org.hibernate.AssertionFailure:
null id in KeywordEntity entry (don't flush the Session after an exception occurs)
How to solve this problem?
You could use some sort of Pessimistic locking mechanism using the database/hibernate or you could make the service method getOrCreateKeyword() synchronized if you run on a single machine.
Here are some references.
Hibernates documentation http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html#transactions-locking
This article shows how to put a lock on a specific entity and all entities from a result of a query which may help you.
http://www.objectdb.com/java/jpa/persistence/lock#Locking_during_Retrieval_
The solution was to discard the current session once ConstraintViolationException occurs and retrieve the keyword one more time within the new session. Hibernate Documentation also point to this:
If the Session throws an exception, the transaction must be rolled back and the session discarded. The internal state of the Session might not be consistent with the database after the exception occurs.

Resources