Stream API 不适用于 EclipseLink / Glassfish 中的延迟加载集合?
Stream API not working for lazy loaded collections in EclipseLink / Glassfish?
在我的一项 Web 服务中检测到缺陷后,我将错误追踪到以下单行:
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
当我肯定地知道域列表包含一个名称等于所提供的 name
的域时,此行 return 是错误的。因此,在摸索了一会儿之后,我最终拆分了整条线以查看发生了什么。我在调试会话中得到以下信息:
请注意以下行:
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
根据调试器,domains
是一个包含两个元素的列表。但是在应用 .stream().collect(Collectors.toList())
之后,我得到了一个完全空的列表。如果我错了,请纠正我,但据我了解,这应该是身份操作和 return 相同的列表(或者如果我们严格的话,它的副本)。那么这里发生了什么???
在你问之前:不,我根本没有操纵那个截图。
为了将此放在上下文中,此代码在有状态请求范围内的 EJB 中执行,使用 JPA 管理实体,在扩展持久性上下文中具有字段访问权限。这里有一些与手头问题相关的代码部分:
@Stateful
@RequestScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class DomainResources {
@PersistenceContext(type = PersistenceContextType.EXTENDED) @RequestScoped
private EntityManager entityManager;
public boolean templateContainsDomainWithName(String name) { // Extra code included to diagnose the problem
MetadataTemplate template = this.getTemplate();
List<Domain> domains = template.getDomains();
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
List<String> names = domains.stream().map(Domain::getName).collect(Collectors.toList());
boolean exists1 = names.contains(name);
boolean exists2 = this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
}
@POST
@RolesAllowed({"root"})
public Response createDomain(@Valid @EmptyID DomainDTO domainDTO, @Context UriInfo uriInfo) {
if (this.getTemplate().getLastVersionState() != State.DRAFT) {
throw new UnmodifiableTemplateException();
} else if (templateContainsDomainWithName(domainDTO.name)) {
throw new DuplicatedKeyException("name", domainDTO.name);
} else {
Domain domain = this.getTemplate().createNewDomain(domainDTO.name);
this.entityManager.flush();
return Response.created(uriInfo.getAbsolutePathBuilder().path(domain.getId()).build()).entity(new DomainDTO(domain)).type(MediaType.APPLICATION_JSON).build();
}
}
}
@Entity
public class MetadataTemplate extends IdentifiedObject {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "metadataTemplate", orphanRemoval = true) @OrderBy(value = "creationDate")
private List<Version> versions = new LinkedList<>();
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OrderBy(value = "name")
private List<Domain> domains = new LinkedList<>();
public List<Version> getVersions() {
return Collections.unmodifiableList(versions);
}
public List<Domain> getDomains() {
return Collections.unmodifiableList(domains);
}
}
我已经包含了 getVersions
和 getDomains
方法,因为我有类似的操作 运行 在版本上完美无缺。我能找到的唯一显着区别是 versions
是急切获取的,而 domains
是延迟获取的。但据我所知,代码正在事务中执行,并且正在加载域列表。如果不是,我会得到一个惰性初始化异常,不是吗?
更新:按照@Ferrybig 的建议,我进一步调查了这个问题,似乎与不正确的延迟加载没有任何关系。如果我以经典方式遍历集合,我仍然无法使用流获得正确的结果:
boolean found = false;
for (Domain domain: this.getTemplate().getDomains()) {
if (domain.getName().equals(name)) {
found = true;
}
}
List<Domain> domains = this.getTemplate().getDomains();
long estimatedSize = domains.spliterator().estimateSize(); // This returns 0!
domains.spliterator().forEachRemaining(domain -> {
// Execution flow never reaches this point!
});
所以看起来即使在加载集合时您仍然有这种奇怪的行为。这似乎是用于管理惰性集合的代理中缺少或空的拆分器实现。你怎么看?
顺便说一句,这是部署在 Glassfish / EclipseLink 上
这里的问题来自其他人在几个地方的错误的组合。所有这些错误的总和引发了这种错误行为。
第一个错误:可疑的继承。 EclipseLink 似乎创建了一个代理来管理 org.eclipse.persistence.indirection.IndirectList
类型的惰性集合。这个 class 扩展 java.util.Vector
尽管它覆盖了除 removeRange
之外的所有内容。亲爱的 Eclipse 开发人员,到底为什么要扩展 class 来覆盖 parent 中的几乎所有内容,而不是声明 class 来实现合适的接口(Iterable<E>
, Collection<E>
或 List<E>
)?
第二个错误:嘿,我继承了你的东西,但不要对你的内部结构给予 $#|T。所以 IndirectList
使用 delegate 来实现延迟加载的魔力。但是,天哪!我如何计算大小?我是否使用(并保持更新)parent 的 elementCount
属性?不,当然,我只是将该任务委派给我的 delegate... 所以如果 parent class 需要做任何与大小相关的事情,那么,不好运气。无论如何,我已经覆盖了所有内容...他们不会向 class 添加任何新内容,他们会吗?
第三个错误:封装破损。输入 Vector
。在 Java 1.8 中,这个 class 得到了增强,现在提供了一个 spliterator
方法来支持新的流功能。他们创建了一个静态内部 class (VectorSpliterator
),让客户端使用闪亮的新 API 遍历向量。一切正常,直到您注意到为了知道何时完成遍历,他们使用 受保护的实例变量 elementCount
而不是使用 public API方法size()
。因为谁会扩展非最终 class 和 return 不基于 elementCount
的大小?你看到灾难来了吗?
所以我们到了,IndirectList
不知不觉地从 Vector
继承了新功能(记住它可能首先不应该从它继承),并用这种错误组合破坏了东西.
总而言之,在使用 EclipseLink(Glassfish 中的默认 JPA 提供程序) 时,即使对于已经加载的集合,惰性集合的流遍历似乎也不起作用。请记住,这些产品来自同一供应商。万岁!
变通方法:如果您遇到此问题但仍想利用 stream()
提供的函数式编程风格,您可以制作该集合的副本,这样一个合适的迭代器被构建。在我的例子中,我能够将域的所有类似用法保留为 one-liners 修改 getDomains
方法。在这种情况下,我更喜欢代码可读性(具有函数式风格)而不是性能:
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<>(domains));
}
READER注意事项:抱歉讽刺,但我不想在这些事情上浪费我宝贵的开发时间。
感谢@Ferrybig 提供的初步线索
更新:错误报告。如果这对您有影响,您可以在 https://bugs.eclipse.org/bugs/show_bug.cgi?id=487799
关注它的进展
我在单元测试中遇到了与此代码非常相似的问题:
Optional<ChildTable> ct = st.getChildren().stream().filter(i -> i.getId().equals(20001000l)).findFirst();
ct.get() 失败,出现 NoSuchElementException。
将 EclipseLink 从 2.5.2 更新到 2.6.2 解决了这个问题。您没有提到 EclipseLink 版本。
我认为您的错误报告与 https://bugs.eclipse.org/bugs/show_bug.cgi?id=433075 重复。
另请参阅 EclipseLink 和 Java 8 流 API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470.
未解决的错误
在我的一项 Web 服务中检测到缺陷后,我将错误追踪到以下单行:
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
当我肯定地知道域列表包含一个名称等于所提供的 name
的域时,此行 return 是错误的。因此,在摸索了一会儿之后,我最终拆分了整条线以查看发生了什么。我在调试会话中得到以下信息:
请注意以下行:
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
根据调试器,domains
是一个包含两个元素的列表。但是在应用 .stream().collect(Collectors.toList())
之后,我得到了一个完全空的列表。如果我错了,请纠正我,但据我了解,这应该是身份操作和 return 相同的列表(或者如果我们严格的话,它的副本)。那么这里发生了什么???
在你问之前:不,我根本没有操纵那个截图。
为了将此放在上下文中,此代码在有状态请求范围内的 EJB 中执行,使用 JPA 管理实体,在扩展持久性上下文中具有字段访问权限。这里有一些与手头问题相关的代码部分:
@Stateful
@RequestScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class DomainResources {
@PersistenceContext(type = PersistenceContextType.EXTENDED) @RequestScoped
private EntityManager entityManager;
public boolean templateContainsDomainWithName(String name) { // Extra code included to diagnose the problem
MetadataTemplate template = this.getTemplate();
List<Domain> domains = template.getDomains();
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
List<String> names = domains.stream().map(Domain::getName).collect(Collectors.toList());
boolean exists1 = names.contains(name);
boolean exists2 = this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
}
@POST
@RolesAllowed({"root"})
public Response createDomain(@Valid @EmptyID DomainDTO domainDTO, @Context UriInfo uriInfo) {
if (this.getTemplate().getLastVersionState() != State.DRAFT) {
throw new UnmodifiableTemplateException();
} else if (templateContainsDomainWithName(domainDTO.name)) {
throw new DuplicatedKeyException("name", domainDTO.name);
} else {
Domain domain = this.getTemplate().createNewDomain(domainDTO.name);
this.entityManager.flush();
return Response.created(uriInfo.getAbsolutePathBuilder().path(domain.getId()).build()).entity(new DomainDTO(domain)).type(MediaType.APPLICATION_JSON).build();
}
}
}
@Entity
public class MetadataTemplate extends IdentifiedObject {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "metadataTemplate", orphanRemoval = true) @OrderBy(value = "creationDate")
private List<Version> versions = new LinkedList<>();
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OrderBy(value = "name")
private List<Domain> domains = new LinkedList<>();
public List<Version> getVersions() {
return Collections.unmodifiableList(versions);
}
public List<Domain> getDomains() {
return Collections.unmodifiableList(domains);
}
}
我已经包含了 getVersions
和 getDomains
方法,因为我有类似的操作 运行 在版本上完美无缺。我能找到的唯一显着区别是 versions
是急切获取的,而 domains
是延迟获取的。但据我所知,代码正在事务中执行,并且正在加载域列表。如果不是,我会得到一个惰性初始化异常,不是吗?
更新:按照@Ferrybig 的建议,我进一步调查了这个问题,似乎与不正确的延迟加载没有任何关系。如果我以经典方式遍历集合,我仍然无法使用流获得正确的结果:
boolean found = false;
for (Domain domain: this.getTemplate().getDomains()) {
if (domain.getName().equals(name)) {
found = true;
}
}
List<Domain> domains = this.getTemplate().getDomains();
long estimatedSize = domains.spliterator().estimateSize(); // This returns 0!
domains.spliterator().forEachRemaining(domain -> {
// Execution flow never reaches this point!
});
所以看起来即使在加载集合时您仍然有这种奇怪的行为。这似乎是用于管理惰性集合的代理中缺少或空的拆分器实现。你怎么看?
顺便说一句,这是部署在 Glassfish / EclipseLink 上
这里的问题来自其他人在几个地方的错误的组合。所有这些错误的总和引发了这种错误行为。
第一个错误:可疑的继承。 EclipseLink 似乎创建了一个代理来管理 org.eclipse.persistence.indirection.IndirectList
类型的惰性集合。这个 class 扩展 java.util.Vector
尽管它覆盖了除 removeRange
之外的所有内容。亲爱的 Eclipse 开发人员,到底为什么要扩展 class 来覆盖 parent 中的几乎所有内容,而不是声明 class 来实现合适的接口(Iterable<E>
, Collection<E>
或 List<E>
)?
第二个错误:嘿,我继承了你的东西,但不要对你的内部结构给予 $#|T。所以 IndirectList
使用 delegate 来实现延迟加载的魔力。但是,天哪!我如何计算大小?我是否使用(并保持更新)parent 的 elementCount
属性?不,当然,我只是将该任务委派给我的 delegate... 所以如果 parent class 需要做任何与大小相关的事情,那么,不好运气。无论如何,我已经覆盖了所有内容...他们不会向 class 添加任何新内容,他们会吗?
第三个错误:封装破损。输入 Vector
。在 Java 1.8 中,这个 class 得到了增强,现在提供了一个 spliterator
方法来支持新的流功能。他们创建了一个静态内部 class (VectorSpliterator
),让客户端使用闪亮的新 API 遍历向量。一切正常,直到您注意到为了知道何时完成遍历,他们使用 受保护的实例变量 elementCount
而不是使用 public API方法size()
。因为谁会扩展非最终 class 和 return 不基于 elementCount
的大小?你看到灾难来了吗?
所以我们到了,IndirectList
不知不觉地从 Vector
继承了新功能(记住它可能首先不应该从它继承),并用这种错误组合破坏了东西.
总而言之,在使用 EclipseLink(Glassfish 中的默认 JPA 提供程序) 时,即使对于已经加载的集合,惰性集合的流遍历似乎也不起作用。请记住,这些产品来自同一供应商。万岁!
变通方法:如果您遇到此问题但仍想利用 stream()
提供的函数式编程风格,您可以制作该集合的副本,这样一个合适的迭代器被构建。在我的例子中,我能够将域的所有类似用法保留为 one-liners 修改 getDomains
方法。在这种情况下,我更喜欢代码可读性(具有函数式风格)而不是性能:
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<>(domains));
}
READER注意事项:抱歉讽刺,但我不想在这些事情上浪费我宝贵的开发时间。
感谢@Ferrybig 提供的初步线索
更新:错误报告。如果这对您有影响,您可以在 https://bugs.eclipse.org/bugs/show_bug.cgi?id=487799
关注它的进展我在单元测试中遇到了与此代码非常相似的问题:
Optional<ChildTable> ct = st.getChildren().stream().filter(i -> i.getId().equals(20001000l)).findFirst();
ct.get() 失败,出现 NoSuchElementException。
将 EclipseLink 从 2.5.2 更新到 2.6.2 解决了这个问题。您没有提到 EclipseLink 版本。
我认为您的错误报告与 https://bugs.eclipse.org/bugs/show_bug.cgi?id=433075 重复。
另请参阅 EclipseLink 和 Java 8 流 API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470.
未解决的错误