I have a very basic JAX-RS service (the BookService
class below) which allows for the creation of entities of type Book
(also below). POST
ing the payload
{
"acquisitionDate": 1418849700000,
"name": "Funny Title",
"numberOfPages": 100
}
successfully persists the Book
and returns 201 CREATED
. However, including an id
attribute with whichever non-null value on the payload triggers an org.hibernate.PersistentObjectException
with the message detached entity passed to persist
. I understand what this means, and including an id
on the payload when creating an object (in this case) makes no sense. However, I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST
in this case (or, at least, ignore the attribute altogether). However, there are two main concerns:
create
is an EJBTransactionRolledbackException
and I'd have to crawl all the way down the stack trace to discover the root cause;org.hibernate.PersistentObjectException
- I'm deploying to Wildfly which uses Hibernate, but I want to maintain my code portable, so I don't really want to catch this specific exception.To my understanding, there are two possible solutions:
book.setId(null)
before bookRepo.create(book)
. This would ignore the fact that the id
attribute carries a value and proceed with the request.book.getId() != null
and throw something like IllegalArgumentException
that could be mapped to a 400
status code. Seems the preferable solution.However, coming from other frameworks (like Django Rest Framework, for example) I'd really prefer this to be handled by the framework itself... My question then is, is there any built-in way to achieve this behaviour 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);
}
}
I don't know if this is really the answer you're looking for, but I was just playing around with the idea and implemented something.
The JAX-RS 2 spec defines a model for bean validation, so I thought maybe you could tap into that. All bad validations will get mapped to a 400. You stated "I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST", but with bad validation you will get that anyway. So however you plan to handle validation exceptions (if at all), you can do the same here.
Basically I just created a constraint annotation to validate for a null value in the id field. You can define the id field's name in the annotation through the idField
annotation attribute, so you are not restricted to id
. Also this can be used for other objects too, so you don't have to repeatedly check the value, as you suggested in your second solution.
You can play around with it. Just thought I'd throw this option out 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;
}
}
}
Usage:
@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 the default idField
is id
, so if you don't specify it, it will look for the id
field in the object class. You can also specify the message
as you would any other constraint annotation:
@NoId(idField = "bookId", message = "bookId must not be specified")
// default "Cannot have value for id attribute"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With