PersistentObjectException Prevention

I have a very simple JAX-RS service ( BookService class below) that allows you to create objects like Book (also below). POST payload

 { "acquisitionDate": 1418849700000, "name": "Funny Title", "numberOfPages": 100 } 

successfully saves the Book and returns 201 CREATED . However, including the id attribute with any value other than zero in the payload, throws an org.hibernate.PersistentObjectException with the message detached entity passed to persist . I understand what this means , and including id in the payload when creating the object (in this case) does not make sense. However, I would prefer to prevent this exception from popping up completely and present to my users, for example, 400 BAD REQUEST in this case (or at least ignore the attribute at all). However, there are two main problems:

  • The exception that comes to create is an EJBTransactionRolledbackException , and I would have to scan the entire path along the stack EJBTransactionRolledbackException to find the root cause;
  • The main reason is org.hibernate.PersistentObjectException - I am deploying to Wildfly that uses Hibernate, but I want to keep my code portable, so I really don't want to catch this particular exception.

As far as I understand, there are two possible solutions:

  • Use book.setId(null) to bookRepo.create(book) . This ignores the fact that the id attribute carries a value and executes the request.
  • Check if book.getId() != null and throw something like IllegalArgumentException , which can be matched with status code 400 . Seems to be the preferred solution.

However, based on other frameworks (for example, Django Rest Framework, for example), I would prefer it to be handled by the card itself ... Then my question is: is there any built-in way to achieve this behavior that I may be missing?

This is the BookService class:

 @Stateless @Path("/books") public class BookService { @Inject private BookRepo bookRepo; @Context UriInfo uriInfo; @Consumes(MediaType.APPLICATION_JSON) @Path("/") @POST @Produces(MediaType.APPLICATION_JSON) public Response create(@Valid Book book) { bookRepo.create(book); return Response.created(getBookUri(book)).build(); } private URI getBookUri(Book book) { return uriInfo.getAbsolutePathBuilder() .path(book.getId().toString()).build(); } } 

This is the Book class:

 @Entity @Table(name = "books") public class Book { @Column(nullable = false) @NotNull @Temporal(TemporalType.TIMESTAMP) private Date acquisitionDate; @Column(nullable = false, updatable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Integer id; @Column(nullable = false) @NotNull @Size(max = 255, min = 1) private String name; @Column(nullable = false) @Min(value = 1) @NotNull private Integer numberOfPages; (getters/setters/...) } 

This is the BookRepo class:

 @Stateless public class BookRepo { @PersistenceContext(unitName = "book-repo") protected EntityManager em; public void create(Book book) { em.persist(book); } } 
+5
source share
1 answer

I don’t know if this is really the answer you are looking for, but I just played with the idea and implemented something.

The JAX-RS 2 specification defines a model for bean validation, so I thought you could take advantage of this. All bad checks will be mapped to 400. You stated: "I would rather not let this exception swell all the way and introduce my users, for example, 400 BAD REQUEST," but with a bad check you will get it anyway. Thus, however, you plan to handle check exceptions (if at all), you can do the same here.

Basically, I just created a constraint annotation to check for a null value in the id field. You can define the id field name in the annotation through the idField annotation idField , so you are not limited to id . It can also be used for other objects, so you do not need to repeatedly check the value, as you suggested in your second solution.

You can play with him. Just thought that I would drop this option there.

 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.util.logging.Level; import java.util.logging.Logger; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import javax.validation.Payload; @Constraint(validatedBy = NoId.NoIdValidator.class) @Target({ElementType.PARAMETER}) @Retention(RUNTIME) public @interface NoId { String message() default "Cannot have value for id attribute"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String idField() default "id"; public static class NoIdValidator implements ConstraintValidator<NoId, Object> { private String idField; @Override public void initialize(NoId annotation) { idField = annotation.idField(); } @Override public boolean isValid(Object bean, ConstraintValidatorContext cvc) { boolean isValid = false; try { Field field = bean.getClass().getDeclaredField(idField); if (field == null) { isValid = true; } else { field.setAccessible(true); Object value = field.get(bean); if (value == null) { isValid = true; } } } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { Logger.getLogger(NoId.class.getName()).log(Level.SEVERE, null, ex); } return isValid; } } } 

Using:

 @POST @Consumes(MediaType.APPLICATION_JSON) public Response createBook(@Valid @NoId(idField = "id") Book book) { book.setId(1); return Response.created(URI.create("http://blah.com/books/1")) .entity(book).build(); } 

Note that the default idField is id , so if you do not specify it, it will look for the id field in the object's class. You can also specify message , like any other constraint annotation:

 @NoId(idField = "bookId", message = "bookId must not be specified") // default "Cannot have value for id attribute" 
+2
source

Source: https://habr.com/ru/post/1209615/


All Articles