关于jakarta ee:在Hibernate验证期间执行EntityManager查询的正确方法

Correct way to do an EntityManager query during Hibernate Validation

我有点Java EE / EJB,但是从我收集的文档和其他帖子中,您不能在实体验证期间使用相同的entitymanager / session查询数据库。

In general, the lifecycle method of a portable application should not invoke EntityManager
or Query operations, access other entity instances, or modify relationships within the
same persistence context.[43] A lifecycle callback method may modify the non-relationship
state of the entity on which it is invoked.

请翻译?

这是非常抽象的...可以用更具体的术语来解释吗?它导致的问题多于答案。例如,如果我的实体有一个延迟加载的集合,是否可以在验证期间访问它?该集合是"另一个实体",并且将需要进行数据库查询,这似乎违反了文档。

这个"生命周期要求"看起来很奇怪,因为某些验证确实确实需要查询数据库,这只是生活中的事实。

在其他帖子中,我还看到人们通过使用entitymanagerfactory创建一个新的entitymanager / session来解决此查询问题。

这使我想到两个有关使用EntityManagers和Hibernate Validation的问题:

  • 我是否可能存在某种设计缺陷,或者因为在验证过程中需要查询数据库而滥用了Hibernate Validation?
  • 鉴于我将Java EE与JBoss结合使用,如何将验证器注入EntityManagerFactory?
  • 我尝试过这样的事情:

    1
    2
    3
    4
    5
    6
    7
    @Stateless
    public class UserValidator implements ConstraintValidator<ValidUser, User> {
        @PersistenceUnit(unitName="blahblah")
        EntityManagerFactory emf;

        ...
    }

    但是EMF永远不会被注入。我猜@Stateless标记变得无关紧要,因为我正在实现ConstraintValidator接口,这对于Hibernate Validator起作用是必需的。

    那么从验证器获取EntityManagerFactory的一般模式是什么?

    谢谢!


    通过一些评论和足够的讨论,我终于找到了某种"规范"的方式来回答我的问题。

    但是要弄清楚,我的问题确实是在问两件事,它们有两个不同的答案:

  • 您如何将事物注入到Hibernate Validation框架中使用的Validator中?
  • 假设我们可以注入事物,那么在JPA生命周期事件中注入EntityManagerFactory或EntityManager并将其用于查询是否安全?
  • 首先回答第二个问题,我简单地说,强烈建议在验证过程中使用第二个EntityManager进行查询。这意味着您应该注入一个EntityManagerFactory并为查询创建一个新的EntityManager(而不是注入一个EntityManager,后者将与创建生命周期事件的对象相同)。

    通常来说,出于验证目的,您无论如何都只会查询数据库,而不会插入/更新,因此这样做相当安全。

    我在这里问了一个非常相关的SO问题。

    现在回答问题1。

    是的,完全有可能将事物注入到Hibernate Validation框架中使用的Validator中。为此,您需要做3件事:

  • 创建一个自定义ConstraintValidatorFactory,它将创建框架中使用的验证器(覆盖Hibernate的默认工厂)。 (我的示例使用Java EE,而不是Spring,所以我使用BeanManager,但在Spring中,您可能为此使用ApplicationContext。)
  • 创建一个validate.xml文件,该文件告诉Hibernate Validation框架将哪个类用于ConstraintValidatorFactory。确保此文件最终出现在您的类路径中。
  • 编写一个注入某些东西的验证器。
  • 这是使用\\'managed \\'(可注入)验证器的示例自定义ConstraintValidatorFactory:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.myvalidator;

    public class ConstraintInjectableValidatorFactory implements ConstraintValidatorFactory {

        private static BeanManager beanManager;

        @SuppressWarnings(value="unchecked")
        @Override
        public <T extends ConstraintValidator<?, ?>> T getInstance(Class< T > clazz) {
            // lazily initialize the beanManager
            if (beanManager == null) {
                try {
                    beanManager = (BeanManager) InitialContext.doLookup("java:comp/BeanManager");
                } catch (NamingException e) {
                    // TODO what's the best way to handle this?
                    throw new RuntimeException(e);
                }
            }

            T result = null;

            Bean< T > bean = (Bean< T >) beanManager.resolve(beanManager.getBeans(clazz));
            // if the bean/validator specified by clazz is not null that means it has
            // injection points so get it from the beanManager and return it. The validator
            // that comes from the beanManager will already be injected.
            if (bean != null) {
                CreationalContext< T > context = beanManager.createCreationalContext(bean);
                if (context != null) {
                    result = (T) beanManager.getReference(bean, clazz, context);
                }
            // the bean/validator was not in the beanManager meaning it has no injection
            // points so go ahead and just instantiate a new instance and return it
            } else {
                try {
                    result = clazz.newInstance();
                } catch (Throwable t) {
                    throw new RuntimeException(t);
                }
            }

            return result;
        }
    }

    这是一个示例validation.xml文件,该文件告诉Hibernate Validator哪个类用作ValidatorFactory:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?xml version="1.0" encoding="UTF-8"?>
    <validation-config
        xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">
        <constraint-validator-factory>
            com.myvalidator.ConstraintInjectableValidatorFactory
        </constraint-validator-factory>
    </validation-config>

    最后是一个带有注入点的验证器类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class UserValidator implements ConstraintValidator<ValidUser, User> {

        @PersistenceUnit(unitName="myvalidator")
        private EntityManagerFactory entityManagerFactory;

        private EntityManager entityManager;

        @Override
        public void initialize(ValidUser annotation) {
        }

        @Override
        public boolean isValid(User user, ConstraintValidatorContext context) {
            // validation takes place during the entityManager.persist() lifecycle, so
            // here we create a new entityManager separate from the original one that
            // invoked this validation
            entityManager = entityManagerFactory.createEntityManager();

            // use entityManager to query database for needed validation

            entityManager.close();
        }
    }


    Translation please?

    生命周期事件不应使用实体管理器,因为它可能导致回归。想象一下,在更新前事件中,您修改了另一个实体。这应该在先前的预更新事件内生成另一个预更新事件。为避免此类问题,不建议使用实体管理器。

    但是,如果您只想读取一些其他数据,从概念上讲就没有问题。评估在更新前和插入前事件中隐式发生。

    如果您从不使用加载后事件,则在生命周期事件中读取数据不应触发嵌套的生命周期事件。据我了解,规范并不是严格禁止查询实体,而是强烈建议不要这样做。在这种情况下,可能没问题。您是否尝试过这种方法?

    So what's the general pattern for getting at an EntityManagerFactory
    from a Validator?

    注入仅在受管实体中有效。如果无法进行注入,则应该能够进行良好的旧查找以获取实体管理器。但是,使用第二个实体管理器时,可能会生成嵌套的生命周期事件。但是,如果您只做一些琐碎的事情,例如阅读旧密码列表,那应该没问题。


    我想我希望您使用令人敬畏的bean验证API进行所有验证,但是请记住这不是必需的。

    此外,请考虑以下两个要求:

  • 密码不能为空。
  • 用户不得使用与任何先前密码相同的密码。
  • 第一个显然仅取决于密码本身,我将其归类为验证数据,因此这种验证属于数据层。

    第二个参数取决于一条数据与许多其他实体或系统当前状态之间的关系。我将其归类为业务层中的某物。

    这就是说,不要将验证约束放在实体类上,而是将它们放在某个业务层类上(是的,如果您现在愿意,甚至可以使用bean验证)。

    例如,假设您有一个带有当前密码字段的User实体和一个Passwords实体,您可以从中查询用户的旧密码。现在,使您的用户数据访问对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Stateful // yes stateful, need the same instance across method invocations
    @ValidatePassword
    public class UserDao {

        @PersistenceContext private EntityManager em;
        private String password;

        public String getPassword() {
            return this.password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public boolean isValidPassword() {
            // use the em to find the old passwords
            // check that the submitted password is valid
        }

        public void savePassword() {
            // find the user
            // set the user's now valid password
        }
    }

    创建您的类级别约束:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Target( { TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = MyPasswordValidator.class)
    public @interface ValidatePassword {

        String message() default"error message";

        Class< ? >[] groups() default {};

        Class<? extends Payload>[] payload() default {};

    }

    和验证者:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class MyPasswordValidator implements ConstraintValidator<ValidatePassword, UserDao> {

        public void initialize(SelfValidating constraintAnnotation) {
            // get message, etc.
        }

        public boolean isValid(UserDao userDao, ConstraintValidatorContext constraintValidatorContext) {
            return userDao.isValidPassword();
        }
    }

    这样的事情应该做。副作用是,由于实际的验证现在是由EJB完成的,因此,如果保留默认的跨国属性为

    ,则验证逻辑本身将被事务处理。