2014-11-08 3 views
10

Java8 macht in meiner JPA EclipseLink 2.5.2-Umgebung immer wieder seltsame Dinge. Ich musste gestern die Frage https://stackoverflow.com/questions/26806183/java-8-sorting-behaviour löschen, da die Sortierung in diesem Fall durch ein merkwürdiges JPA-Verhalten beeinflusst wurde - ich fand einen Workaround für diesen, indem ich den ersten Sortierschritt erzwang, bevor ich die endgültige Sortierung durchführte.Java8 Collections.sort (manchmal) sortiert keine JPA-zurückgegebenen Listen

Noch in Java 8 mit JPA Eclipselink 2.5.2 der folgende Code einige Male nicht sortiert in meiner Umgebung (Linux, MacOSX, beide mit Build 1.8.0_25-b17). Es funktioniert wie erwartet in der JDK 1.7-Umgebung.

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

Beim Aufruf aus einem JUnit-Test funktioniert die obige Funktion korrekt. Wenn in einer Produktionsumgebung debbuging bekomme ich einen Protokolleintrag:

INFORMATION: sorting 34 by modification date 

aber in Timsort die return-Anweisung mit nRemaining < 2 getroffen wird - so keine Sortierung geschieht. Die von JPA gelieferte IndirectList (siehe What collections does jpa return?) gilt als leer.

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 

Diese Problemumgehung sortiert richtig:

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

Frage:

Ist das ein JPA Eclipse Bug oder was könnte ich tun, im Allgemeinen über sie in meinem eigenen Code?

Bitte beachten Sie - Ich kann die Software noch nicht auf Java8 Quellkompatibilität ändern. Die aktuelle Umgebung ist eine Java8-Laufzeitumgebung.

Ich bin überrascht über dieses Verhalten - es ist besonders ärgerlich, dass der Testfall korrekt ausgeführt wird, während es in der Produktionsumgebung ein Problem gibt.

Es gibt ein Beispielprojekt unter https://github.com/WolfgangFahl/JPAJava8Sorting , das eine vergleichbare Struktur wie das ursprüngliche Problem hat.

Es enthält ein http://sscce.org/ Beispiel mit einem JUnit-Test, der das Problem reproduzierbar macht, indem er em.clear() aufruft, wodurch alle Objekte getrennt werden und die Verwendung einer IndirectList erzwungen wird. Sehen Sie sich diesen JUnit-Fall als Referenz an.

Mit eifrig fetching:

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

The Unit Fall funktioniert. Wenn FetchType.LAZY verwendet wird oder der Abruf-Typ in JDK 8 weggelassen wird, kann das Verhalten anders sein als in JDK 7 (das muss ich jetzt überprüfen). Warum ist das so? Zu diesem Zeitpunkt gehe ich davon aus, dass man Eager fetching oder iterate einmal über die zu sortierende Liste im Grunde manuell vor dem Sortieren abrufen muss. Was könnte noch getan werden?

JUnit-Test

persistence.xml und pom.xml kann von https://github.com/WolfgangFahl/JPAJava8Sorting Der Test genommen werden kann, mit einer MySQL-Datenbank oder im Speicher mit 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

Sie sicher, dass die Sammlung Sie sortieren nicht versuchen, geändert wird von einer externen Quelle? – fge

+0

Zwischen docs.size() und Collections.sort (docs, comparator) gibt es nur den Konstruktor. Mein Debugging zeigt, dass dies wieder ein JPA-Problem sein könnte. Die Liste ist eine IndirectList und die Sortierung scheint dem elementCount zu vertrauen, welches Null ist, modcount ist 2 Größe ist 2. –

+1

Java8 wurde vor mehr als 6 Monaten veröffentlicht. Würden Sie wirklich von einem Fehler in Java8 Collections ausgehen, anstatt sich Ihren eigenen Code genauer anzusehen? Verwenden Sie etwas Besseres als grundlegende Systeme (wie JPDA), wenn Sie verzweifelt sind, aber ich denke, dass Sie sich auf Ihren Code konzentrieren sollten. –

Antwort

13

Nun, dies ist ein perfektes didaktisches Spiel, das Ihnen erklärt, warum Programmierer Klassen nicht erweitern sollten, die nicht dafür entworfen wurden, unterklassifiziert zu werden. Bücher wie "Effektives Java" sagen dir warum: Der Versuch, jede Methode zu unterbrechen, um ihr Verhalten zu ändern, wird fehlschlagen, wenn sich die Superklasse entwickelt.

Hier IndirectList erweitert Vector und überschreibt fast alle Methoden, um sein Verhalten zu ändern, ein klares Anti-Muster. Mit Java 8 hat sich die Basisklasse weiterentwickelt.

Da Java 8 können Schnittstellen default Methoden haben und so Methoden wie sort wurden hinzugefügt, die den Vorteil haben, dass, im Gegensatz zu Collections.sort, Implementierungen der Methode überschreiben kann, und stellt eine Implementierung besser geeignet, um die bestimmten interface Implementierung. Vector tut dies, aus zwei Gründen: jetzt der Vertrag, dass alle Methoden sind synchronized erweitert, um auch zu sortieren und die optimierte Implementierung kann seine interne Array an die Arrays.sort Methode überspringen den Kopiervorgang aus früheren Implementierungen bekannt (ArrayList macht das gleiche).

Um diesen Vorteil sofort für bereits vorhandenen Code zu erhalten, wurde nachgerüstet. Er delegiert an List.sort, der standardmäßig zu einer anderen Methode delegieren wird, die das alte Verhalten des Kopierens über toArray implementiert und TimSort verwendet. Wenn jedoch eine List Implementierung die List.sort überschreibt, wirkt sich dies auch auf das Verhalten von aus.

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

so ist es ein Fehler. https://github.com/WolfgangFahl/JPAJava8Sorting verwendet jetzt 2.6.0-M3 und es ist reproduzierbar, dass sich das Verhalten bei Verwendung von Lazy Fetching auf "nicht sortieren" ändert, wenn Sie die Laufzeit ändern. –

+2

@Wolfgang Fahl: Natürlich ist es ein Fehler. Ich versuchte zu erklären, dass es sich um einen Konstruktionsfehler handelt, der tiefer geht als nur das Sortieren. Es ist klar, dass die neuen Methoden 'removeIf (Prädikat)', 'replaceAll (UnaryOperator)', 'forEach (Consumer)' auch für 'IndirectList' durchbrochen werden und so auch die gesamte Stream-Unterstützung wie [von Stuart Marks] (http://stackoverflow.com/questions/26816650/java8-collections-sort-sotimeimes-does-not-sort-jpa-returned-lists/26841569?noredirect=1#comment42219547_26816650). Und alle Algorithmen in "Collections", die diese neuen Methoden (jetzt) ​​verwenden, werden ebenfalls brechen. – Holger

+1

@Wolfgang Fahl: und es ist klar, dass das Hinzufügen der erforderlichen überschreibenden Methoden (ohne die Vererbung zu ändern) nur ein Hotfix wäre. Wenn der Konstruktionsfehler der Unterklasse "Vector" nicht behoben wird, können solche Probleme bei jeder folgenden Java-Version auftreten. Aber ich weiß nicht, ob das Reparieren der wirklichen Ursache möglich ist, da es die Kompatibilität mit dem Code unterbricht und erwartet, dass es eine Unterklasse von 'Vector' ist (vernünftiger Programmierer sollte dies nicht tun, da es die' List' Schnittstelle für mehr als fünfzehn gibt Jahre jetzt). – Holger

3

Das Problem ausgeführt werden Sie haben, ist nicht mit Art.

Timsort über Arrays.sort die Folgendes genannt:

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

So können Sie die Größe des Arrays sehen Timsort wird immer entweder 0 oder 1

Arrays.sort von Collections.sort genannt wird, die tut folgendes.

Der Grund, warum Ihre Sammlung nicht sortiert wird, ist, dass sie ein leeres Array zurückgibt. Die Sammlung, die verwendet wird, stimmt nicht mit der Auflistungs-API überein, indem ein leeres Array zurückgegeben wird.

Sie sagen, Sie haben eine Persistenzschicht. Es scheint also, dass das Problem darin liegt, dass die Bibliothek, die Sie verwenden, Entitäten auf eine faule Art und Weise abruft und nicht ihr Backing-Array auffüllt, es sei denn, es muss. Sehen Sie sich die Sammlung genauer an, die Sie sortieren möchten, und sehen Sie, wie sie funktioniert. Der ursprüngliche Komponententest zeigte nichts, da nicht versucht wurde, die gleiche Sammlung zu sortieren, die in der Produktion verwendet wird.

+0

Sie antworten, ist etwas auf dem richtigen Weg. Ich änderte meine Frage, um JPA/IndirectList spezifisch zu sein –

+0

Ich aktualisierte die Frage mit einem JUnit Test und einem Zeiger zu einem Beispielprojekt auf github. Die IndirectList verhält sich wie Sie darauf hinweisen. Ich denke, eifriges Fechten könnte Dinge reparieren und ich werde es ausprobieren. Dies erklärt jedoch nicht den Unterschied zwischen JDK7- und JDK8-Verhalten. –

3

Warten Sie, bis der Fehler https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 behoben wurde. Verwenden Sie die folgende Abhängigkeit, wenn sie verfügbar ist oder einen Snapshot.

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

Bis dahin die Abhilfe von der Frage verwenden:

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

oder geben eifrig holen, wo möglich:

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