繁体   English   中英

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的域时,这一行返回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);
    }
}

我已经包含了getVersionsgetDomains方法,因为我有类似的操作在版本上运行完美。 我能找到的唯一显着差异是,在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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM