Stream API не работает для лениво загруженных коллекций в EclipseLink / Glassfish?



После обнаружения дефекта в одном из моих веб-сервисов я отследил ошибку до следующего однострочного:



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


Эта строка возвращала false, когда я положительно знал, что список доменов содержит домен, имя которого было равно указанному name. Поэтому, почесав немного в затылке, я в конце концов разделил всю линию, чтобы посмотреть, что происходит. Я получил следующее В моей сессии отладки:



Скриншот сеанса отладки



Пожалуйста, обратите внимание на следующее строка:



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, потому что у меня есть аналогичные операции, выполняемые безупречно на версиях. Единственное существенное различие, которое я могу найти, заключается в том, что versions охотно выбираются, а domains лениво выбираются. Но насколько мне известно код выполняется внутри транзакции и списка доменов идет погрузка. Если нет, я получу исключение ленивой инициализации, не так ли?



UPDATE : следуя предложению @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

642   2  

2 ответов:

Проблема здесь возникает из-за комбинации чужих ошибок в нескольких местах. Сумма всех этих ошибок провоцирует такое поведение багги.

Первая ошибка: сомнительное наследство . 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 вместо использования метода public API size(). Потому что кто будет расширять не конечный класс и возвращать размер, не основанный на elementCount? Вы видите приближение катастрофы?

Итак, мы здесь, IndirectList неосознанно наследует новую функциональность от Vector (помните, что она, вероятно, не должна наследовать от него в первую очередь), и ломает вещи с этой комбинацией ошибок.

Подводя итог, кажется, что обход потока ленивых коллекции не будут работать даже для уже загруженных коллекций при использовании EclipseLink (поставщик JPA по умолчанию в Glassfish) . Помните, что эти продукты поступают от одного и того же поставщика. Ура!

Обходной путь: Если вы столкнулись с этой проблемой и все еще хотите использовать функциональный стиль программирования, предоставляемый stream(), вы можете сделать копию коллекции, чтобы построить правильный итератор. В моем случае я смог сохранить все подобные использования доменов в качестве однострочных, модифицирующих getDomains метод. В этом случае я предпочитаю читаемость кода (с функциональным стилем) производительности:

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

примечание для читателя : извините за сарказм, но я ненавижу терять свое драгоценное время разработки с этими вещами.

Спасибо @Ferrybig за первоначальный ключ

UPDATE : сообщение об ошибке. Если это поразило вас, вы можете следить за его прогрессом в 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 stream API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470 .

Comments

    Ничего не найдено.