Java8 Collections.sort(иногда) не сортирует возвращаемые списки JPA

Java8 продолжает делать странные вещи в моей среде JPA EclipseLink 2.5.2. Мне пришлось удалить вопрос https://stackoverflow.com/info/26806183/java-8-sorting-behaviour вчера, так как сортировка в этом случае зависела от странного поведения JPA - я нашел обходное решение для этого, заставив первый шаг сортировки, прежде чем делать окончательный вид.

Еще в Java 8 с JPA Eclipselink 2.5.2 следующий код несколько раз не сортируется в моей среде (Linux, MacOSX, как с использованием build 1.8.0_25-b17). Он работает как ожидается в среде JDK 1.7.

public List<Document> getDocumentsByModificationDate() {
    List<Document> docs=this.getDocuments();
    LOGGER.log(Level.INFO,"sorting "+docs.size()+" by modification date");
    Comparator<Document> comparator=new ByModificationComparator();
    Collections.sort(docs,comparator);
    return docs;
}

При вызове из теста JUnit вышеуказанная функция работает правильно. При отладке в рабочей среде я получаю запись в журнале:

INFORMATION: sorting 34 by modification date

но в TimSort оператор return с nRemaining < 2 - так что сортировка не происходит. Список IndirectList (см. Какие коллекции возвращает jpa?), предоставленный JPA, считается пустым.

static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

Это временное решение сортируется правильно:

   if (docs instanceof IndirectList) {
        IndirectList iList = (IndirectList)docs;
        Object sortTargetObject = iList.getDelegateObject();
        if (sortTargetObject instanceof List<?>) {
            List<Document> sortTarget=(List<Document>) sortTargetObject;
            Collections.sort(sortTarget,comparator);
        }
    } else {
        Collections.sort(docs,comparator);
    }

Вопрос:

Является ли это ошибкой JPA Eclipselink или что я могу сделать с этим в своем собственном коде?

Обратите внимание: я не могу изменить программное обеспечение на соответствие требованиям Java8. Текущая среда - это среда выполнения Java8.

Я удивлен этим поведением - это особенно раздражает то, что тестовый тест работает правильно, а в производственной среде есть проблема.

Существует пример проекта https://github.com/WolfgangFahl/JPAJava8Sorting который имеет сопоставимую структуру как исходную проблему.

Он содержит http://sscce.org/ пример с тестом JUnit, который делает проблему воспроизводимой, вызывая em.clear(), тем самым отделяя все объекты и принуждение использования IndirectList. См. Этот случай JUnit ниже для справки.

С нетерпением:

// https://stackoverflow.com/info/8301820/onetomany-relationship-is-not-working
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER)

Работает блок. Если используется FetchType.LAZY или тип исключений опущен в JDK 8, поведение может быть иным, чем в JDK 7 (мне нужно будет это проверить сейчас). Почему это так? В это время я предполагаю, что нужно задать выбор Eager или перебрать один раз над списком, который будет отсортирован в основном для загрузки вручную перед сортировкой. Что еще можно сделать?

Тест JUnit

persistence.xml и pom.xml могут быть взяты из https://github.com/WolfgangFahl/JPAJava8Sorting Тест можно запустить с помощью базы данных MYSQL или в памяти с помощью DERBY (по умолчанию)

package com.bitplan.java8sorting;

import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.Table;

import org.eclipse.persistence.indirection.IndirectList;
import org.junit.Test;

/**
 * Testcase for 
 * https://stackoverflow.com/info/26816650/java8-collections-sort-sometimes-does-not-sort-jpa-returned-lists
 * @author wf
 *
 */
public class TestJPASorting {

  // the number of documents we want to sort
  public static final int NUM_DOCUMENTS = 3;

  // Logger for debug outputs
  protected static Logger LOGGER = Logger.getLogger("com.bitplan.java8sorting");

  /**
   * a classic comparator
   * @author wf
   *
   */
  public static class ByNameComparator implements Comparator<Document> {

    // @Override
    public int compare(Document d1, Document d2) {
      LOGGER.log(Level.INFO,"comparing " + d1.getName() + "<=>" + d2.getName());
      return d1.getName().compareTo(d2.getName());
    }
  }

  // Document Entity - the sort target
  @Entity(name = "Document")
  @Table(name = "document")
  @Access(AccessType.FIELD)
  public static class Document {
    @Id
    String name;

    @ManyToOne
    Folder parentFolder;

    /**
     * @return the name
     */
    public String getName() {
      return name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
      this.name = name;
    }
    /**
     * @return the parentFolder
     */
    public Folder getParentFolder() {
      return parentFolder;
    }
    /**
     * @param parentFolder the parentFolder to set
     */
    public void setParentFolder(Folder parentFolder) {
      this.parentFolder = parentFolder;
    }
  }

  // Folder entity - owning entity for documents to be sorted
  @Entity(name = "Folder")
  @Table(name = "folder")
  @Access(AccessType.FIELD)
  public static class Folder {
    @Id
    String name;

    // https://stackoverflow.com/info/8301820/onetomany-relationship-is-not-working
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER)
    List<Document> documents;

    /**
     * @return the name
     */
    public String getName() {
      return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
      this.name = name;
    }

    /**
     * @return the documents
     */
    public List<Document> getDocuments() {
      return documents;
    }

    /**
     * @param documents the documents to set
     */
    public void setDocuments(List<Document> documents) {
      this.documents = documents;
    }

    /**
     * get the documents of this folder by name
     * 
     * @return a sorted list of documents
     */
    public List<Document> getDocumentsByName() {
      List<Document> docs = this.getDocuments();
      LOGGER.log(Level.INFO, "sorting " + docs.size() + " documents by name");
      if (docs instanceof IndirectList) {
        LOGGER.log(Level.INFO, "The document list is an IndirectList");
      }
      Comparator<Document> comparator = new ByNameComparator();
      // here is the culprit - do or don't we sort correctly here?
      Collections.sort(docs, comparator);
      return docs;
    }

    /**
     * get a folder example (for testing)
     * @return - a test folder with NUM_DOCUMENTS documents
     */
    public static Folder getFolderExample() {
      Folder folder = new Folder();
      folder.setName("testFolder");
      folder.setDocuments(new ArrayList<Document>());
      for (int i=NUM_DOCUMENTS;i>0;i--) {
        Document document=new Document();
        document.setName("test"+i);
        document.setParentFolder(folder);
        folder.getDocuments().add(document);
      }
      return folder;
    }
  }

  /** possible Database configurations
  using generic persistence.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <!-- generic persistence.xml which only specifies a persistence unit name -->
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
      version="2.0">
      <persistence-unit name="com.bitplan.java8sorting" transaction-type="RESOURCE_LOCAL">
        <description>sorting test</description>
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes> 
        <properties>
        <!--  set programmatically -->
         </properties>
      </persistence-unit>
    </persistence>
  */
  // in MEMORY database
  public static final JPASettings JPA_DERBY=new JPASettings("Derby","org.apache.derby.jdbc.EmbeddedDriver","jdbc:derby:memory:test-jpa;create=true","APP","APP");
  // MYSQL Database
  //  needs preparation:
  //    create database testsqlstorage;
  //    grant all privileges on testsqlstorage to [email protected] identified by 'secret';
  public static final JPASettings JPA_MYSQL=new JPASettings("MYSQL","com.mysql.jdbc.Driver","jdbc:mysql://localhost:3306/testsqlstorage","cm","secret");

  /**
   * Wrapper class for JPASettings
   * @author wf
   *
   */
  public static class JPASettings {
    String driver;
    String url;
    String user;
    String password;
    String targetDatabase;

    EntityManager entityManager;
    /**
     * @param driver
     * @param url
     * @param user
     * @param password
     * @param targetDatabase
     */
    public JPASettings(String targetDatabase,String driver, String url, String user, String password) {
      this.driver = driver;
      this.url = url;
      this.user = user;
      this.password = password;
      this.targetDatabase = targetDatabase;
    }

    /**
     * get an entitymanager based on my settings
     * @return the EntityManager
     */
    public EntityManager getEntityManager() {
      if (entityManager == null) {
        Map<String, String> jpaProperties = new HashMap<String, String>();
        jpaProperties.put("eclipselink.ddl-generation.output-mode", "both");
        jpaProperties.put("eclipselink.ddl-generation", "drop-and-create-tables");
        jpaProperties.put("eclipselink.target-database", targetDatabase);
        jpaProperties.put("eclipselink.logging.level", "FINE");

        jpaProperties.put("javax.persistence.jdbc.user", user);
        jpaProperties.put("javax.persistence.jdbc.password", password);
        jpaProperties.put("javax.persistence.jdbc.url",url);
        jpaProperties.put("javax.persistence.jdbc.driver",driver);

        EntityManagerFactory emf = Persistence.createEntityManagerFactory(
            "com.bitplan.java8sorting", jpaProperties);
        entityManager = emf.createEntityManager();
      }
      return entityManager;
    }
  }

  /**
   * persist the given Folder with the given entityManager
   * @param em - the entityManager
   * @param folderJpa - the folder to persist
   */
  public void persist(EntityManager em, Folder folder) {
    em.getTransaction().begin();
    em.persist(folder);
    em.getTransaction().commit();    
  }

  /**
   * check the sorting - assert that the list has the correct size NUM_DOCUMENTS and that documents
   * are sorted by name assuming test# to be the name of the documents
   * @param sortedDocuments - the documents which should be sorted by name
   */
  public void checkSorting(List<Document> sortedDocuments) {
    assertEquals(NUM_DOCUMENTS,sortedDocuments.size());
    for (int i=1;i<=NUM_DOCUMENTS;i++) {
      Document document=sortedDocuments.get(i-1);
      assertEquals("test"+i,document.getName());
    }
  }

  /**
   * this test case shows that the list of documents retrieved will not be sorted if 
   * JDK8 and lazy fetching is used
   */
  @Test
  public void testSorting() {
    // get a folder with a few documents
    Folder folder=Folder.getFolderExample();
    // get an entitymanager JPA_DERBY=inMemory JPA_MYSQL=Mysql disk database
    EntityManager em=JPA_DERBY.getEntityManager();
    // persist the folder
    persist(em,folder);
    // sort list directly created from memory
    checkSorting(folder.getDocumentsByName());

    // detach entities;
    em.clear();
    // get all folders from database
    String sql="select f from Folder f";
    Query query = em.createQuery(sql);
    @SuppressWarnings("unchecked")
    List<Folder> folders = query.getResultList();
    // there should be exactly one
    assertEquals(1,folders.size());
    // get the first folder
    Folder folderJPA=folders.get(0);
    // sort the documents retrieved
    checkSorting(folderJPA.getDocumentsByName());
  }
}

Ответ 1

Ну, это прекрасная дидактическая игра, рассказывающая вам, почему программисты не должны расширять классы, не предназначенные для подкласса. Такие книги, как "Эффективная Java", говорят вам, почему: попытка перехвата каждого метода для изменения его поведения будет терпеть неудачу при развитии суперкласса.

Здесь IndirectList extends Vector и переопределяет почти все методы, чтобы изменить его поведение, ясный анти-шаблон. Теперь с Java 8 базовый класс развился.

Так как Java 8, интерфейсы могут иметь методы default, и поэтому были добавлены методы, такие как sort, которые имеют то преимущество, что в отличие от Collections.sort реализации могут переопределить этот метод и обеспечить реализацию, более подходящую для конкретного interface реализация. Vector делает это по двум причинам: теперь контракт, что все методы synchronized также расширяется для сортировки, а оптимизированная реализация может передать свой внутренний массив методу Arrays.sort, пропуская операцию копирования, известную из предыдущих реализаций (ArrayList делает то же самое).

Чтобы получить это преимущество сразу даже для существующего кода, Collections.sort был доработан. Он делегирует List.sort, который по умолчанию делегирует другой метод, реализующий старое поведение копирования через toArray и используя TimSort. Но если реализация List переопределяет List.sort, это также повлияет на поведение Collections.sort.

                  interface method              using internal
                  List.sort                     array w/o copying
Collections.sort ─────────────────> Vector.sort ─────────────────> Arrays.sort

Ответ 2

Проблема, с которой вы сталкиваетесь, не сорта.

TimSort вызывается через Arrays.sort, который выполняет следующие действия:

TimSort.sort(a, 0, a.length, c, null, 0, 0);

Итак, вы можете увидеть размер массива, который получает TimSort, либо 0, либо 1.

Arrays.sort вызывается из Collections.sort, что делает следующее.

Object[] a = list.toArray();
Arrays.sort(a, (Comparator)c);

Поэтому причина, по которой ваша коллекция не сортируется, заключается в том, что она возвращает пустой массив. Таким образом, коллекция, которая используется, не соответствует API-интерфейсам коллекций, возвращая пустой массив.

Вы говорите, что у вас есть слой persistence. Похоже, проблема заключается в том, что библиотека, которую вы используете, извлекает объекты ленивым способом и не заполняет свой поддерживающий массив, если это не нужно. Познакомьтесь с коллекцией, которую вы пытаетесь отсортировать, и посмотрите, как она работает. Ваш оригинальный unit test ничего не показывал, поскольку он не пытался сортировать ту же коллекцию, которая используется в производстве.

Ответ 3

Подождите, пока не будет исправлена ​​ошибка https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236. Используйте приведенную ниже зависимость, когда она станет доступной или снимок.

<dependency>
  <groupId>org.eclipse.persistence</groupId>
  <artifactId>eclipselink</artifactId>
  <version>2.6.0</version>
</dependency>

До тех пор используйте обходной путь из вопроса:

if (docs instanceof IndirectList) {
    IndirectList iList = (IndirectList)docs;
    Object sortTargetObject = iList.getDelegateObject();
    if (sortTargetObject instanceof List<?>) {
        List<Document> sortTarget=(List<Document>) sortTargetObject;
        Collections.sort(sortTarget,comparator);
    }
} else {
    Collections.sort(docs,comparator);
}

или указать, где возможно, желаемый выбор:

// http://stackoverflow.com/info/8301820/onetomany-relationship-is-not-working
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER)