Stream API无法在EclipseLink/Glassfish中的延迟加载的集合中使用? [英] Stream API not working for lazy loaded collections in EclipseLink / Glassfish?

查看:105
本文介绍了Stream API无法在EclipseLink/Glassfish中的延迟加载的集合中使用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在检测到我的一个Web服务中的缺陷后,我将错误归结为以下一种情况:

After detecting a flaw in one of my web services I tracked down the error to the following one-liner:

return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));

当我明确知道域列表包含名称等于提供的name的域时,此行返回false.因此,挠了一下头之后,我最终将整条线分开,看看发生了什么.在调试会话中,我得到了以下内容:

This line was returning false when I positively knew that the list of domains contained a domain which name was equal to the provided name. So after scratching my head for a while, I ended up splitting the whole line to see what was going on. I got the following in my debugging session:

请注意以下行:

List<Domain> domains2 = domains.stream().collect(Collectors.toList());

根据调试器,domains是一个包含两个元素的列表.但是在应用.stream().collect(Collectors.toList())之后,我得到一个完全空的列表.如果我错了,请纠正我,但是据我了解,那应该是身份操作,并返回相同的列表(如果严格的话,请返回它的副本).那么这是怎么回事呢???

According to the debugger, domains is a list with two elements. But after applying .stream().collect(Collectors.toList()) I get a completely empty list. Correct me if I'm wrong, but from what I understand, that should be the identity operation and return the same list (or a copy of it if we are strict). So what is going on here???

在您问:不,我完全没有操纵该屏幕截图.

将其放在上下文中,此代码是在具有状态请求范围的EJB中使用JPA管理的实体在扩展持久性上下文中进行字段访问的情况下执行的.这里有一些与手头问题有关的代码:

To put this in context, this code is executed in a stateful request scoped EJB using JPA managed entities with field access in a extended persistence context. Here you have some parts of the code relevant to the problem at hand:

@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方法,因为我在版本上完美地运行了类似的操作.我能够找到的唯一显着差异是,versions是热切获取的,而domains是懒洋洋的获取的.但是据我所知,代码是在事务内执行的,并且正在加载域列表.如果没有,我会得到一个懒惰的初始化异常,不是吗?

I've included both getVersions and getDomains methods because I have similar operations running flawlessly on versions. The only significant difference I'm able to find is that versions are eagerly fetched while domains are lazily fetched. But as far as I know the code is being executed inside a transaction and the list of domains is being loaded. If not I'd get a lazy initialization exception, wouldn't I?

更新:遵循@Ferrybig的建议,我进一步研究了该问题,并且似乎与不正确的延迟加载没有任何关系.如果我以经典方式遍历集合,我仍然无法使用流获得正确的结果:

UPDATE: Following @Ferrybig 's suggestion I've investigated the issue a bit further, and there doesn't seem to have anything to do with improper lazy loading. If I traverse the collection in a classic way I still can't get proper results using streams:

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!
});

因此,即使加载了集合,您似乎仍然会有这种奇怪的行为.在用于管理惰性集合的代理中,这似乎是缺少的或空白的分隔符实现.你觉得呢?

So it seems that even when the collection has been loaded you still have that odd behavior. This seems to be a missing or empty spliterator implementation in the proxy used to manage lazy collections. What do you think?

顺便说一句,它已部署在Glassfish/EclipseLink

BTW, this is deployed on Glassfish / EclipseLink

推荐答案

这里的问题来自在某些地方其他人的错误.所有这些错误的总和会引发这种越野车行为.

The problem here comes from a combination of somebody else's mistakes in several places. The sum of all those mistakes provokes this buggy behavior.

第一个错误:可疑继承. EclipseLink似乎创建了一个代理来管理类型为org.eclipse.persistence.indirection.IndirectList的惰性集合.此类扩展了 java.util.Vector,尽管它覆盖了除removeRange之外的所有内容.亲爱的Eclipse开发人员,到底为什么要扩展一个类以覆盖父类中的几乎所有内容,而不是声明该类来实现合适的接口(Iterable<E>Collection<E>List<E>)?

First mistake: Dubious inheritance. EclipseLink seems to create a proxy to manage lazy collections of type org.eclipse.persistence.indirection.IndirectList. This class extends java.util.Vector although it overrides everything but removeRange. Why on earth, dear Eclipse developers, do you extend a class to override almost everything in the parent, instead of declaring that class to implement a suitable interface (Iterable<E>, Collection<E> or List<E>)?

第二个错误:嘿,我是从您那里继承过来的,但是没有提供有关您的内部信息的$#| T .因此,IndirectList做到了使用 delegate 延迟加载内容的魔力.但是,天哪!如何计算尺寸?我是否使用(并保持更新)父级的elementCount属性?不,当然,我只是将该任务委托给我的 delegate ...,因此,如果父类需要做一些与大小有关的事情,那么运气不好.无论如何,我已经覆盖了所有内容……他们不会在该课程中添加任何新内容,对吗?

Second mistake: Hey, I inherit from you but don't give a $#|T about your internals. So IndirectList does its magic of lazy loading things using a delegate. But, oh my! How do I compute size? Do I use (and maintain updated) the parent's elementCount property? No, of course, I just delegate that task to my delegate... so if the parent class needs to do anything related to size, well, bad luck. Anyway I've overrided everything... and they won't add anything new to that class, will they?

第三个错误:封装损坏.输入Vector.在Java 1.8中,该类得到了增强,现在提供了spliterator方法来支持新的流功能.他们创建了一个静态内部类(VectorSpliterator),该类允许客户端使用闪亮的新API遍历向量.一切正常,直到您注意到为了知道何时完成遍历,他们使用受受保护的实例变量 elementCount而不是使用公共API方法size().因为谁会扩展非最终类并返回不基于elementCount的大小?你看到灾难来了吗?

Third mistake: Encapsulation breakage. Enters Vector. In Java 1.8 this class is augmented and now provides a spliterator method to support the new stream functionalities. They create a static inner class (VectorSpliterator) that lets clients traverse the vector using the shiny new API. Everything ok until you notice that in order to know when to finish the traversal they use the protected instance variable elementCount instead of using the public API method size(). Because who would extend a non final class and return a size not based on elementCount? Do you see the disaster coming?

所以我们在这里,IndirectList毫无意识地从Vector继承了新功能(请记住,它可能首先不应该从它继承),并通过这种错误组合破坏事物.

So here we are, IndirectList is unawarely inheriting new functionality from Vector (remember that it probably shouldn't inherit from it in first place), and breaking things with this combination of mistakes.

总结起来,似乎在使用EclipseLink(Glassfish中的默认JPA提供程序)时,即使对于已经加载的集合,对惰性集合的流遍历也不起作用.请记住,这些产品来自同一供应商.哇!

Summing up, it seems that stream traversal of lazy collections won't work even for already loaded collections when using EclipseLink (default JPA provider in Glassfish). Remember that these products come from the same vendor. Hooray!

WORKAROUND :如果您遇到此问题,但仍想利用stream()提供的功能编程样式,则可以制作该集合的副本,以便构建适当的迭代器.就我而言,我能够保留所有类似的域用法,就像修改getDomains方法一样.在这种情况下,我更喜欢代码的可读性(带有功能样式)而不是性能:

WORKAROUND: In case you face this problem and still want to leverage the functional programming style provided by stream() you can make a copy of the collection so a proper iterator is built. In my case I was able to keep all similar uses of domains as one-liners modifying the getDomains method. I favor code readability (with functional style) over performance in this case:

public List<Domain> getDomains() {
    return Collections.unmodifiableList(new ArrayList<>(domains));
}

注意读者:很抱歉,但是我讨厌在这些事情上浪费我宝贵的开发时间.

NOTE TO THE READER: Sorry for the sarcasm, but I hate to lose my precious development time with these things.

感谢@Ferrybig提供了最初的线索

Thanks to @Ferrybig for the initial clue

更新:报告了错误.如果遇到问题,可以通过 https://bugs.eclipse跟踪其进度. .org/bugs/show_bug.cgi?id = 487799

UPDATE: Bug reported. If this has hit you, you can follow its progress at https://bugs.eclipse.org/bugs/show_bug.cgi?id=487799

这篇关于Stream API无法在EclipseLink/Glassfish中的延迟加载的集合中使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆