2013-08-16 42 views
8

我是一位Java EE/EJB noob,但是從我收集的文檔和其他帖子中,您無法在實體期間使用相同的entitymanager/session查詢數據庫驗證。在Hibernate驗證期間執行EntityManager查詢的正確方法

一般而言,便攜式應用的生命週期方法不應該調用的EntityManager 或查詢操作,訪問其他實體實例,或修改 相同持久上下文內的關係。[43]生命週期回調方法可能會修改調用它的實體的非關係狀態 。

請翻譯?

這是相當抽象的...可以用更具體的術語來解釋它嗎?它會導致更多的問題,而不是它的答案。例如,如果我的實體有一個延遲加載的集合,我允許在驗證期間訪問它嗎?該集合是「另一個實體」,將需要一個似乎違反文檔的數據庫查詢。

這個'生命週期需求'看起來很奇怪,因爲某些驗證確實需要查詢數據庫,這只是生活中的事實。

從其他帖子我也看到人們通過使用entitymanagerfactory創建一個新的entitymanager/session來解決這個查詢問題。

這使我兩個問題有關使用EntityManagers和Hibernate驗證:

  1. 是否有可能我有某種設計缺陷或正在濫用Hibernate的驗證,因爲我需要驗證過程中查詢數據庫?
  2. 鑑於我使用Java EE與JBoss,我如何注入我的驗證與EntityManagerFactory?

我已經試過這樣的事情:

@Stateless 
public class UserValidator implements ConstraintValidator<ValidUser, User> { 
    @PersistenceUnit(unitName="blahblah") 
    EntityManagerFactory emf; 

    ... 
} 

但EMF永遠不會被注入。我在猜測@Stateless標籤變得不相關,因爲我正在實現一個ConstraintValidator接口,這是Hibernate Validator的工作所需要的。

那麼從驗證器獲取EntityManagerFactory的一般模式是什麼?

謝謝!

+0

聽起來就像你試圖驗證用戶不存在,爲什麼不讓數據庫這樣做? – rdcrng

+0

真的,我在一般意義上提出這個問題。需要將數據庫訪問作爲驗證的一部分對我來說並不罕見。在這種特殊情況下,儘管我有一項業務要求,即用戶更改密碼不會重複使用舊密碼。所以我必須加入密碼錶。我懶加載列表,但有一個方法User.getOldPasswords()是醜陋的,所以我寧願不將List pwds映射到用戶身上,只是在驗證過程中查詢db。 – lostdorje

回答

6

通過一些評論和足夠的評論,我終於想出了一個有點「規範」的方式來回答我的問題。

但要明確的事情了,我的問題其實是問這有2個不同的答案兩兩件事:

  1. 你怎麼注入東西放到驗證器在Hibernate的驗證框架使用?
  2. 假設我們可以注入事物,注入一個EntityManagerFactory或EntityManager並將其用於在JPA生命週期事件期間進行查詢是安全的嗎?

首先回答第二個問題我會簡單地說,強烈建議在驗證期間使用第二個EntityManager進行查詢。這意味着您應該注入一個EntityManagerFactory併爲查詢創建一個新的EntityManager(而不是注入一個EntityManager,它將與創建生命週期事件的EntityManager相同)。

一般來說,出於驗證的目的,您只需要查詢數據庫,而不是插入/更新,因此這應該相當安全。

我問了一個非常相關的SO問題here

現在來回答問題1

是的,它是完全有可能注入到東西校驗器在Hibernate驗證框架使用。要做到這一點,你需要做3件事:

  1. 創建一個自定義的ConstraintValidatorFactory,它將創建框架中使用的驗證器(覆蓋Hibernate的默認工廠)。 (我的例子使用Java EE,而不是Spring,所以我使用BeanManager,但在Spring中你可能會使用ApplicationContext)。
  2. 創建一個validation.xml文件,它告訴Hibernate Validation框架哪個類用於ConstraintValidatorFactory。確保這個文件在你的類路徑中結束。
  3. 編寫一個注入了一些東西的驗證器。

下面是一個使用「管理」(可注射的)驗證的示例定製ConstraintValidatorFactory:

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驗證作爲ValidatorFactory使用哪個類:

<?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> 

最後又用注射點的驗證器類:

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

+1,因爲我學到了一些關於自定義驗證解決方案和bean管理的新知識。也就是說,這一切都歸結爲對初始上下文'InitialContext.doLookup(「java:comp/BeanManager」);'的調用,所以我沒有看到在驗證器中查找實體管理器本身的明顯好處(正如我在我的回答中所建議的)。 – ewernli

+0

我從你的回答中得到的查詢,非常感謝! :-)我認爲走這條路線可能會更好,因爲通過這條路線可以讓你注入bean管理員知道的任何東西。可能或可能沒有在JNDI中註冊的東西。我也沒有關於這方面的事實,但我認爲JNDI查找可能會很昂貴,所以1查找bean管理器,然後從bean管理器中獲取其他所有內容應該更高效。 – lostdorje

0

我認爲我希望能夠使用真棒bean驗證API來完成所有的驗證,但請記住這不是必需的。

此外,想想這兩個要求:

  1. 密碼不能爲空。
  2. 用戶不得使用與以前任何密碼相同的密碼。

第一個顯然只取決於密碼本身,我將它分類爲驗證數據,因此這種驗證屬於數據層。

第二個依賴於一塊數據與多個其他實體或與系統的當前狀態的關係。我會將它歸類爲業務層中的某些內容。這就是說,不是試圖將驗證約束放在實體類上,而是將它們放在某個業務層類(是的,如果你現在願意,你甚至可以使用bean驗證)。

例如,假設您有一個User實體(包含當前密碼字段)和一個Passwords實體,您可以從中查詢用戶的舊密碼。現在,讓你的用戶的數據訪問對象:

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

創建類級約束:

@Target({ TYPE }) 
@Retention(RUNTIME) 
@Constraint(validatedBy = MyPasswordValidator.class) 
public @interface ValidatePassword { 

    String message() default "error message"; 

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

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

} 

和確認:

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現在完成的,所以驗證邏輯本身將被處理,如果您保留默認的跨國屬性。

+0

感謝您的回答。你的回答很有意義,我認爲它非常接近。在一個稍微不同的形式中,我的主要問題仍然存在,但如果我在驗證程序中接受您的建議,則需要注入DAO。但是,這遭受了我原來的問題所要求的同樣的問題,那就是如何將entitymanagerfactory注入驗證器?如果我不能這樣做,那麼我也不能注入一個DAO。 – lostdorje

+0

據我所知,*注入* ConstraintValidator中的任何東西在CDI 1.0之前都不起作用。在CDI 1.1中,這是可能的,請查看http://docs.jboss.org/cdi/spec/1.1/cdi-spec.html#_relationship_to_bean_validation。然而,在我看來,上述做你想要的,儘管不是注射。爲什麼你要堅持*注入*什麼東西,如果你只是將你正在驗證的實例傳遞給'ConstrintValidator'並獲得相同的效果?我的意思是,我知道注射很好,但侷限性是有限的。 – rdcrng

+0

「如果你只能通過你正在驗證的實例」......這裏被驗證的實例是用戶,並且* *會被傳入。但是UserDao不會被傳入。這需要被注入。如果未注入,則意味着編寫自己的代碼以基於persistence.xml文件初始化新的EMF,從EMF創建EM(或UserDAO),然後使用EM或DAO執行驗證查詢。在上面的代碼片段中,userDao對象來自哪裏? – lostdorje

0

請翻譯?

生命週期事件不應該使用實體管理器,因爲它可能會導致迴歸。想象一下,在更新前的事件中,您可以修改另一個實體。這應該在之前產生另一個更新前事件。爲了避免這些問題,不鼓勵使用實體管理器。

但是,如果你想要做的只是讀取一些額外的數據,概念上沒有問題。 Valation隱式地發生在更新前和預插入事件中。

如果您從不使用加載後事件,那麼讀取生命週期事件中的數據不應觸發嵌套生命週期事件。據我瞭解規範,查詢實體並不嚴格禁止,但強烈不鼓勵。在這種情況下,它可能會很好。你試過了,如果這有效嗎?

那麼從Validator獲取EntityManagerFactory 的一般模式是什麼?

注射僅在管理實體中有效。當注射不可能時,你應該可以做一個好的舊的lookup to obtain an entity manager。但是,在使用第二個實體管理器時,可能會生成嵌套的生命週期事件。但是,如果你只是做一些微不足道的事情,比如閱讀舊密碼列表,那應該沒問題。