0

Please, look at the two code examples bellow which I'm going to use in my Spring Boot project. They both do merely the same thing - add a new object into users table, represented by User entity with username defined as @Id and a unique constraint imposed on email column (there are some other columns as well, but they are not shown here for brevity). Note: I can't simply use save() method from CrudRepository, because it merges existing record with new object if they both have the same username value. Instead, I need to insert a new object with appropriate exception thrown for duplicate data persistence.

My question is about which option should be given a favor. With EntityManager, I don't need to construct SQL statement. Apart from that obvious observation, are there any advantages which one method may offer over the other (especially, in the matter of performance and resources consumption)?

Also, when I read latest books and tutorials about data persistence in Spring Boot, they mainly focus on Spring Data JPA. For example, the 5th edition of "Spring in Action" has no word about Hibernate's EntityMnager. Does it mean that dealing with Hibernate directly can be regarded as kind of "old school" and should generally be avoided in modern projects?

Option #1: Hibernate's EntityManager

@RestController
@RequestMapping(path = "/auth/register", produces = "application/json")
@Transactional
public class RegistrationController {

    @PersistenceContext
    EntityManager entityManager;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Map<String, String> registerNewUser(@RequestBody @Valid User newUser) {

        try {
            entityManager.persist(newUser);
            entityManager.flush();
        } catch (PersistenceException ex) {
            // parse exception to find out which constraints have been 
            // broken - either it's duplicate username, email or both
            String message = parseExceptionForConstraintNames(ex);
            throw new ResponseStatusException(HttpStatus.CONFLICT, messsage);
        }
        
        return Collections.singletonMap("message", "Success..."); 
    }

}

Option #2: custom @Query from CrudRepository

@RestController
@RequestMapping(path = "/auth/register", produces = "application/json")
public class RegistrationController {

    private final UsersRepository usersRepository;

    @Autowired
    public RegistrationController(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Map<String, String> registerNewUser(@RequestBody @Valid User newUser) {

        try {
            usersRepository.insert(newUser);
        } catch (DataIntegrityViolationException ex) {
            // parse exception to find out which constraints have been 
            // broken - either it's duplicate username, email or both
            String message = parseExceptionForConstraintNames(ex);
            throw new ResponseStatusException(HttpStatus.CONFLICT, message);
        }
        
        return Collections.singletonMap("message", "Success..."); 
    }

}
public interface UsersRepository extends CrudRepository<User, String> {

    @Modifying
    @Transactional
    @Query(nativeQuery = true, value = "INSERT INTO users (username, email) " +
            "VALUES (:#{#user.username}, :#{#user.email})")
    void insert(@Param("user") User newUser);

}
escudero380
  • 536
  • 2
  • 7
  • 25

2 Answers2

2

See this answer for Using JPA repository vs Entity Manager.

Best practice is to not use Repository directly. use Service layer between controller and repository where you can implement the logic for duplicate entries by checking if the record already exist in DB using findByUsername(String username); throw exception if it already exist else save() the object in DB

Aqib Nabi
  • 192
  • 2
  • 10
  • 2
    I agree, adding service layer should generally be considered as a "must have". – escudero380 Sep 18 '20 at 13:37
  • 1
    As far as `findByUsername()` then `save()` approach... it's what I actually was trying to avoid, because this _check-then-act_ logic might result in concurrency problems. Imagine, two parallel requests are trying to add "jeremy" to the database. Thread A finds no user with such name and then invokes `save()`, but in between thread B manages to save its "jeremy"... As a result `save()` from thread A throws no exception and merges its "jeremy" with the one that has just been added by thread B. This would require additional locking to mange such situations. – escudero380 Sep 18 '20 at 13:45
  • 2
    @escudero380 So you need optimistic locking (`@Version`) to tackle the problem. – Oleksii Valuiskyi Sep 18 '20 at 14:03
  • OK. It sounds reasonable, I could also wrap the whole check-then-act transaction with `@Retryable(ObjectOptimisticLockingFailureException.class)` annotation, and it would save me from a lot of pain as to figuring out which constraint was exactly violated, because what I don't really like in both code examples from my post is that I have to parse causes of `DataIntegrityViolationException` or `ResponseStatusException` to find our which constraint was exactly violated. On the other hand, if I want to use optimistic locking, I need to add a special `version` field to the table. – escudero380 Sep 18 '20 at 14:50
  • @escudero380 I always make editable entities extended from class annotated as MappedSuperclass that have id field and version field – Oleksii Valuiskyi Sep 18 '20 at 18:37
1

With the given requirements, the username filed in the entity never qualifies for the @Id.

Why can't u add an explicit id field with some sequence generator for the id filed and just keep the username marked with unique constraint only.

Vikas P R
  • 101
  • 8
  • I agree with you. I should have chosen this option from the start :) But let's assume for a moment, I need to deal with the `users` table from existing database that cannot be altered in the process. – escudero380 Sep 18 '20 at 12:40
  • If you cannot alter the existing table then implement `Persistable`, create a transient isNew() method which returns true by default. Also add a method annotated with `@PostLoad` where set the return value of `isNew` to false. So all of your newly created entities will be treated as new objects during calling Spring Data JPA save() method: they will be persisted. All of the entities loaded from he DB will be treated as existing objects, so they will be merged when you pass them to the save() method. – Selindek Sep 19 '20 at 12:54