Java 防止';PersistentObjectException';

Java 防止';PersistentObjectException';,java,jakarta-ee,jpa,jax-rs,Java,Jakarta Ee,Jpa,Jax Rs,我有一个非常基本的JAX-RS服务(下面的BookService类),它允许创建Book类型的实体(也在下面)POST装载有效负载 { "acquisitionDate": 1418849700000, "name": "Funny Title", "numberOfPages": 100 } 成功保存书籍,并返回创建的201。但是,在有效负载上包含带有非空值的id属性会触发org.hibernate.PersistentObjectException,并将消息分离实体

我有一个非常基本的JAX-RS服务(下面的
BookService
类),它允许创建
Book
类型的实体(也在下面)<代码>POST装载有效负载

{
    "acquisitionDate": 1418849700000,
    "name": "Funny Title",
    "numberOfPages": 100
}
成功保存
书籍
,并返回创建的
201
。但是,在有效负载上包含带有非空值的
id
属性会触发
org.hibernate.PersistentObjectException
,并将消息
分离实体传递给persist
我理解这意味着什么,在创建对象(在本例中)时,在有效负载上包含一个
id
是没有意义的。但是,我更愿意防止这个异常一直冒泡,并向我的用户提供一个
400错误请求
(或者至少完全忽略该属性)。然而,有两个主要问题:

  • 到达
    create
    的异常是一个
    EJBTransactionRolledbackException
    ,我必须沿着堆栈跟踪一直爬行以发现根本原因
  • 根本原因是
    org.hibernate.PersistentObjectException
    ——我正在部署使用hibernate的Wildfly,但我想保持代码的可移植性,所以我不想捕获这个特定的异常
  • 据我了解,有两种可能的解决方案:

  • bookRepo.create(book)
    之前使用
    book.setId(null)
    。这将忽略
    id
    属性携带一个值并继续请求这一事实
  • 检查是否
    book.getId()!=null
    并抛出类似于
    IllegalArgumentException
    的内容,该内容可以映射到
    400
    状态代码。似乎是更好的解决办法
  • 然而,来自其他框架(比如Django Rest框架),我真的更喜欢由框架本身来处理。。。那么,我的问题是,是否有任何内在的方式来实现这种行为,我可能会错过

    这是
    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();
        }
    }
    
    @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/...)
    }
    
    @Stateless
    public class BookRepo {
        @PersistenceContext(unitName = "book-repo")
        protected EntityManager em;
    
        public void create(Book book) {
            em.persist(book);
        }
    }
    
    这是
    书籍
    课程:

    @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();
        }
    }
    
    @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/...)
    }
    
    @Stateless
    public class BookRepo {
        @PersistenceContext(unitName = "book-repo")
        protected EntityManager em;
    
        public void create(Book book) {
            em.persist(book);
        }
    }
    
    这是
    BookRepo
    类:

    @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();
        }
    }
    
    @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/...)
    }
    
    @Stateless
    public class BookRepo {
        @PersistenceContext(unitName = "book-repo")
        protected EntityManager em;
    
        public void create(Book book) {
            em.persist(book);
        }
    }
    

    我不知道这是否真的是你想要的答案,但我只是在玩弄这个想法并实现了一些东西

    JAX-RS2规范定义了一个用于bean验证的模型,所以我想您可以利用它。所有错误的验证都将映射到400。您声明“我更愿意防止这个异常冒泡,并向我的用户提供一个400个错误的请求”,但如果验证不正确,您将得到它。因此,无论您计划如何处理验证异常(如果有),您都可以在这里执行相同的操作

    基本上,我只是创建了一个约束注释来验证id字段中的空值。您可以通过
    idField
    annotation属性在注释中定义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;
            }
        }
    }
    
    请注意,默认的
    idField
    id
    ,因此如果不指定它,它将在对象类中查找
    id
    字段。您还可以像指定任何其他约束注释一样指定
    消息

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