I’ve run into an issue with my Spring 3 web application:
Users can upload multiple attachments to an AttachmentParent by dragging and dropping into their browser which triggers a multipart upload. A spring controller picks up the file, creates an Attachment entity, and then sends it to the service layer where it is added to the list of attachments on the AttachmentParent.
It works fine with one file, but if they drag multiple and they finish uploading at close to the same time then service layer gets called multiple times, and since each file is a separate request and the service is marked @Transactional, multiple transactions are opened around the AttachmentParent at the same time. The first one closes successfully, but all the rest of them throw optimistic locking exceptions since the parent has changed since they opened and the update doesn’t happen.
Here’s the relevant code:
FileUploadController.java
@RequestMapping(value = "/attach/{parentid}", method = RequestMethod.POST)
public View handleFormUpload(@RequestParam("file") MultipartFile file, @PathVariable("parentid") long parentId, Model model) {
AttachmentParent parent = em.find(AttachmentParent.class, parentId);
try {
StaticFile staticFile = fileUploader.uploadFile(file, parent.getUrl() + "/attachments/");
Attachment a = new Attachment(staticFile.getFilename(), "", staticFile);
attachmentService.addAttachment(parentId, a);
//add data for the json response to the client
model.addAttribute("name", file.getName());
model.addAttribute("size", file.getSize());
model.addAttribute("url", staticFile.getUrl());
model.addAttribute("thumbnail_url", staticFile.getThumbnail_url());
} catch (IOException e) {
log.error("Cannot upload file");
model.addAttribute("error", e.getMessage());
}
return new MappingJacksonJsonView();
}
AttachmentService.java
@Transactional
public class AttachmentService extends GenericService<Attachment> {
...
public void addAttachment(long parentId, Attachment attachment) {
log.debug("Starting to add an attachment for "+parentId);
AttachmentParent parent = em.find(AttachmentParent.class, parentId);
attachment.setParent(parent);
parent.addAttachment(attachment);
log.debug("About to merge "+parent.getId());
em.merge(parent);
}
The output of the log messages shows the problem:
Starting to add an attachment for 38
Starting to add an attachment for 38
Starting to add an attachment for 38
About to merge 38
About to merge 38
About to merge 38
and the console error message while trying to close each transaction following the first:
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
I’m using a spring-managed transaction manager, hibernate, and mysql5:
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
From what I understand from reading articles around the web, this is optimistic locking at work. My question is, what is the best way to solve this problem?
Three possible solutions I’ve come up with:
-
Mark the service method as synchronized – this fixes the problem. But its more restrictive than it needs to be: the only problem is when multiple threads access the same attachment parent. Maybe there’s a spring annotation I don’t know about that allows you to use spring el to specify when a method should be synchronous? Also, I’m not sure if this will have other effects that I’m unaware of: I’ve never seen a service layer method marked synchronous, so am assuming it’s frowned upon for some reason
-
Accept all files in the same request, and iterate over them so that each transaction gets closed before another opened. I’m guessing that this is possible, but doesn’t answer the original question about transaction boundaries
-
Somehow leave a single transaction open over multiple requests. I’m not sure if this is possible but is very different from my current pattern (transaction boundary on service layer)
Update: full AttachmentParent class below
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AttachmentParent extends BaseEntity {
private Set<Attachment> attachments = new HashSet<Attachment>();
@OneToMany(cascade = CascadeType.ALL)
//annotation added in response to gkamal's answer, to no effect
@org.hibernate.annotations.OptimisticLock(excluded = true)
public Set<Attachment> getAttachments() {
return attachments;
}
public void setAttachments(Set<Attachment> attachments) {
this.attachments = attachments;
}
public void addAttachment(Attachment a) {
attachments.add(a);
}
public void removeAttachment(Attachment a) {
attachments.remove(a);
}
}
I’ve ended up just removing the list from the parent and storing a reference to the parent in the child. The convenience of having the list of children in the parent wasn’t worth the troubles it caused.
Since I still wanted a count of the number of attachments to be stored in the parent, I added an integer
attachmentCountvariable to the parent class. Trying to increment it inside of theaddAttachmentmethod resulted in a similar issue, since multiple transactions would read the same value, increment it, and set it to the same value when they should have waited for the first to finish. To fix that, I set up another method on the service,refreshParentAttachmentCount, which runs a query to determine the number of attachments for a given parent and set the parent’s attachmentCount to that number.I’m closing this issue, but if anyone has a better answer feel free to leave a comment.