Java8 Collections.sort (a veces) no ordena las listas devueltas de JPA

Java8 sigue haciendo cosas raras en mi entorno JPA EclipseLink 2.5.2. Tuve que eliminar la pregunta https://stackoverflow.com/questions/26806183/java-8-sorting-behaviour ayer ya que la clasificación en ese caso estuvo influenciada por un extraño comportamiento de JPA. Encontré una solución alternativa para eso al forzar el primer paso de clasificación antes de hacer la clasificación final.

Todavía en Java 8 con JPA Eclipselink 2.5.2, el siguiente código algunas veces no se ordena en mi entorno (Linux, MacOSX, ambos usando build 1.8.0_25-b17). Funciona como se esperaba en el entorno JDK 1.7.

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

Cuando se llama desde una prueba JUnit, la función anterior funciona correctamente. Cuando se depura en un entorno de producción, obtengo una entrada de registro:

 INFORMATION: sorting 34 by modification date 

pero en TimSort, la statement de devolución con nRemaining <2 es acierta, por lo que no ocurre ninguna clasificación. La Lista indirecta (consulte ¿Qué colecciones jpa return? ) Suministradas por JPA se considera vacía.

 static  void sort(T[] a, int lo, int hi, Comparator 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 

Esta solución alternativa se ordena correctamente:

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

Pregunta:

¿Es esto un error de JPA Eclipselink o qué podría hacer generalmente al respecto en mi propio código?

Tenga en cuenta que aún no puedo cambiar el software a la fuente de Java8. El entorno actual es un tiempo de ejecución Java8.

Estoy sorprendido por este comportamiento, es especialmente molesto que el testcase se ejecute correctamente, mientras que en el entorno de producción hay un problema.

Hay un proyecto de ejemplo en https://github.com/WolfgangFahl/JPAJava8Sorting que tiene una estructura comparable a la del problema original.

Contiene un http://sscce.org/ ejemplo con una prueba JUnit que hace que el problema sea reproducible llamando a em.clear () separando así todos los objetos y forzando el uso de una IndirectList. Vea este caso JUnit a continuación para referencia.

Con ansiosa búsqueda:

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

El caso de la Unidad funciona. Si se usa FetchType.LAZY o se omite el tipo de búsqueda en JDK 8, el comportamiento puede ser diferente que en JDK 7 (tendré que comprobar esto ahora). ¿Por qué es así? En este momento, supongo que es necesario especificar Buscar con impaciencia o iterar una vez sobre la lista que se ordenará, básicamente, ir a buscar manualmente antes de ordenar. Que mas se podria hacer?

JUnit Test

persistence.xml y pom.xml pueden tomarse de https://github.com/WolfgangFahl/JPAJava8Sorting La prueba se puede ejecutar con una base de datos MYSQL o en la memoria con DERBY (predeterminado)

 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/questions/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 { // @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/questions/8301820/onetomany-relationship-is-not-working @OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER) List 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 getDocuments() { return documents; } /** * @param documents the documents to set */ public void setDocuments(List documents) { this.documents = documents; } /** * get the documents of this folder by name * * @return a sorted list of documents */ public List getDocumentsByName() { List 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 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()); 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:     sorting test org.eclipse.persistence.jpa.PersistenceProvider false      */ // 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 cm@localhost 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 jpaProperties = new HashMap(); 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 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 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()); } } 

Bueno, este es un juego didáctico perfecto que te dice por qué los progtwigdores no deberían extender las clases que no están diseñadas para ser subclasificadas. Libros como “Java efectivo” te dicen por qué: el bash de interceptar cada método para alterar su comportamiento fracasará cuando la superclase evolucione.

Aquí, IndirectList extiende Vector e invalida casi todos los métodos para modificar su comportamiento, un anti-patrón claro. Ahora, con Java 8, la clase base ha evolucionado.

Desde Java 8, las interfaces pueden tener métodos default y por lo tanto se agregaron métodos como sort que tienen la ventaja de que, a diferencia de Collections.sort , las implementaciones pueden anular el método y proporcionar una implementación más adecuada para la implementación de la interface particular. Vector hace esto, por dos razones: ahora el contrato que todos los métodos están synchronized expande a la clasificación también y la implementación optimizada puede pasar su matriz interna al método Arrays.sort omitiendo la operación de copia conocida de implementaciones anteriores ( ArrayList hace lo mismo) .

Para obtener este beneficio de inmediato incluso con el código existente, Collections.sort se ha modernizado. Se delega en List.sort que de forma predeterminada delegará a otro método que implementa el antiguo comportamiento de copiar mediante toArray y utilizando TimSort . Pero si una implementación de List anula List.sort , también afectará el comportamiento de Collections.sort .

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

Espere a que se solucione el error https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 . Use la dependencia a continuación cuando esté disponible o una instantánea.

  org.eclipse.persistence eclipselink 2.6.0  

Hasta entonces, use la solución de la pregunta:

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

o especifique buscar con entusiasmo siempre que sea posible:

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

El problema que estás teniendo no es con el género.

TimSort se llama a través de Arrays.sort que hace lo siguiente:

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

Entonces puede ver que el tamaño de la matriz que recibe TimSort es 0 o 1.

Arrays.sort se llama desde Collections.sort , que hace lo siguiente.

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

Por lo tanto, la razón por la cual su colección no está siendo ordenada es porque devuelve una matriz vacía. Por lo tanto, la colección que se está utilizando no se ajusta a la API de colecciones devolviendo una matriz vacía.

Usted dice que tiene una capa de persistencia. Parece que el problema es que la biblioteca que está utilizando recupera las entidades de forma perezosa y no llena su matriz de respaldo a menos que sea necesario. Eche un vistazo más de cerca a la colección que está tratando de ordenar y vea cómo funciona. La prueba de unidad original no mostró nada, ya que no estaba tratando de ordenar la misma colección que se utiliza en producción.