想听听专家关于从 JSF UI 编辑 JPA 实体的最佳实践。
所以,关于这个问题的几句话。
想象一下,我有一个持久化的对象MyEntity
,我获取它进行编辑。在DAO层我使用
return em.find(MyEntity.class, id);
它返回MyEntity
带有"父"实体代理的实例 - 想象一下其中一个是MyParent
. MyParent
作为代理问候语获取给@Access(AccessType.PROPERTY)
:
@Entity
public class MyParent {
@Id
@Access(AccessType.PROPERTY)
private Long id;
//...
}
MyEntity有对它的引用:
@ManyToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.PROXY)
private MyParent myParent;
目前为止,一切都好。在UI中,我只是直接使用获取的对象,而不创建任何值对象,并使用选择列表中的父对象:
<h:selectOneMenu value="#{myEntity.myParent.id}" id="office">
<f:selectItems value="#{parents}"/>
</h:selectOneMenu>
一切都很好,没有发生LazyInitializationException
。但是当我保存对象时,我收到
LazyInitializationException: could not initialize proxy - no Session
MyParent
代理setId()
方法。
如果我将MyParent
关系更改为EAGER
,我可以轻松解决问题
@ManyToOne(fetch = FetchType.EAGER)
private MyParent myParent;
或者使用 left join fetch p.myParent
获取对象(实际上这就是我现在的做法(。在这种情况下,保存操作工作正常,关系透明地更改为新的MyParent
对象。无需执行其他操作(手动复制、手动引用设置(。非常简单方便。
但是。如果对象引用了 10 个其他对象 - em.find()
将导致 10 个额外的连接,这不是一个好的数据库操作,尤其是当我根本不使用引用对象状态时。我所需要的只是指向对象的链接,而不是它们的状态。
这是一个全球性的问题,我想知道,JSF 专家如何处理其应用程序中的 JPA 实体,这是避免额外连接和LazyInitializationException
的最佳策略。
扩展持久性上下文对我来说是不行的。
谢谢!
您应该准确提供视图所需的模型。
如果 JPA 实体恰好与所需的模型完全匹配,则立即使用它。
如果 JPA 实体碰巧具有太少或太多的属性,则使用带有更具体的 JPQL 查询的 DTO(子类(和/或构造函数表达式,如有必要,请使用显式FETCH JOIN
。或者可能使用Hibernate特定的获取配置文件,或EclipseLink特定的属性组。否则,它可能会导致所有位置的延迟初始化异常,或者消耗不必要的内存。
"视图中打开会话"模式是一个糟糕的设计。在整个HTTP请求-响应处理过程中,您基本上保持单个数据库事务处于打开状态。对是否开始新的数据库事务的控制完全被剥夺了。当业务逻辑需要时,不能在同一 HTTP 请求期间生成多个事务。请记住,当单个查询在事务期间失败时,将回滚整个事务。参见 什么时候有必要或方便地使用 Spring 或 EJB3 或所有这些?
在 JSF 透视图中,"视图中打开会话"模式还意味着可以在呈现响应时执行业务逻辑。这与其他异常处理不太配合,后者的目的是向最终用户显示自定义错误页面。如果在呈现响应的中途抛出业务异常,最终用户因此已经收到了响应标头和 HTML 的一部分,则服务器无法再清除响应以显示一个漂亮的错误页面。此外,在 getter 方法中执行业务逻辑在 JSF 中是一种不受欢迎的做法,因为 JSF 会多次调用 getter。
只需在呈现响应阶段开始之前,通过托管 Bean 操作/侦听器方法中的常规服务方法调用准确准备视图所需的模型。例如,一种常见的情况是,手头有一个现有的(非托管的(父实体,其中包含一个延迟加载的一对多子属性,并且您希望通过 ajax 操作在当前视图中呈现它,那么您应该让 ajax 侦听器方法在服务层中获取并初始化它。
<f:ajax listener="#{bean.showLazyChildren(parent)}" render="children" />
public void showLazyChildren(Parent parent) {
someParentService.fetchLazyChildren(parent);
}
public void fetchLazyChildren(Parent parent) {
parent.setLazyChildren(em.merge(parent).getLazyChildren()); // Becomes managed.
parent.getLazyChildren().size(); // Triggers lazy initialization.
}
特别是在 JSF UISelectMany
组件中,还有另一个完全出乎意料的可能LazyInitializationException
原因:在保存选定项期间,JSF 需要在用选定项填充之前重新创建基础集合,但是如果它恰好是特定于持久性层的延迟加载集合实现,那么也会引发此异常。解决方案是将UISelectMany
组件的 collectionType
属性显式设置为所需的"普通"类型。
<h:selectManyCheckbox ... collectionType="java.util.ArrayList">
这在org.hibernate.LazyInitializationException中详细询问和回答,网址为com.sun.faces.renderkit.html_basic。MenuRenderer.convertSelectManyValuesForModel.
另请参阅:
- LazyInitializationException in selectManyCheckbox on @ManyToMany(fetch=LAZY(
- 什么是休眠中的延迟加载?
对于休眠>= 4.1.6,请阅读此 https://stackoverflow.com/a/11913404/3252285
使用OpenSessionInView过滤器(设计模式(非常有用,但在我看来它并不能完全解决问题,原因如下:
如果我们有一个实体存储在会话中或由会话 Bean 处理或从缓存中检索,并且它的一个集合尚未在同一加载请求期间初始化,那么我们以后随时调用它时都会获得异常,即使我们使用 OSIV 设计模式。
让我们详细说明问题:
- 任何休眠代理都需要附加到打开的会话才能正常工作。
- Hibernate没有提供任何工具(
Listener or Handler
(来重新分配代理,以防他的会话关闭或他与自己的会话分离。
为什么休眠不提供?因为要确定哪个会话并不容易,代理应该被重新分配,但在许多情况下我们可以。
那么当LazyInitializationException发生时如何重新连接代理呢?
在我的ERP中,我修改了那些类:JavassistLazyInitializer
和AbstractPersistentCollection
,然后我再也不关心这个异常了(自3年以来一直使用,没有任何错误(:
class JavassistLazyInitializer{
@Override
public Object invoke(
final Object proxy,
final Method thisMethod,
final Method proceed,
final Object[] args) throws Throwable {
if ( this.constructed ) {
Object result;
try {
result = this.invoke( thisMethod, args, proxy );
}
catch ( Throwable t ) {
throw new Exception( t.getCause() );
}
if ( result == INVOKE_IMPLEMENTATION ) {
Object target = null;
try{
target = getImplementation();
}catch ( LazyInitializationException lze ) {
/* Catching the LazyInitException and reatach the proxy to the right Session */
EntityManager em = ContextConfig.getCurrent().getDAO(
BaseBean.getWcx(),
HibernateProxyHelper.getClassWithoutInitializingProxy(proxy)).
getEm();
((Session)em.getDelegate()).refresh(proxy);// attaching the proxy
}
try{
if (target==null)
target = getImplementation();
.....
}
....
}
和
class AbstractPersistentCollection{
private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) {
SessionImplementor originalSession = null;
boolean isTempSession = false;
boolean isJTA = false;
if ( session == null ) {
if ( allowLoadOutsideTransaction ) {
session = openTemporarySessionForLoading();
isTempSession = true;
}
else {
/* Let try to reatach the proxy to the right Session */
try{
session = ((SessionImplementor)ContextConfig.getCurrent().getDAO(
BaseBean.getWcx(), HibernateProxyHelper.getClassWithoutInitializingProxy(
owner)).getEm().getDelegate());
SessionFactoryImplementor impl = (SessionFactoryImplementor) ((SessionImpl)session).getSessionFactory();
((SessionImpl)session).getPersistenceContext().addUninitializedDetachedCollection(
impl.getCollectionPersister(role), this);
}catch(Exception e){
e.printStackTrace();
}
if (session==null)
throwLazyInitializationException( "could not initialize proxy - no Session" );
}
}
if (session==null)
throwLazyInitializationException( "could not initialize proxy - no Session" );
....
}
...
}
铌:
- 我没有像JTA或其他情况那样解决所有的可能性。
- 当您激活缓存时,此解决方案的效果会更好
一种非常常见的方法是在视图筛选器中创建打开的实体管理器。春天提供了一个(检查这里(。
我看不出你正在使用 Spring,但这不是一个真正的问题,你可以根据自己的需要调整该类中的代码。您还可以检查筛选器"在视图中打开会话",该筛选器执行相同的操作,但它使休眠会话保持打开状态,而不是实体管理器。
此方法可能不适合您的应用程序,SO 中有一些关于此模式或反模式的讨论。链接 1.我认为对于大多数应用程序(smalish,少于 20 个并发用户(,此解决方案运行良好。
编辑
这里有一个春季班与 FSF 的联系更好
EJB3 中没有对开放会话的标准支持,请参阅此答案。
映射的获取类型只是一个默认选项,我可以在查询时被覆盖。这是一个示例:
select g from Group g fetch join g.students
因此,普通 EJB3 中的另一种方法是通过显式查询所需的数据,确保在渲染开始之前加载渲染视图所需的所有数据。
延迟加载是一项重要的功能,可以很好地提高性能。然而,它的可用性比它应该的要差得多。
特别是当你开始处理AJAX请求时,遇到未初始化的集合,注释主义者只是有用的告诉Hibernate不要立即加载它。Hibernate不会处理其他任何事情,但会向你抛出LazyInitializationException
- 正如你所经历的那样。
我对此的解决方案 - 可能并不完美或噩梦 - 通过应用以下规则在任何情况下都有效(我不得不承认,这是在一开始写的,但从那时起就有效(:
每个使用 fetch = FetchType.LAZY
的实体都必须扩展LazyEntity
,并在相关collection
的 getter 中调用 initializeCollection()
,然后才能返回。(自定义验证器正在处理此约束,报告缺少的扩展和/或对initializeCollection
的调用(
示例类(用户,其组加载延迟(:
public class User extends LazyEntity{
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@BatchSize(size = 5)
List<Group> groups;
public List<Group> getGroups(){
initializeCollection(this.groups);
return this.groups;
}
}
其中initializeCollection(Collection collection)
的实现如下所示。内联注释应让您了解哪种方案需要什么。同步该方法以避免 2 个活动会话转移实体的所有权,而另一个会话当前正在获取数据。(仅当并发 Ajax 请求在同一实例上进行时显示。
public abstract class LazyEntity {
@SuppressWarnings("rawtypes")
protected synchronized void initializeCollection(Collection collection) {
if (collection instanceof AbstractPersistentCollection) {
//Already loaded?
if (!Hibernate.isInitialized(collection)) {
AbstractPersistentCollection ps = (AbstractPersistentCollection) collection;
//Is current Session closed? Then this is an ajax call, need new session!
//Else, Hibernate will know what to do.
if (ps.getSession() == null) {
//get an OPEN em. This needs to be handled according to your application.
EntityManager em = ContextHelper.getBean(ServiceProvider.class).getEntityManager();
//get any Session to obtain SessionFactory
Session anySession = em.unwrap(Session.class);
SessionFactory sf = anySession.getSessionFactory();
//get a new session
Session newSession = sf.openSession();
//move "this" to the new session.
newSession.update(this);
//let hibernate do its work on the current session.
Hibernate.initialize(collection);
//done, we can abandon the "new Session".
newSession.close();
}
}
}
}
}
但请注意,这种方法需要您验证实体是否与当前会话相关联,无论何时保存它 - 否则您必须在调用merge()
之前再次将整个对象树移动到当前会话。
视图中的开放会话设计模式可以在Java EE环境中轻松实现(不依赖于休眠,弹簧或其他Java EE之外的东西(。它与OpenSessionInView中的基本相同,但是您应该使用JTA事务而不是Hibernate会话
。@WebFilter(urlPatterns = {"*"})
public class JTAFilter implements Filter{
@Resource
private UserTransaction ut;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try{
ut.begin();
chain.doFilter(request, response);
}catch(NotSupportedException | SystemException e){
throw new ServletException("", e);
} finally {
try {
if(ut.getStatus()!= Status.STATUS_MARKED_ROLLBACK){
ut.commit();
}
} catch (Exception e) {
throw new ServletException("", e);
}
}
}
@Override
public void destroy() {
}
}