防止' PersistentObjectException'

时间:2014-12-20 16:22:40

标签: java java-ee jpa jax-rs

我有一个非常基本的JAX-RS服务(下面是BookService类),它允许创建Book类型的实体(也在下面)。 POST有效载荷

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

成功保留Book并返回201 CREATED。但是,包含有效负载上的任何非空值的id属性会触发org.hibernate.PersistentObjectException消息detached entity passed to persist我理解这意味着什么,并且在创建对象时(在这种情况下)在有效负载上包含id毫无意义。但是,我更倾向于防止此异常一直向上冒泡,并在这种情况下向我的用户显示400 BAD REQUEST(或者,至少完全忽略该属性)。但是,有两个主要问题:

  1. 到达create的异常是EJBTransactionRolledbackException,我必须一直爬下堆栈跟踪以发现根本原因;
  2. 根本原因是org.hibernate.PersistentObjectException - 我正在部署到使用Hibernate的Wildfly,但我想保持我的代码可移植,所以我真的不想抓住这个特定的异常。 / LI>

    据我了解,有两种可能的解决方案:

    1. book.setId(null)之前使用bookRepo.create(book)。这将忽略id属性带有值并继续处理请求的事实。
    2. 检查是否book.getId() != null并抛出可以映射到IllegalArgumentException状态代码的400之类的内容。似乎是更好的解决方案。
    3. 然而,来自其他框架(例如Django Rest Framework)我真的更喜欢这个由框架本身处理......我的问题是,是否有任何内置的方法来实现我可能会遗漏这种行为吗?

      这是BookService类:

      @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();
          }
      }
      

      这是Book类:

      @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/...)
      }
      

      这是BookRepo类:

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

1 个答案:

答案 0 :(得分:2)

我不知道这是否真的是你正在寻找的答案,但我只是在玩这个想法并实施了一些东西。

JAX-RS 2规范定义了bean验证的模型,所以我想也许你可以利用它。所有错误的验证都将映射到400.您说“我更愿意阻止此异常一直冒泡并向我的用户提供例如400 BAD REQUEST”,但是验证不好,无论如何你都会得到它。因此,无论您计划处理验证异常(如果有的话),您都可以在此处执行相同操作。

基本上我只是创建了一个约束注释来验证id字段中的空值。您可以通过idField注释属性在注释中定义id字段的名称,因此您不限于id。此外,它也可以用于其他对象,因此您不必像第二个解决方案中的建议那样重复检查该值。

你可以玩它。我以为我会把这个选项扔出去。

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;
        }
    }
}

用法:

@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();
}

请注意,默认idFieldid,因此如果您不指定它,它将在对象类中查找id字段。您也可以像任何其他约束注释一样指定message

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