Partial Update using Spring Boot

175 Views Asked by At

I am working on a Java Spring Boot project. I have an entity that has 20 fields. I currently have an put method that takes these 20 fields, all of them are non-nulls and I validate them using validators including my custom validators. But it is costly. Even if I want to change a single field in my db, I completely fetch the row, update all fields using custom mapper and save into db. In this implementation, even if only one field changes, I have to send all fields in request and also I overwrite 19 unchanged field into db which is costly. I am looking for solutions to:

  1. I should be able to send only fields that is supposed to change and I should be able to validate them in DTO level using validator (for example: I should be able to check if a values between 0 and 100. In current implementation I have a custom validator for this)
  2. I should be able to overwrite only changed fields into db. I shouldnt overwrite values that should remain same.
  3. (kind of optional) It would be better if it can be generic. I can easily adapt for my different controllers.

I have been searching Java Reflections and found this implementation:

    @PatchMapping("/{id}")
    public Product updateProductFields(@PathVariable int id,@RequestBody Map<String, Object> fields){
        return service.updateProductByFields(id,fields);
    }
    public Product updateProductByFields(int id, Map<String, Object> fields) {
        Optional<Product> existingProduct = repository.findById(id);

        if (existingProduct.isPresent()) {
            fields.forEach((key, value) -> {
                Field field = ReflectionUtils.findField(Product.class, key);
                field.setAccessible(true);
                ReflectionUtils.setField(field, existingProduct.get(), value);
            });
            return repository.save(existingProduct.get());
        }
        return null;
    }

public interface ProductRepository extends
        JpaRepository<Product, Integer> {


}

But I am not sure if this satisfies my desires. Do you have any suggestions?

1

There are 1 best solutions below

4
lance-java On

Personally I prefer type safety so don't like dealing with Map<String, Object>.

You mentioned in a comment that you want to do this for many different DTO's, each with many fields. I think you should consider generating code using an annotation processor. This would allow you to generate an XDelta and an XMerger for each of your model classes.

eg You could annotate the Product class with a custom @Delta annotation

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Delta {
}

@Delta
public class Product {
   @Id
   private Long id;
   
   @NotEmpty 
   private String name;
   
   @GreaterThan(0) 
   private BigDecimal price;
}

The annotation processor would then generate

@Generated("generated by DeltaAnnotationProcessor")
public class ProductDelta {
   public Long getId() { ... }

   public void setName(String name) { ... }
   public boolean isNameSet() { ... }
   public String getName() { ... }

   public void setPrice(BigDecimal price) { ... }
   public boolean isPriceSet() { ... }
   public BigDecimal getPrice() { ... }
}

@Generated("generated by DeltaAnnotationProcessor")
public class ProductMerger {
   public Product merge(Product product, ProductDelta delta) { 
      String name = delta.isNameSet() ? delta.getName() : product.getName();
      BigDecimal price = delta.isPriceSet() ? delta.getPrice() : product.getPrice();
      return new Product(product.getId(), name, price, ...);
   }
}

For generating code I usually use freemarker