Reputation: 75
I have a question about Jpa and the concurrent access. Here a sample code
User
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
...
UserService
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public String getFirstName(Long userId) throws InterruptedException {
User user = userRepository.findById(userId).get();
Thread.sleep(5000L); // 5s
// ...
return user.getFirstname();
}
@Transactional
public void updateUser(Long userId, String firstName, String lastName) {
User user = userRepository.findById(userId).get();
user.setFirstname(firstName);
user.setLastname(lastName);
userRepository.save(user);
}
}
if I call both endpoints at the same time ( GET before POST), the getFirstName method will overwrite the update because at the end of the transaction JPA will make an implicit save even if there is no update. My update will be overwritten since at the start of my getFirstName method, I load the user with the old first name
Is there a way to disable the sync if we don't call the save method of the repository ? I don't want to detach the entity every time. Moreover I don't want to put the transaction in read only (Maybe I need to update another entity in the getFirstName method).
I don't want JPA auto save my entites if I haven't made any changes. It can be a problem because of the concurrent access.
Thanks
EDIT (solve)
I used a converter for an attribut User
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
@Convert(converter = SimpleConverter.class)
private customObject object;
...
@Component
@Converter
public class SimpleConverter implements AttributeConverter<CustomObject, String> {
private static ObjectMapper objectMapper;
@SneakyThrows
@Override
public String convertToDatabaseColumn(CustomObject object) {
if (Objects.isNull(object)) {
return null;
}
return objectMapper.writeValueAsString(object);
}
@SneakyThrows
@Override
public CustomObject convertToEntityAttribute(String data) {
if (StringUtils.isBlank(data)) {
return null;
}
return objectMapper.readValue(data, CustomObject.class);
}
@Autowired
public void setObjectMapper(ObjectMapper objectMapper) {
TutorialConverter.objectMapper = objectMapper;
}
}
I cannot explain why but with this converter, my entity is update in the database at the end of each transaction even if I didn't make any changes.
I removed this attribute and now getFirstName method does not overwrite data when both methods are executed at the same time
Edit 2
CustomObject class haven't equals method ... Big mistake. That explain this behavior. Thanks !
Upvotes: 1
Views: 4232
Reputation: 103
As pointed out by M. Deinum, you must be changing something on the 'User' object in 'getFirstName' for an update to happen, otherwise, hibernate would not update the entity in the database.
That being said, a better practice, is to return the entire 'User' object from the service
public User getUser(Long userId) throws InterruptedException {
return userRepository.findById(userId).orElse(null); // or throw an expection / return optional
}
You would then use the service:
userService.getUser().getFirstname()
This is much more flexible and implicitly prevents problems like this
Upvotes: 1
Reputation: 21
By removing @Transactional, the persistanceContext will not be flushed. Or you can have @Transactional(readOnly=true). You can read more here: https://vladmihalcea.com/spring-read-only-transaction-hibernate-optimization/
Upvotes: 2