1

Here is my code.

Entit class:

@Entity
public class Book
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long bookId;
    private double bookPrice;
    private String bookTitle;
    private String bookAuthor;
    private String bookLanguage;
    private LocalDate publicationDate;
    private String publisher;
    private byte[] bookImage;
    private long isbn;
    private int bookQuantity;

Controller class:

@PutMapping("/updatebooks")
    public ResponseEntity<ApiResponse> updateBook(@RequestBody BookDto bookDto)
            throws DataNotFoundException
    {
        return ResponseEntity.ok(bookService.updateBook(bookDto));
    }

Service class:

@Override
    public ApiResponse updateBook(BookDto bookDto) throws DataNotFoundException
    {
        Book book = bookRepository.findById(bookDto.getBookId())
                .orElseThrow(() -> new DataNotFoundException("Book not found"));
            book.setBookAuthor(bookDto.getBookAuthor());
            book.setBookLanguage(bookDto.getBookLanguage());
            book.setBookPrice(bookDto.getBookPrice());
            book.setBookTitle(bookDto.getBookTitle());
            book.setIsbn(bookDto.getIsbn());
            book.setPublicationDate(bookDto.getPublicationDate());
            book.setPublisher(bookDto.getPublisher());
            bookRepository.save(book);
            return new ApiResponse(HttpStatus.OK.value(), "Updation successful");
    }

So through postman I just want to update bookAuthor field alone and other fields has to be same as it is in the database. But when I update just one field the others are automatically assigned as null and I just want to update only one field.

Postman ScreenShot

Here see that i'm just updating the bookAuthor field but others are simply changing to null. So how can I update only particular fields and display the others as as it is in database.

Pre Updation DB: Pre Updation DB

Post Updation DB: Post Updation DB:

3 Answers3

0
@Modifying
@Query(nativeQuery = true, value = "update book set book_author = :bookAuthor where id = :bookId")
int updateBookAuthor(@Param("bookAuthor") String bookAuthor, @Param("bookId") Long bookId);

You can do something like this in your BookRepository, then invoke from the service class like...

bookRepository.updateBookAuthor(bookAuthor, bookId)

Or modify your service class method like the following...

    @Override
    public ApiResponse updateBook(BookDto bookDto) throws DataNotFoundException
    {
        Book book = bookRepository.findById(bookDto.getBookId())
                .orElseThrow(() -> new DataNotFoundException("Book not found"));
        book.setBookAuthor(bookDto.getBookAuthor());
        bookRepository.save(book);
        return new ApiResponse(HttpStatus.OK.value(), "Updation successful");
    }
Yaseen
  • 78
  • 2
  • 9
  • So, I have to write separate query methods for each fields?? – Arivazhagan Mar 15 '21 at 10:14
  • Not really. You can do what I am saying in the last portion of my answer. I have updated my answer. Set only those fields which need to be mutated. – Yaseen Mar 15 '21 at 10:22
0

Don't get the fields from the DTO. Just Do findByID, set the new bookAuthor to the entity and save.

0

This is not really springboot jpa problem. The issue is in the way updateBook method in service has been implemented. The updateBook method is setting all the fields from dto on persisted book entity without checking if the dto.get* returns null or not.

Very simply but crude solution to this problem would be to have if(dto.get* != null) check for each field before setting the value in book entity. i.e.

if (bookDto.getBookAuthor() != null) 
    book.setBookAuthor(bookDto.getBookAuthor());
if (bookDto.getBookLanguage() != null)
    book.setBookLanguage(bookDto.getBookLanguage());
if (bookDto.getBookPrice() != 0)
    book.setBookPrice(bookDto.getBookPrice());
...

This leaves the updateBook service method generic to allow updating one or more fields without worrying about others being overwritten. If this makes your service method too noisy the dto to entity conversation part can be extracted into its own method for clarity.

For more advance usecases and if your entity/dto has more than a handful fields or nested objects as fields then this becomes cumbersome. In such scenarios you may want to handcraft a separate implementation which perhaps uses reflection to map fields from dto to entity if the field value is not null. The usage of reflection is however likely to be slow and/or error prone if not done properly.

There are however libraries and frameworks for more regular usecases which makes such conversation easier. At the simplest springframework's BeanUtils.copyProperties method provides a way to map values from one object to another and the last argument of ignored properties can be used to provide fields names to be ignored. This stackoverflow answer shows how to write a generic method that creates a list of property names to ignore if they are null in source object; and they uses this list to pass as parameter in BeanUtils.copyProperties.

public static String[] getNullPropertyNames (Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<>();
    for(PropertyDescriptor pd : pds) {
        Object srcValue = src.getPropertyValue(pd.getName());
        if (srcValue == null) emptyNames.add(pd.getName());
    }
    return emptyNames.toArray(new String[0]);
}

then in your service method;

Book book = bookRepository.findById(bookDto.getBookId())
            .orElseThrow(() -> new DataNotFoundException("Book not found"));
BeanUtils.copyProperties(bookDto, book, getNullPropertyNames(bookDto));
bookRepository.save(book);

For further advanced usecases you can use frameworks such as mapstruct or modelmapper.

madteapot
  • 2,208
  • 2
  • 19
  • 32