mk_89
mk_89

Reputation: 2742

Spring hibernate CrudRepository make the save method update based on a unique constraint

I understand by default that the CrudRepository.save method inserts and updates based on a primary key.

Consider the following entity

@Entity
public class BookEntity {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;

    @Basic
    @Column(name = "isbn", unique = true)
    private String isbn;

    @Basic
    @Column(name = "title")
    private String title;

    @Basic
    @Column(name = "author")
    private String author;

    @Basic
    @Column(name = "publication_date")
    private Date publicationDate;

    @Basic
    @Column(name = "rank")
    private Integer rank;
}

Because I can not have books with the same isbn in the table and I don't want to worry about generating ids I normally post the following json to an enpoint

{ "isbn": "10932011", "title": "harry", "author": "jk", "publicationDate": "2018-10-10", "rank": 1000 }

Which returns the following with an auto generated id

{ "id": 3, "isbn": "10932011", "title": "harry", "author": "jk", "publicationDate": "2018-10-10T00:00:00.000+0000", "rank": 1000 }

If I make second call using the same isbn I'll get a constraint error

{ "isbn": "10932011", "title": "harry", "author": "jk3", "publicationDate": "2018-10-10", "rank": 1000 }

But I would like to in fact update the book with the same isbn and not have to specify the auto generated id in the post json as this is not important to me. Is there a way of doing this without having to add logic to a service class?

Upvotes: 5

Views: 4946

Answers (3)

lepe
lepe

Reputation: 25210

The best way I have found is based in this article using @NaturalId:

Step 1. Create a NaturalRepository interface (to fine-tune the built-in JpaRepository):

@NoRepositoryBean
interface NaturalRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
    Optional<T> findBySimpleNaturalId(ID naturalId);
}

Step 2. Extend SimpleJpaRepository and implement NaturalRepository(to customize it):

import org.hibernate.Session;

@Transactional(readOnly = true)
class NaturalRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements NaturalRepository<T, ID> {
    final EntityManager entityManager;

    NaturalRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
    }

    @Override
    Optional<T> findBySimpleNaturalId(ID naturalId) {

        return entityManager.unwrap(Session.class)
            .bySimpleNaturalId(this.getDomainClass())
            .loadOptional(naturalId);
    }
}

Step 3. Tell Spring to use this customized repository base:

@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = NaturalRepositoryImpl.class)
class MySpringApp {
...

Step 4. Time to use @NaturalId:

@Entity
public class BookEntity {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;

    @NaturalId
    @Column(name = "isbn")
    private String isbn;

Step 5. Define a classical Spring repository:

@Repository
interface BookRepository extends NaturalRepository<Book, String> {
    // You can add other methods here like:
    List<Book> findByTitle(String title);
    List<Book> findByAuthor(String author);
}

Take note:

  • its now using @Repository (instead of @RepositoryRestResource)
  • that it should now extends NaturalRepository (instead of CrudRepository)
  • the second type should be String

Step 6. Finally:

...
@Autowired
BookRepository repository;
...
String isbn = "...";
try {
    Book book = repository.findBySimpleNaturalId(isbn).get();
    book.author = "gal";
    repository.save(book);
} catch(NoSuchElementException notfound) {
    ...
}

The nice thing is that you can reuse the NaturalRepository and NaturalRepositoryImpl in any other project or with any other repository for other tables.

Upvotes: 1

C. Weber
C. Weber

Reputation: 937

Since you are using Hibernate, you can also take a look at the NaturalId API Hibernate provides. In addition to your generated ID you annotate your isbn as a @NaturalId and then use the NaturalId API to retrieve your books.

@Entity
public class BookEntity {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;

    @NaturalId
    @Column(name = "isbn")
    private String isbn;

Load example:

BookEntity book = entityManager.unwrap(Session.class)
.bySimpleNaturalId(BookEntity.class)
.load("your isbn goes here");

For further reading on NaturalIds take a look at this article (its sample is with isbns) or this one.

A big plus of NaturalIds is you can benefit of hibernate caching mechanisms.

Upvotes: 1

htshame
htshame

Reputation: 7340

You can get the BookEntity, change it and save it.

Since your isbn is unique, you can do something like this:

BookEntity book = bookRepository.findByIsbn(isbn);
book.setWhateverFieldYouWant(value);
bookRepository.save(book);.

Or another solution

You can create a method with @Query annotation in BookRepository:

@Query("UPDATE BookEntity b SET b.whateverField = :value WHERE b.isbn = :isbn")
void updateBook(@Param("value") String value, @Param("isbn") String isbn);

and call it from service:

bookRepository.updateBook(value, isbn);

Upvotes: 2

Related Questions