通过无知解决 LazyInitializationException
Solving LazyInitializationException via ignorance
这里有无数的问题,如何通过急切获取,保持事务打开,打开另一个,OpenEntityManagerInViewFilter
等等来解决“无法初始化代理”的问题。
但是是否可以简单地告诉 Hibernate 忽略该问题并假装集合为空?就我而言,之前不获取它只是意味着我不在乎。
这其实是一个XY问题,下面的Y:
我类喜欢
class Detail {
@ManyToOne(optional=false) Master master;
...
}
class Master {
@OneToMany(mappedBy="master") List<Detail> details;
...
}
并且想要服务于两种请求:一种是 returning 一个 master
及其所有 details
,另一种是 returning 一个 master
s 没有 details
。结果被 Gson 转换为 JSON。
我试过 session.clear
和 session.evict(master)
,但它们没有触及用于代替 details
的代理。有效的是
master.setDetails(nullOrSomeCollection)
这感觉很老套。我更喜欢“无知”,因为它通常适用于不知道代理的哪些部分。
写一个 Gson TypeAdapter
忽略 AbstractPersistentCollection
和 initialized=false
的实例可能是一种方法,但这将取决于 org.hibernate.collection.internal
,这肯定不是好事。在 TypeAdapter
中捕获异常听起来并没有好多少。
一些答案后更新
我的目标不是“加载数据而不是异常”,而是“如何获取 null 而不是异常
我
一个有效的观点是忘记获取和 returning 错误的数据比异常更糟糕。但有一个简单的解决方法:
- 仅对集合执行此操作
- 永远不要为他们使用
null
- return
null
而不是空集合作为未获取数据的指示
这样,结果永远不会被错误解读。如果我忘记取东西,响应将包含无效的 null
。
您可以尝试如下解决方案。
正在创建一个名为 LazyLoader
的界面
@FunctionalInterface // Java 8
public interface LazyLoader<T> {
void load(T t);
}
在您的服务中
public class Service {
List<Master> getWithDetails(LazyLoader<Master> loader) {
// Code to get masterList from session
for(Master master:masterList) {
loader.load(master);
}
}
}
并像下面这样调用此服务
Service.getWithDetails(new LazyLoader<Master>() {
public void load(Master master) {
for(Detail detail:master.getDetails()) {
detail.getId(); // This will load detail
}
}
});
并且在 Java 8 中,您可以使用 Lambda,因为它是单一抽象方法 (SAM)。
Service.getWithDetails((master) -> {
for(Detail detail:master.getDetails()) {
detail.getId(); // This will load detail
}
});
您可以将上述解决方案与 session.clear
和 session.evict(master)
一起使用
您可以利用 Hibernate.isInitialized,它是 Hibernate public API.
的一部分
因此,在 TypeAdapter
中您可以添加如下内容:
if ((value instanceof Collection) && !Hibernate.isInitialized(value)) {
result = new ArrayList();
}
但是,在我看来,您的方法通常不是正确的方法。
"In my case, not fetching it before simply means that I don't care."
或者这意味着你忘记获取它,现在你返回了错误的数据(比得到异常更糟糕;服务的消费者认为集合是空的,但它不是)。
我不想提出 "better" 解决方案(这不是问题的主题,每种方法都有其自身的优势),但我在大多数用例中解决此类问题的方式(并且它是常用的方法之一)是使用 DTO:简单地定义一个代表服务响应的 DTO,将其填充到事务上下文中(那里没有 LazyInitializationException
s)并将其提供给将对其进行转换的框架到服务响应(json、xml 等)。
我过去曾提出过类似的问题 (why dependent collection isn't evicted when parent entity is),并且得到了一个答案,您可以针对您的情况进行尝试。
这个问题的解决方案是使用查询而不是关联(一对多或多对多)。甚至 Hibernate 的一位原作者也说 Collections are a feature 而不是最终目标。
在您的情况下,您可以更灵活地删除集合映射,并在数据访问层中需要时简单地获取关联关系。
您可以为每个实体创建一个 Java 代理,这样每个方法都被 try/catch 块包围,当 [=15= 时 returns null
] 被抓住了。
为此,您的所有实体都需要实现一个接口,并且您需要在整个程序中引用此接口(而不是实体 class)。
如果您不能(或只是不想)使用接口,那么您可以尝试使用 javassist or cglib, or even manually, as explained in this article 构建动态代理。
如果您使用常见的 Java 代理,这里有一个草图:
public static <T> T ignoringLazyInitialization(
final Object entity,
final Class<T> entityInterface) {
return (T) Proxy.newProxyInstance(
entityInterface.getClassLoader(),
new Class[] { entityInterface },
new InvocationHandler() {
@Override
public Object invoke(
Object proxy,
Method method,
Object[] args)
throws Throwable {
try {
return method.invoke(entity, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getTargetException();
if (cause instanceof LazyInitializationException) {
return null;
}
throw cause;
}
}
});
}
所以,如果你有一个实体 A
如下:
public interface A {
// getters & setters and other methods DEFINITIONS
}
及其实施:
public class AImpl implements A {
// getters & setters and other methods IMPLEMENTATIONS
}
然后,假设您有对实体 class 的引用(由 Hibernate 返回),您可以按如下方式创建代理:
AImpl entityAImpl = ...; // some query, load, etc
A entityA = ignoringLazyInitialization(entityAImpl, A.class);
注意 1: 您还需要代理 Hibernate 返回的集合(留作 reader 的练习);)
注意 2: 理想情况下,您应该在 DAO 或某种类型的外观中执行所有这些代理操作,以便所有内容对实体的用户都是透明的
注意 3: 这绝不是最优的,因为它会为每次访问未初始化字段创建堆栈跟踪
注意 4: 这可行,但增加了复杂性;考虑是否真的有必要。
这里有无数的问题,如何通过急切获取,保持事务打开,打开另一个,OpenEntityManagerInViewFilter
等等来解决“无法初始化代理”的问题。
但是是否可以简单地告诉 Hibernate 忽略该问题并假装集合为空?就我而言,之前不获取它只是意味着我不在乎。
这其实是一个XY问题,下面的Y:
我类喜欢
class Detail {
@ManyToOne(optional=false) Master master;
...
}
class Master {
@OneToMany(mappedBy="master") List<Detail> details;
...
}
并且想要服务于两种请求:一种是 returning 一个 master
及其所有 details
,另一种是 returning 一个 master
s 没有 details
。结果被 Gson 转换为 JSON。
我试过 session.clear
和 session.evict(master)
,但它们没有触及用于代替 details
的代理。有效的是
master.setDetails(nullOrSomeCollection)
这感觉很老套。我更喜欢“无知”,因为它通常适用于不知道代理的哪些部分。
写一个 Gson TypeAdapter
忽略 AbstractPersistentCollection
和 initialized=false
的实例可能是一种方法,但这将取决于 org.hibernate.collection.internal
,这肯定不是好事。在 TypeAdapter
中捕获异常听起来并没有好多少。
一些答案后更新
我的目标不是“加载数据而不是异常”,而是“如何获取 null 而不是异常 我
- 仅对集合执行此操作
- 永远不要为他们使用
null
- return
null
而不是空集合作为未获取数据的指示
这样,结果永远不会被错误解读。如果我忘记取东西,响应将包含无效的 null
。
您可以尝试如下解决方案。
正在创建一个名为 LazyLoader
@FunctionalInterface // Java 8
public interface LazyLoader<T> {
void load(T t);
}
在您的服务中
public class Service {
List<Master> getWithDetails(LazyLoader<Master> loader) {
// Code to get masterList from session
for(Master master:masterList) {
loader.load(master);
}
}
}
并像下面这样调用此服务
Service.getWithDetails(new LazyLoader<Master>() {
public void load(Master master) {
for(Detail detail:master.getDetails()) {
detail.getId(); // This will load detail
}
}
});
并且在 Java 8 中,您可以使用 Lambda,因为它是单一抽象方法 (SAM)。
Service.getWithDetails((master) -> {
for(Detail detail:master.getDetails()) {
detail.getId(); // This will load detail
}
});
您可以将上述解决方案与 session.clear
和 session.evict(master)
您可以利用 Hibernate.isInitialized,它是 Hibernate public API.
的一部分因此,在 TypeAdapter
中您可以添加如下内容:
if ((value instanceof Collection) && !Hibernate.isInitialized(value)) {
result = new ArrayList();
}
但是,在我看来,您的方法通常不是正确的方法。
"In my case, not fetching it before simply means that I don't care."
或者这意味着你忘记获取它,现在你返回了错误的数据(比得到异常更糟糕;服务的消费者认为集合是空的,但它不是)。
我不想提出 "better" 解决方案(这不是问题的主题,每种方法都有其自身的优势),但我在大多数用例中解决此类问题的方式(并且它是常用的方法之一)是使用 DTO:简单地定义一个代表服务响应的 DTO,将其填充到事务上下文中(那里没有 LazyInitializationException
s)并将其提供给将对其进行转换的框架到服务响应(json、xml 等)。
我过去曾提出过类似的问题 (why dependent collection isn't evicted when parent entity is),并且得到了一个答案,您可以针对您的情况进行尝试。
这个问题的解决方案是使用查询而不是关联(一对多或多对多)。甚至 Hibernate 的一位原作者也说 Collections are a feature 而不是最终目标。
在您的情况下,您可以更灵活地删除集合映射,并在数据访问层中需要时简单地获取关联关系。
您可以为每个实体创建一个 Java 代理,这样每个方法都被 try/catch 块包围,当 [=15= 时 returns null
] 被抓住了。
为此,您的所有实体都需要实现一个接口,并且您需要在整个程序中引用此接口(而不是实体 class)。
如果您不能(或只是不想)使用接口,那么您可以尝试使用 javassist or cglib, or even manually, as explained in this article 构建动态代理。
如果您使用常见的 Java 代理,这里有一个草图:
public static <T> T ignoringLazyInitialization(
final Object entity,
final Class<T> entityInterface) {
return (T) Proxy.newProxyInstance(
entityInterface.getClassLoader(),
new Class[] { entityInterface },
new InvocationHandler() {
@Override
public Object invoke(
Object proxy,
Method method,
Object[] args)
throws Throwable {
try {
return method.invoke(entity, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getTargetException();
if (cause instanceof LazyInitializationException) {
return null;
}
throw cause;
}
}
});
}
所以,如果你有一个实体 A
如下:
public interface A {
// getters & setters and other methods DEFINITIONS
}
及其实施:
public class AImpl implements A {
// getters & setters and other methods IMPLEMENTATIONS
}
然后,假设您有对实体 class 的引用(由 Hibernate 返回),您可以按如下方式创建代理:
AImpl entityAImpl = ...; // some query, load, etc
A entityA = ignoringLazyInitialization(entityAImpl, A.class);
注意 1: 您还需要代理 Hibernate 返回的集合(留作 reader 的练习);)
注意 2: 理想情况下,您应该在 DAO 或某种类型的外观中执行所有这些代理操作,以便所有内容对实体的用户都是透明的
注意 3: 这绝不是最优的,因为它会为每次访问未初始化字段创建堆栈跟踪
注意 4: 这可行,但增加了复杂性;考虑是否真的有必要。