2014-11-08 10 views
10

Java8 continua a fare cose strane nel mio ambiente EclipseLink 2.5.2 JPA. Ho dovuto cancellare la domanda https://stackoverflow.com/questions/26806183/java-8-sorting-behaviour ieri dal momento che l'ordinamento in quel caso è stato influenzato da uno strano comportamento dell'APP - Ho trovato una soluzione alternativa per questo costringendo il primo passo di ordinamento prima di fare l'ordinamento finale.Java8 Collections.sort (a volte) non ordina gli elenchi restituiti JPA

Ancora in Java 8 con JPA Eclipselink 2.5.2 il codice seguente alcune volte non esegue l'ordinamento nel mio ambiente (Linux, MacOSX, entrambi con build 1.8.0_25-b17). Funziona come previsto nell'ambiente 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; 
} 

Quando chiamato da un test JUnit, la funzione sopra riportata funziona correttamente. Quando debbuging in un ambiente di produzione ottengo una voce di registro:

INFORMATION: sorting 34 by modification date 

ma in timsort l'istruzione return con nRemaining < 2 è colpito - in modo che nessun ordinamento accade. The IndirectList (vedi What collections does jpa return?) fornito da JPA è considerato come vuoto.

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 

Questa soluzione ordina correttamente:

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

Domanda:

È questo un bug JPA EclipseLink o cosa potevo generalmente fare al riguardo nel mio codice?

Nota: non riesco ancora a modificare il software in conformità con la sorgente Java8. L'ambiente corrente è un runtime Java8.

Sono sorpreso da questo comportamento: è particolarmente fastidioso che il testcase funzioni correttamente mentre in ambiente di produzione c'è un problema.

C'è un progetto di esempio a https://github.com/WolfgangFahl/JPAJava8Sorting che ha una struttura simile al problema originale.

Contiene un esempio http://sscce.org/ con un test JUnit che rende riproducibile il problema richiamando em.clear() scollegando quindi tutti gli oggetti e forzando l'utilizzo di una IndirectList. Vedi questo caso JUnit sotto per riferimento.

Con recupero ansiosi:

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

Il caso unità funziona. Se viene utilizzato FetchType.LAZY o il tipo di recupero viene omesso in JDK 8, il comportamento potrebbe essere diverso rispetto a JDK 7 (dovrò verificarlo ora). Perché è così? In questo momento presumo che sia necessario specificare Eager che recuperi o esegui iterazione una volta sull'elenco da ordinare in genere, manualmente prima dell'ordinamento. Cos'altro si potrebbe fare?

JUnit test

persistence.xml e pom.xml può essere preso da https://github.com/WolfgangFahl/JPAJava8Sorting Il test può essere eseguito con un database MySQL o in-memory con DERBY (default)

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<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/questions/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()); 
    } 
} 
+0

Sei sicuro la collezione si sta cercando di risolvere non viene modificato da qualche fonte esterna? – fge

+0

Tra docs.size() e Collections.sort (documenti, comparatore) c'è solo il costruttore. Il mio debug mostra che questo potrebbe essere di nuovo un problema JPA. L'elenco è di tipo IndirectList e l'ordinamento sembra fidarsi di elementCount che è zero, il modcount è di 2 dimensioni è 2. –

+1

Java8 è stato rilasciato più di 6 mesi fa. Prenderesti davvero un bug nelle raccolte Java8 piuttosto che guardare più da vicino il tuo codice? Usa qualcosa di meglio dei sysout di base (come JPDA) se sei disperato, ma penso che dovresti concentrarti sul tuo codice. –

risposta

13

Bene, questo è un gioco didattico perfetto che ti dice perché i programmatori non dovrebbero estendere classi non progettate per essere sottoclasse. Libri come "Effective Java" ti dicono perché: il tentativo di intercettare ogni metodo per alterarne il comportamento fallirà quando la superclasse evolverà.

Qui, IndirectList si estende Vector e sovrascrive quasi tutti i metodi per modificarne il comportamento, un chiaro anti-pattern. Ora, con Java 8 la classe base si è evoluta.

Da Java 8, le interfacce possono avere default metodi e quindi sono stati aggiunti metodi come sort che hanno il vantaggio che, a differenza Collections.sort, implementazioni possono sostituire il metodo e fornire un'implementazione più adatti alla particolare interface attuazione. Vector esegue questa operazione, per due motivi: ora il contratto che tutti i metodi sono synchronized si espande anche all'ordinamento e l'implementazione ottimizzata può passare il suo array interno al metodo Arrays.sort ignorando l'operazione di copia nota dalle precedenti implementazioni (ArrayList fa lo stesso).

Per ottenere questo vantaggio immediatamente anche per il codice esistente, Collections.sort è stato adattato. Delega a List.sort che delegherà per impostazione predefinita a un altro metodo che implementa il vecchio comportamento di copia tramite toArray e utilizza TimSort. Tuttavia, se un'implementazione di List ha la precedenza su List.sort, influirà sul comportamento di Collections.sort.

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

quindi è un bug. https://github.com/WolfgangFahl/JPAJava8Sorting ora utilizza 2.6.0-M3 ed è riproducibile che se si modifica il runtime il comportamento cambia in "non ordinamento" quando si utilizza il recupero lazy. –

+2

@Wolfgang Fahl: Certo, è un bug. Ho cercato di spiegare che si tratta di un errore di progettazione che va più in profondità del semplice non riuscire a ordinare. È chiaro che i nuovi metodi 'removeIf (Predicate)', 'replaceAll (UnaryOperator)', 'forEach (Consumer)' saranno interrotti anche per 'IndirectList' e quindi l'intero supporto per il flusso come [menzionato da Stuart Marks] (http://stackoverflow.com/questions/26816650/java8-collections-sort-sometimes-does-not-sort-jpa-returned-lists/26841569?noredirect=1#comment42219547_26816650). E anche tutti gli algoritmi in 'Collections' che usano questi nuovi metodi (ora) si romperanno. – Holger

+1

@Wolfgang Fahl: ed è chiaro che l'aggiunta dei metodi di sovrascrittura richiesti (senza modificare l'ereditarietà) sarebbe solo una correzione. A meno che l'errore di progettazione della sottoclasse di 'Vector' non venga risolto, è possibile che si verifichino tali problemi con ogni successiva versione di Java. Ma non so se correggere la vera causa sia possibile in quanto interromperà la compatibilità con il codice aspettandosi che sia una sottoclasse di 'Vector' (il programmatore non dovrebbe farlo perché esiste un'interfaccia' List' per più di quindici anni). anni ora). – Holger

3

Il problema si stanno avendo non è con specie.

timsort si chiama via Arrays.sort che esegue le seguenti operazioni:

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

Così si può vedere la dimensione dell'array timsort sta ottenendo è 0 o 1.

Arrays.sort è chiamato da Collections.sort, che fa quanto segue

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

Quindi il motivo per cui la raccolta non viene ordinata è che restituisce un array vuoto. Quindi la raccolta che viene utilizzata non è conforme all'API delle collezioni restituendo una matrice vuota.

Si dice di avere un livello di persistenza. Quindi sembra che il problema è che la libreria che stai usando recupera le entità in modo lazy e non popola il suo backing array a meno che non lo sia. Dai un'occhiata più da vicino alla collezione che stai cercando di ordinare e vedere come funziona. Il test dell'unità originale non ha mostrato nulla in quanto non stava cercando di ordinare la stessa collezione utilizzata in produzione.

+0

La tua risposta è un po 'sulla strada giusta. Ho cambiato la mia domanda per essere più specifica JPA/IndirectList –

+0

Ho aggiornato la domanda con un test JUnit e un puntatore a un progetto di esempio su github. La lista indiretta si comporta come te. Penso che il petting desideroso possa aggiustare le cose e ci proverò. Ancora questo non spiega la differenza tra il comportamento di JDK7 e JDK8. –

3

Attendere il bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 da correggere. Utilizza la dipendenza sottostante quando è disponibile o un'istantanea.

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

Fino ad allora utilizzare la soluzione dalla questione:

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

o specificare desiderosi recupero dove possibile:

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