Spring JPA repository: prevent update on save
Asked Answered
T

4

12

My user DB table looks like this:

CREATE TABLE user (
    username VARCHAR(32) PRIMARY KEY,
    first_name VARCHAR(256) NOT NULL,
    last_name VARCHAR(256) NOT NULL,
    password VARCHAR(32) NOT NULL,
    enabled BOOL
) ENGINE = InnoDB;

This is the field definitions of my entity:

@Entity
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String firstName;

    @Column(nullable = false)
    private String lastName;

    @Column(nullable = false)
    private String password;

The field username is the key of my table/entity and it's up to me to set its value. When I need to create another user, I do this in my service:

public User insertUserImpl(String username, String firstName, String lastName) {
    Assert.hasText(username);
    Assert.hasText(firstName);
    Assert.hasText(lastName);

    String password = UUID.randomUUID().toString().substring(0, 4); // temp
    User user = new User(username, password);
    user.setFirstName(firstName);
    user.setLastName(lastName);
    user.setEnabled(false);
    this.userRepository.save(user);
    // FIXME - assegnare un ruolo
    return user;
}

Anyway, if the username is already taken, the repository just do an update, because the specified identifier is not null. This is not the behaviour that I want, I need it to throw something like a duplicate entry exception. Is there any way to prevent it? Do I have to do it by myself? E.g.:

User user = this.userRepository.findOne(username);
if(user != null) {
    throw new RuntimeException("Username already taken"); // FIXME - eccezione applicativa
}
Tyranny answered 5/3, 2016 at 17:50 Comment(2)
If it is updating the value instead of making a new insertion, it means it thinks it is the same object, but with updated values, are you reusing the object to make several insertions? what flush mode are you using?Bedelia
I'm not reusing the same object, I edited the text so you can see the full method. How can I check my flush mode? I think it's the Spring Boot default, I didn't set anything about it.Tyranny
B
20

When using the default configuration, and using CrudRepository#save() or JpaRepository#save() it will delegate to the EntityManager to use either persists() if it is a new entity, or merge() if it is not.

The strategy followed to detect the entity state, new or not, to use the appropiate method, when using the default configuration is as follows:

  • By default, a Property-ID inspection is performed, if it is null, then it is a new entity, otherwise is not.
  • If the entity implements Persistable the detection will be delegated to the isNew() method implemented by the entity.
  • There is a 3rd option, implementing EntityInformation, but further customizations are needed.

source

So in your case, as you are using the username as ID, and it isn't null, the Repository call ends up delegating to EntityManager.merge() instead of persist(). So there are two possible solutions:

  • use a diferent ID property, set it to null, and use any auto-generation method, or
  • make User implement Persistable and use the isNew() method, to determine if it is a new entity or not.

If for some reason, you don't want to modify your entities, you can also change the behaviour modifying the flush mode configuration. By default, in spring data jpa, hibernate flush mode is set to AUTO. What you want to do is to change it to COMMIT, and the property to change it is org.hibernate.flushMode. You can modify this configuration by overriding a EntityManagerFactoryBean in a @Configuration class.


And if you don't want to mess the configuration of the EntityManager, you can use the JpaRepository#flush() or JpaRepository#saveAndFlush() methods, to commit the pending changes to the database.

Bedelia answered 6/3, 2016 at 16:4 Comment(1)
Could someone summarize, please, there is some sort of insert-only annotation I can put on a JpaRepository class to make entity non-updatable? Doesn't org.hibernate.flushMode apply to whole project?Clarenceclarenceux
C
2

One can perhaps use existsById(ID primaryKey) to test it, if userRepository extends CrudRepository:

if(userRepository.existsById(username)){
    //Throw your Exception
} else {
    this.userRepository.save(user);
}

see https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

Calibrate answered 9/1, 2019 at 16:35 Comment(1)
Is that thread-safe - will it catch the case where the new user has been created but not committed in another thread when we do the existsById query?Socha
R
0

Instead of this.userRepository.save(user) , can you try this.userRepository.saveAndFlush(user)

My best guess is, it will make your entity detached and as per the JPA documentation, it states an EntityExistsException is thrown by the persist method when the object passed in is a detached entity. Or any other PersistenceException when the persistence context is flushed or the transaction is committed.

Recognizee answered 5/3, 2016 at 21:59 Comment(0)
P
0

Also you can create custom insert method in repository and use it. So if there would be ID duplicates, you will get erro.

Pneumonic answered 10/9 at 12:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.