[英]Stream API not working for lazy loaded collections in EclipseLink / Glassfish?
在检测到我的某个Web服务中存在缺陷后,我将错误跟踪到以下单行:
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
当我肯定知道域列表包含一个名称等于提供name
的域时,这一行返回false。 所以在我搔了一会儿之后,我最终分开整条线来看看发生了什么。 我在调试会话中得到以下内容:
请注意以下行:
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
根据调试器, domains
是一个包含两个元素的列表。 但在应用.stream().collect(Collectors.toList())
我得到一个完全空的列表。 如果我错了,请纠正我,但根据我的理解,那应该是身份操作并返回相同的列表(如果我们是严格的话,还是它的副本)。 那么这里发生了什么?
在你问之前: 不,我根本没有操纵过截图。
为了将其置于上下文中,此代码在有状态请求范围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
方法,因为我有类似的操作在版本上运行完美。 我能找到的唯一显着差异是,在domains
被懒散地获取时,急切地获取versions
。 但据我所知,代码正在事务中执行,并且正在加载域列表。 如果不是我会得到一个懒惰的初始化异常,不是吗?
更新 :根据@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!
});
因此,即使集合已经加载,您仍然会有这种奇怪的行为。 这似乎是用于管理延迟集合的代理中缺少或空的spliterator实现。 你怎么看?
顺便说一句,这部署在Glassfish / EclipseLink上
这里的问题来自于其他地方的其他人的错误。 所有这些错误的总和引发了这种错误的行为。
第一个错误:可疑的继承 。 EclipseLink似乎创建了一个代理来管理org.eclipse.persistence.indirection.IndirectList
类型的延迟集合。 这个类扩展了 java.util.Vector
虽然它覆盖了removeRange
所有东西。 亲爱的Eclipse开发人员为什么要扩展一个类来覆盖父级中的几乎所有内容,而不是声明该类实现一个合适的接口( Iterable<E>
, Collection<E>
或List<E>
)?
第二个错误:嘿,我继承了你,但是没有给你一个关于你内部的$#| T. 因此, IndirectList
使用委托来实现延迟加载的神奇功能。 但是,哦,我的! 我如何计算尺寸? 我是否使用(并维护更新)父级的elementCount
属性? 不,当然,我只是将该任务委托给我的委托 ......所以如果父类需要做任何与大小相关的事情,那么,运气不好。 无论如何,我已经超越了所有......而且他们不会在这个课程中添加任何新内容,是吗?
第三个错误:封装破损 。 进入Vector
。 在Java 1.8中,这个类得到了扩充,现在提供了一个spliterator
方法来支持新的流功能。 它们创建了一个静态内部类( VectorSpliterator
),允许客户端使用闪亮的新API遍历矢量。 一切正常,直到你注意到为了知道何时完成遍历,他们使用受保护的实例变量 elementCount
而不是使用公共API方法size()
。 因为谁会扩展非最终类并返回不基于elementCount
的大小? 你看到灾难来了吗?
所以我们在这里, IndirectList
无意识地继承了Vector
新功能(记住它可能不应该首先从它继承),并且用这种错误组合来破坏事物。
总而言之, 在使用EclipseLink(Glassfish中的默认JPA提供程序)时 ,似乎即使对于已加载的集合,对惰性集合的流遍历也不起作用 。 请记住,这些产品来自同一供应商。 万岁!
回到顶端|提供反馈替代方法 :如果您遇到此问题并仍希望利用stream()
提供的函数编程样式,您可以复制该集合,以便构建正确的迭代器。 在我的情况下,我能够将域的所有类似用途保持为修改getDomains
方法的单行。 在这种情况下,我更喜欢代码可读性(具有功能样式)而不是性能:
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<>(domains));
}
读者注意 :抱歉讽刺,但我讨厌用这些东西失去宝贵的开发时间。
感谢@Ferrybig的最初线索
更新 :Bug报告。 如果这对您造成影响,您可以访问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 。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.