0

I have this model -

public class Product implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @NotBlank
    @Column(length = 500, nullable = false)
    private String name;

    @Column(nullable = false)
    private double buyingPrice;
    
    //getters and setters
}

These are the two controller methods responsible for edit/update product -

@Controller
@RequestMapping("/product")
@SessionAttributes("product")
public class ProductController {

    @GetMapping(value = "/edit")
    public String edit(@RequestParam("id") int id, ModelMap model) {
        Product product = productService.getById(id);
        model.put("product", product);

        return PRODUCT_EDIT_PAGE;
    }

    @PostMapping(value = "/edit")
    public String update(@Valid @ModelAttribute("product") Product product,
                         BindingResult result,
                         ModelMap model) {

        //I get `product.getName()` here
        
        if (result.hasErrors()) {

            //no matter what I set product name in the edit page
            //`BindingResult` always has this error - 
            //[Field error in object 'product' on field 'name': rejected value [null] ...];
            
            model.put("product", product);
            return PRODUCT_EDIT_PAGE;
        }

        productService.save(product);

        return "redirect:/product/list";
    }
}

This is the edit page -

<form:form method="POST" modelAttribute="product">
    <form:input type="text" path="name"/>
    <form:errors path="name"/>

    <form:input type="number" path="buyingPrice"/>
    <form:errors path="buyingPrice"/>

    <button type="submit">
</form:form>

No matter what I set product name in the edit page BindingResult always has this error -

[Field error in object 'product' on field 'name': rejected value [null]; codes [NotBlank.product.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.name,name]; arguments []; default message [name]]; default message [must not be blank]]

Even though I get product.getName() in the POST controller method! This weird behavior is occurring only when updating an existing product not when creating a new product.

Update 1:

I noticed while debugging that in the post method @ModelAttribute gets a hibernate proxy object of Product. A hibernate interceptor [ByteBuddyInterceptor] is intercepting the request. I guess this might be the issue.

here is the debug screen shot

Update 2:

My service class -

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Product getById(int id) {
        return productRepository.getOne(id);
    }
}

My Repository -

public interface ProductRepository extends JpaRepository<Product, Integer> {
    List<Product> getAllByUser(User user);
}
Erfan Ahmed
  • 1,536
  • 4
  • 19
  • 34
  • 1
    Does your service method `getById` by any chance also call `getById` on the repository (and assuming you are using Spring Data JPA here). If so use `findById` instead of `getById`. The difference `getById` will return a proxy instead of actually going to the database (well that is delayed upon first access), storing that in the `HttpSession` (due to `@SessionAttributes`) will break as on the next request the hibernate session has been closes already (as that is bound to a transaction or request). Tip in your post method you don't need `model.put("product", product);` it's already in the model. – M. Deinum Apr 26 '21 at 06:54
  • @M.Deinum spot on! silly me! I should've noticed it! I was calling `.getOne()` method of `JpaRepository` which was giving a proxy object; `findById()` solves the issue. Please add this as an answer I'll accept it. – Erfan Ahmed Apr 26 '21 at 09:12

2 Answers2

1

I think you are looking up for the @NotBlank annotation, am I wrong?

Here is the difference:

  • @NotEmpty: a constrained CharSequence, Collection, Map, or Array is valid as long as it's not null and its size/length is greater than zero
  • @NotBlank: a constrained String is valid as long as it's not null and the trimmed length is greater than zero

This probably will not fix the error but I just wanted to inform you about the difference.

Julian
  • 86
  • 1
  • 8
  • Thanks for pointing out the difference and yes I mistakenly put `@NotEmpty` instead of `@NotBlank` but that does not solve the issue. I don't know why or how `name` becomes `null` in the process of validation! – Erfan Ahmed Apr 26 '21 at 04:19
1

I suspect an issue with Hibernate, lazy proxies and storing the latter in the HttpSession.

Judging from the service method you call, productService.getById(id);, I suspect you are also calling getById (or getOne when using an older Spring Data version) which will return a lazy proxy from Hibernate. This proxy will only access the database when methods are called on it (like getName).

As you are storing this proxy inside the HttpSession due to @SessionAttributes the original Hibernate session has already been closed (and it is a proxy). So the field will actually be empty (as the proxy doesn't really have the value, the enclosed object has).

What you should do is either use a separate DTO to overcome this issue or not use getById/getOne but rather findById to return a direct entity.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224