JSF bean validation and exceptions thrown by validators - validation

Bean validation is suppressed when an exception is thrown by a validator. I wonder what is the correct way of handling this. The form:
<h:form id="registration_form">
<h:panelGrid columns="3">
<h:outputLabel for="username">Username</h:outputLabel>
<h:inputText id="username" value="#{userController.user.username}">
<f:validator binding="#{userController$UniqueUsernameValidator}"
redisplay="true"/> <!-- Not sure about this -->
<f:ajax event="blur" render="usernameMessage" />
</h:inputText>
<h:message id="usernameMessage" for="username" />
<!-- etc-->
</h:panelGrid>
</h:form>
The UserController:
#ManagedBean
#ViewScoped
public class UserController {
// http://stackoverflow.com/a/10691832/281545
private User user;
#EJB
// do not inject stateful beans !
private UserService service;
public User getUser() {
return user;
}
#PostConstruct
void init() {
// http://stackoverflow.com/questions/3406555/why-use-postconstruct
user = new User();
}
#ManagedBean
#RequestScoped
public static class UniqueUsernameValidator implements Validator {
// Can't use a Validator (no injection) - see:
// http://stackoverflow.com/a/7572413/281545
#EJB
private UserService service;
#Override
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
if (value == null) return; // Let required="true" handle, if any.
try {
if (!service.isUsernameUnique((String) value)) {
throw new ValidatorException(new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Username is already in use.", null));
}
} catch (Exception e) {
System.out.println(cause(e));
Throwable cause = e.getCause();
if (cause instanceof PersistenceException) {
Throwable cause2 = cause.getCause();
// ((PersistenceException)cause) - only superclass methods
if (cause2 instanceof DatabaseException) {
// now this I call ugly
int errorCode = ((DatabaseException) cause2)
.getDatabaseErrorCode(); // no java doc in eclipse
if (errorCode == 1406)
throw new ValidatorException(new FacesMessage(
FacesMessage.SEVERITY_ERROR, "Max 45 chars",
null));
}
}
// TODO: DEGUG, btw the EJBException has null msg
throw new ValidatorException(new FacesMessage(
FacesMessage.SEVERITY_ERROR, cause.getMessage(), null));
}
}
private static String cause(Exception e) {
StringBuilder sb = new StringBuilder("--->\nEXCEPTION:::::MSG\n"
+ "=================\n");
for (Throwable t = e; t != null; t = t.getCause())
sb.append(t.getClass().getSimpleName()).append(":::::")
.append(t.getMessage()).append("\n");
sb.append("FIN\n\n");
return sb.toString();
}
}
}
The entity:
/** The persistent class for the users database table. */
#Entity
#Table(name = "users")
#NamedQuery(name = "User.findAll", query = "SELECT u FROM User u")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private int iduser;
#NotNull(message = "Please enter a username")
#Pattern(regexp = "[A-Za-z0-9_]{6}[A-Za-z0-9_]*",
message = "Usernames can have latin characters, the underscore and "
+ "digits and are at least 6 characters")
#Size(max = 45)
private String username;
//etc
}
and the service:
#Stateless
public class UserService {
#PersistenceContext
private EntityManager em;
public boolean isUsernameUnique(String username) {
Query query = em
.createNativeQuery("SELECT r1_check_unique_username(?)");
short i = 0;
query.setParameter(++i, username);
return (boolean) query.getSingleResult();
}
}
What happens is that if I put a username longer than 45 chars MySql throws an exception - the output of cause() is:
INFO: --->
EXCEPTION:::::MSG
=================
EJBException:::::null
PersistenceException:::::Exception[EclipseLink-4002](Eclipse Persistence Services\
- 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data\
too long for column 'username_given' at row 198
Error Code: 1406
Call: SELECT r1_check_unique_username(?)
bind => [1 parameter bound]
Query: DataReadQuery(sql="SELECT r1_check_unique_username(?)")
DatabaseException:::::
Internal Exception: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data\
too long for column 'username_given' at row 198
Error Code: 1406
Call: SELECT r1_check_unique_username(?)
bind => [1 parameter bound]
Query: DataReadQuery(sql="SELECT r1_check_unique_username(?)")
MysqlDataTruncation:::::Data truncation:Data too long for column 'username_given'\
at row 198
FIN
The way I handle it the "Max 45 chars" message is shown.
But this handling (with explicit check on the error code) is a bit smelly. On the other hand, if I do not throw a ValidatorException (and just catch (Exception e) {} which is also smelly) the bean validation kicks in ( the one induced by #Size(max = 45) ) but I still see the exception trace in the glassfish logs.
Questions
Is this way of handling it correct (should I catch (Exception ignore) {} in the validator or check the exception I get manually - ideally I would be able to require the validator be run after the (all) bean validation(s) in User - and only if these validations pass)
How do I suppress the Exception being printed in the server logs ?

Related

Save entities using Spring Data JPA saveAll method even in exception scenario

Is there any way to save the true entities in the DB even after any exception occurred during saveAll execution?
For example, I have 100 entities and 2 entities result in data violation exception but I still want to save the other 98 entities in DB. As of now if a data violation exception occurred it save none in the DB.
#Autowired
BillingJpaRepository billingJpaRepository;
#Transactional
private void saveBillingRecords(List<BillingInfo> billingInfos) {
try {
billingJpaRepository.saveAll(billingInfos);
} catch (DataIntegrityViolationException dataIntegrityViolationException) {
this.saveBillingRecordsIndividually(billingInfos);
} catch (Exception ex) {
throw ex;
}
}
#Transactional
private void saveBillingRecordsIndividually(List<BillingInfo> billingInfos) {
for(BillingInfo billingInfo : billingInfos) {
try {
billingJpaRepository.save(billingInfo);
} catch (DataIntegrityViolationException dataIntegrityViolationException) {
logger.error("data voilation exception occured");
} catch (Exception ex) {
throw ex;
}
}
}
Because of this, I have to save all true entities individually which is a performance hit. Since all the entities will be in detached stated after saveAll it will execute merge operation instead of persist and because of this, an extra SELECT statement will be triggered for each save operation. Below is my entity structure
#AllArgsConstructor
#NoArgsConstructor
#Data
#Entity
#Table(name= "BLI_INFO")
public class BillingInfo implements Persistable<long> {
#Id
#Column(name = "BLI_ID")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_gen")
#SequenceGenerator(sequenceName = "BLI_ID_SEQ", name = "id_gen", allocationSize = 1)
private long billingId;
...
// rest of the fields
...
#Transient
private boolean isNew = true;
#PrePersist
#PostLoad
void markNotNew() {
this.isNew = false;
}
#Override
public boolean isNew(){
return true;
}
}

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

Overriding user injected by #CreatedBy Annotation in AuditorAware Implementation

Following is my implementation of AuditAware.
#Component
public class AuditorAwareImpl implements AuditorAware<String> {
#Log
private Logger logger;
#Override
public String getCurrentAuditor() {
String user = "SYSTEM";
try {
final Subject subject = SecurityUtils.getSubject();
if (subject != null) {
final Session session = subject.getSession(false);
if (session != null) {
final User userObj = (User) session.getAttribute("user");
if (userObj != null) {
user = userObj.getUsername();
}
}
}
} catch (final Exception e) {
this.logger.error("getCurrentAuditor", "No logged in user information found.", e);
}
return user;
}
}
As we know, The value of user returned by AuditorAwareImpl injected by Spring in attribute marked by #CreatedBy annotation as below.
#CreatedBy
#Column(name = "CREATED_BY", length = 150, nullable = false, updatable = false)
private String createdBy;
There are two problems I want to solve.
I am triggering a separate thread which saves
thousands of objects in database. If session times out in between, session object becomes null and AuditAwareImpl throws error and hence background process errors out.
I also want to run few quartz Schedular related jobs in which in which session object is itself not available. But I still want to inject some hard coded user in that case. But how do I do it?
One thing that I have tried at individual object level, using following code is,
#Transient
private String overRiddenUser;
#PrePersist
public void updateCreatedBy(){
if(overRiddenUser!=null){
this.createdBy=overRiddenUser;
}
}
I explicitly set overRiddenUser="Admin" whenever I want to override user injected by spring in createdBy and overwrite it using PrePersist method. But even this does not seem to work since before even I overwrite this value, spring tries to populate created by using value returned by AuditAware. If session is already expired by then the process errors outs! How do I solve this?
I found the solution to above problems as below.
Step 1
I defined a bean as below that contains a HashMap. This map will store current thread name as key and mapped username as value.
public class OverRiddenUser {
Map<String,String> userThreadMap= new HashMap<>();
//add getters/setters
}
Step 2
Initialised this bean on startup in class annoted by #Configuration.
#Bean("overRiddenUser")
public OverRiddenUser overRideUser(){
OverRiddenUser obj = new OverRiddenUser();
return obj;
// Singleton implementation can be done her if required
}
Step 3
Auto wired this bean in the impl class where I want to override session user.
#Autowired
OverRiddenUser overRiddenUser;
Assigned a random name to currently running thread.
RandomString randomThreadName = new RandomString(9);
Thread.currentThread().setName(randomThreadName.toString());
And then put required user name for current thread in hashmap as below.
try {
if(overRiddenUser!=null){
overRiddenUser.getUserThreadMap().put(Thread.currentThread().getName(),"My Over ridden username");
}
// Entire Database related code here
} catch (Exception e){
// handle the exceptions thrown by code
} finally {
//Perform clean up here
if(overRiddenUser!=null){
overRiddenUser.getUserThreadMap().remove(Thread.currentThread().getName());
}
}
}
Step 4
Then I modified AuditAwareImpl as below.
#Component
public class AuditorAwareImpl implements AuditorAware<String> {
#Log
private Logger logger;
#Autowired
OverRiddenUser overRiddenUser;
#Override
public String getCurrentAuditor() {
String user = "SYSTEM";
try {
if(overRiddenUser!=null && overRiddenUser.getUserThreadMap()!=null && overRiddenUser.getUserThreadMap().get(Thread.currentThread().getName())!=null){
user=overRiddenUser.getUserThreadMap().get(Thread.currentThread().getName());
}else{
final Subject subject = SecurityUtils.getSubject();
if (subject != null) {
final Session session = subject.getSession(false);
if (session != null) {
final User userObj = (User) session.getAttribute("user");
if (userObj != null) {
user = userObj.getUsername();
}
}
}
}
} catch (final Exception e) {
this.logger.error("getCurrentAuditor", "No logged in user information found.", e);
}
return user;
}
}
So if the currently running thread accesses AuditAwareImpl then it gets custom user name which was set for that thread. If this is null then it pulls username from session as it was doing earlier. Hope that helps.

org.springframework.validation.BeanPropertyBindingResult

Controller class-
#Controller
#SessionAttributes({"id", "roleId"})
#RequestMapping("/User")
public class UserController {
#Autowired
private UserService userv = null;
#InitBinder("stdUser")
private void initBinder(WebDataBinder binder) {
System.out.println("1111======"+binder.getObjectName());
binder.setValidator(new NewUserValidator());
System.out.println("2222======"+binder.getObjectName());
}
#RequestMapping(value = "/allUsers")
public ModelAndView allUser(#ModelAttribute("userSetup")#Valid UserBean stdUser, BindingResult result, Map<String, Object> map, HttpSession session) {
StdCheckAccessV chk = new StdCheckAccessV();
chk.setFexe(Screens.User.substring(Screens.User.lastIndexOf("/") + 1, Screens.User.length()));
chk.setUid(Long.parseLong(session.getAttribute("id").toString().trim()));
chk.setRid(Long.parseLong(session.getAttribute("roleId").toString().trim()));
chk = userv.getAccess(chk);
List<StdUsers> l = userv.getUsersList();
stdUser.setChkAccessV(chk);
map.put("userList", l);
return new ModelAndView(Screens.User);
}
#RequestMapping(value = "/submitUser")
public ModelAndView addOrUpdate(#ModelAttribute("userSetup")#Valid UserBean stdUser, BindingResult result, HttpSession session, final RedirectAttributes redA) {
try {
int res = 0;
Long id = stdUser.getStdUsers().getId();
if (result.hasErrors()) {
System.out.println("///////result has errors " + result.toString());
return new ModelAndView("redirect:/User/allUsers");
}
System.out.println("////////the id============"+id);
if (id == null) {
System.out.println("/////inside the if");
stdUser.getStdUsers().setUserGroupId(Long.parseLong(session.getAttribute("id").toString().trim()));
stdUser.getStdUsers().setCreatedBy(Long.parseLong(session.getAttribute("id").toString().trim()));
stdUser.getStdUsers().setCreationDate(new Date());
res = userv.submitUser(stdUser);
} else {
System.out.println("/////inside the else");
stdUser.getStdUsers().setUpdateDate(new Date());
stdUser.getStdUsers().setUpdatedBy(Long.parseLong(session.getAttribute("id").toString().trim()));
res = userv.updateUser(stdUser);
}
} catch (Exception e) {
System.out.println("////Exception in add or update method " + e);
}
return new ModelAndView("redirect:/User/allUsers");
//return "redirect:/User/allUsers";
}}
Validator class-
#Component
public class NewUserValidator implements Validator {
#Override
public boolean supports(Class<?> userOb) {
return UserBean.class.equals(userOb);
}
#Override
public void validate(Object target, Errors errors) {
try {
UserBean user = (UserBean) target;
if (user.getStdUsers().getUserName() == null) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "stdUsers.userName", "user.name.empty");
//errors.reject("stdUsers.userName", "User Name is mandatory");
}
} catch (Exception e) {
System.out.println(e);
}
}
}
Bean Class-
public class UserBean {
private #Valid StdUsers stdUsers;
private #Valid StdCheckAccessV chkAccessV;
private boolean listView = true;
private String rePassword;
getters and setters.... }
index.jsp-
<body>
<%
session.setAttribute("id", 1);
session.setAttribute("roleId", 1);
%>
User<br>
</body>
CMN/STD100002.jsp page
<td class="tdright"><form:label path="stdUsers.userName">User Name<em>*</em></form:label></td>
<td><form:input path="stdUsers.userName"/></td>
<td><form:errors path="stdUsers.userName"></form:errors></td>
<a href="#" name="btnsub" id="btnsub" onclick="submitUser()">
<table cellspacing="0" cellpadding="0" border="0">
<tr>
<td width="25" height="24px" align="center">
<img src="${pageContext.servletContext.contextPath}/resources/images/Buttons/gray icon/Add.png" width="16" height="15" border="0" />
</td>
<td>Submit</td>
<td></td>
</tr>
</table>
</a>
<script type="text/javascript">
function submitUser(){
document.form1.action="${pageContext.servletContext.contextPath}/User/submitUser";
document.form1.submit();
}
</script>
I am following a tutorial for using validation in spring as i am new for spring.
On click of the submit button while leaving the User Name field empty, it should get validated with the message from Validator.
When i tried to print the error in controller class by-
System.out.println("result has errors " + result.toString());
it prints-
result has errors org.springframework.validation.BeanPropertyBindingResult: 1 errors
Please help, i am not getting where i am wrong.
I used this code to check where is the error-
for (Object object : result.getAllErrors()) {
if (object instanceof FieldError) {
FieldError fieldError = (FieldError) object;
System.out.println("the field errors::::::::::::::::"+fieldError.getCode());
}
if (object instanceof ObjectError) {
ObjectError objectError = (ObjectError) object;
System.out.println("the object errors:::::::::"+objectError.getCode());
}
}
and i found-
the field errors::::::::::::::::user.name.empty
the object errors:::::::::user.name.empty
///////result has errors org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'userSetup' on field 'stdUsers.userName': rejected value []; codes [user.name.empty.userSetup.stdUsers.userName,user.name.empty.stdUsers.userName,user.name.em pty.userName,user.name.empty.java.lang.String,user.name.empty]; arguments []; default message [null]
Since these two fields i am putting empty to check validation. If i will get these error on putting the fields empty then how can i validate them?
StdUser-
#Entity
#Table(name = "STD_USERS")
#NamedQueries({
#NamedQuery(name = "StdUsers.findAll", query = "SELECT s FROM StdUsers s")})
public class StdUsers implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GenericGenerator(name="generator",strategy="increment")
#GeneratedValue(generator="generator")
#Basic(optional = false)
#Column(name = "ID", nullable = false)
private Long id;
#Basic(optional = false)
#Column(name = "USER_NAME", nullable = false, length = 50)
private String userName;
setters and getters.....
Any one please reply.
Oh finally got the solution just here-
Changed the Validator class as-
try {
UserBean user = (UserBean) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "stdUsers.userName","required.stdUsers.userName" ,"User name required");
} catch (Exception e) {
System.out.println(e);
}
and in Controller class rather than returning to controller's allUsers method when result.hasError() i.e.-
return new ModelAndView("redirect:/User/allUsers");
changed to(navigated to page)-
return new ModelAndView("/CMN/STD100002");
and yes if you are getting date conversion related error, use this in your controller class-
#InitBinder
private void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");//edit for the format you need
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}

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