1. Overview

Spring JPA simplifies the interaction with a database and makes communication transparent. However, default Spring implementations sometimes need adjustments based on application requirements.

In this tutorial, we’ll learn how to implement a solution that won’t allow updates by default. We’ll consider several approaches and discuss the pros and cons of each.

2. Default Behavior

The save(T) method in JpaRepository<T, ID> behaves as upsert by default. This means it would update an entity if we already have it in the database:

@Transactional
@Override
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Based on the ID, if this is the first insert, it would persist the entity. Otherwise, it’ll call the merge(S) method to update it.

3. Service Check

The most obvious solution for this problem is explicitly checking if an entity contains an ID and choosing an appropriate behavior. It’s a little bit more invasive solution, but at the same time, this behavior is often dictated by the domain logic.

Thus, although this approach would require an if statement and a couple of lines of code from us, it’s clean and explicit. Also, we have more freedom to decide what to do in each case and aren’t restricted by the JPA or database implementations:

@Service
public class SimpleBookService {
    private SimpleBookRepository repository;

    @Autowired
    public SimpleBookService(SimpleBookRepository repository) {
        this.repository = repository;
    }

    public SimpleBook save(SimpleBook book) {
        if (book.getId() == null) {
            return repository.save(book);
        }
        return book;
    }

    public Optional<SimpleBook> findById(Long id) {
        return repository.findById(id);
    }
}

4. Repository Check

This approach is similar to the previous one but moves the check directly into the repository. However, if we don’t want to provide the implementation for the save(T) method from scratch, we need to implement an additional one:

public interface RepositoryCheckBookRepository extends JpaRepository<RepositoryCheckBook, Long> {
    default <S extends RepositoryCheckBook> S persist(S entity) {
        if (entity.getId() == null) {
            return save(entity);
        }
        return entity;
    }
}

Note that this solution would work only when the database generates the IDs. Thus, we can assume that an entity with an ID has already persisted, which is a reasonable assumption in most cases. The benefit of this approach is that we’re more in control over the resulting behavior. We’re silently ignoring the update here, but we can change the implementation if we want to notify a client.

5. Using EntityManager

This approach also requires a custom implementation, but we’ll use the EntityManger directly. It also might provide us with more functionality. However, we must first create a custom implementation because we cannot inject beans into an interface. Let’s start with an interface:

public interface PersistableEntityManagerBookRepository<S> {
    S persistBook(S entity);
}

After that, we can provide an implementation for it. We’ll be using @PersistenceContext, which behaves similar to @Autowired, but more specific:

public class PersistableEntityManagerBookRepositoryImpl<S> implements PersistableEntityManagerBookRepository<S> {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public S persist(S entity) {
        entityManager.persist(entity);
        return entity;
    }
}

It’s important to follow the correct naming convention. The implementation should have the same name as the interface but end with Impl. To tie all the things together, we need to create another interface that would extend both our custom interface and JpaRepository<T, ID>:

public interface EntityManagerBookRepository extends JpaRepository<EntityManagerBook, Long>, 
  PersistableEntityManagerBookRepository<EntityManagerBook> {
}

If the entity had an ID, the persist(T) method would throw InvalidDataAccessApiUsageException caused by PersistentObjectException.

6. Using Native Queries

Another way to alter the default behavior of JpaRepository<T> is to use @Query annotations. As we cannot use JPQL for insert queries, we’ll use native SQL:

public interface CustomQueryBookRepository extends JpaRepository<CustomQueryBook, Long> {
    @Modifying
    @Transactional
    @Query(value = "INSERT INTO custom_query_book (id, title) VALUES (:#{#book.id}, :#{#book.title})",
      nativeQuery = true)
    void persist(@Param("book") CustomQueryBook book);
}

This will force a specific behavior on the method. However, it has several issues. The main problem is that we must provide an ID, which is impossible if we delegate its generation to the database. Another thing is connected to the modifying queries. They can return only void or int, which might be inconvenient.

Overall, this method would cause DataIntegrityViolationException due to ID conflicts. This might create an overhead. Additionally, the method’s behavior isn’t straightforward, so this approach should be avoided when possible.

7. Persistable<ID> Interface

We can achieve a similar result by implementing a Persistable<ID> interface:

public interface Persistable<ID> {
    @Nullable
    ID getId();
    boolean isNew();
}

Simply put, this interface allows adding custom logic while identifying if the entity is new or already exists. This is the same isNew() method we’ve seen in the default save(S) implementation.

We can implement this interface and always tell JPA that the entity is new:

@Entity
public class PersistableBook implements Persistable<Long> {
    // fields, getters, and setters
    @Override
    public boolean isNew() {
        return true;
    }
}

This would force save(S) to pick persist(S) all the time, throwing an exception in case of ID constraint violation. This solution would generally work, but it might create problems, as we’re violating the persistence contract, considering all the entities to be new.

8. Non-Updatable Fileds

The best approach is to define the fields as non-updatable. This is the cleanest way to handle the problem and allows us to identify only those fields we want to update. We can use @Column annotation to define such fields:

@Entity
public class UnapdatableBook {
    @Id
    @GeneratedValue
    private Long id;
    @Column(updatable = false)
    private String title;

    private String author;

    // constructors, getters, and setters
}

JPA will silently ignore these fields while updating. At the same time, it still allows us to update other fields:

@Test
void givenDatasourceWhenUpdateBookTheBookUpdatedIgnored() {
    UnapdatableBook book = new UnapdatableBook(TITLE, AUTHOR);
    UnapdatableBook persistedBook = repository.save(book);
    Long id = persistedBook.getId();
    persistedBook.setTitle(NEW_TITLE);
    persistedBook.setAuthor(NEW_AUTHOR);
    repository.save(persistedBook);
    Optional<UnapdatableBook> actualBook = repository.findById(id);
    assertTrue(actualBook.isPresent());
    assertThat(actualBook.get().getId()).isEqualTo(id);
    assertThat(actualBook.get().getTitle()).isEqualTo(TITLE);
    assertThat(actualBook.get().getAuthor()).isEqualTo(NEW_AUTHOR);
}

We didn’t change the title of the book but successfully updated the author of the book.

9. Conclusion

Spring JPA not only provides us with convenient tools to interact with databases but is also highly flexible and configurable. We can use many different methods to alter the default behavior and fit the needs of our application.

Picking the proper method for a specific situation requires deep knowledge of available functionality.

As usual, all the code used in this tutorial is available over on GitHub.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.