2014-11-08 17 views
10

Java8 ciągle robi dziwne rzeczy w moim środowisku JPA EclipseLink 2.5.2. Wczoraj musiałem usunąć pytanie: https://stackoverflow.com/questions/26806183/java-8-sorting-behaviour , ponieważ na sortowanie w tym przypadku miało wpływ dziwne zachowanie JPA - znalazłem obejście tego problemu przez wymuszenie pierwszego kroku sortowania przed wykonaniem ostatecznego sortowania.Java8 Collections.sort (czasami) nie sortuje zwróconych list JPA

Nadal w Javie 8 z JPA Eclipselink 2.5.2 poniższy kod kilka razy nie sortuje w moim środowisku (Linux, MacOSX, oba przy użyciu wersji 1.8.0_25-b17). Działa zgodnie z oczekiwaniami w środowisku 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; 
} 

Po wywołaniu z testu JUnit powyższa funkcja działa poprawnie. Kiedy debbuging w środowisku produkcyjnym dostaję wpisu:

INFORMATION: sorting 34 by modification date 

ale w TimSort instrukcji return z nRemaining < 2 jest trafiony - tak bez sortowania się dzieje. Lista pośrednia (patrz What collections does jpa return?) dostarczana przez JPA jest uważana za pustą.

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 

To obejście sortuje poprawnie:

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

Pytanie:

Jest to błąd JPA EclipseLink lub co mogę zrobić generalnie w moim kodzie?

Uwaga - Nie mogę jeszcze zmienić oprogramowania na zgodność z Java 8. Obecne środowisko to środowisko wykonawcze Java8.

Jestem zaskoczony tym zachowaniem - szczególnie irytujące jest to, że testcase działa poprawnie, podczas gdy w środowisku produkcyjnym pojawia się problem.

Istnieje przykładowy projekt pod adresem https://github.com/WolfgangFahl/JPAJava8Sorting , który ma porównywalną strukturę jak oryginalny problem.

Zawiera przykład http://sscce.org/ z testem JUnit, który sprawia, że ​​problem jest odtwarzalny, wywołując funkcję em.clear(), oddzielając w ten sposób wszystkie obiekty i wymuszając użycie opcji IndirectList. Zobacz ten przypadek JUnit poniżej w celach informacyjnych.

z upragnieniem pobieraniem:

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

Sprawa Jednostka działa. Jeśli użyto FetchType.LAZY lub typ pobrania został pominięty w JDK 8, zachowanie może być inne niż w JDK 7 (będę musiał to sprawdzić teraz). Dlaczego tak jest? W tym momencie zakładam, że trzeba określić skasowane pobieranie lub powtórzenie raz na liście, która ma być sortowana, po prostu pobierając ręcznie przed sortowaniem. Co jeszcze można zrobić?

JUnit test

persistence.xml i pom.xml można zaczerpnąć z https://github.com/WolfgangFahl/JPAJava8Sorting Test można uruchomić z bazy danych MySQL lub w pamięci z Derby (domyślny)

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

Czy jesteś pewien zbiór starasz się rozwiązać nie jest modyfikowana przez jakieś zewnętrzne źródło? – fge

+0

Pomiędzy docs.size() i Collections.sort (docs, comparator) znajduje się tylko konstruktor. Moje debugowanie pokazuje, że może to znowu być kwestia JPA. Lista jest nazwą pośrednią, a sortowanie wydaje się ufać elementowi Element, który wynosi zero, modcount ma rozmiar 2. 2. –

+1

Java8 zostało wydane ponad 6 miesięcy temu. Czy naprawdę założysz błąd w kolekcjach Java8, zamiast dokładniej przyjrzeć się własnemu kodowi? Użyj czegoś lepszego niż podstawowe sysouts (np. JPDA), jeśli jesteś zdesperowany, ale myślę, że powinieneś skupić się na swoim kodzie. –

Odpowiedz

13

Cóż, jest to doskonała gra dydaktyczna z informacją, dlaczego programiści nie powinni rozszerzać klas, które nie zostały zaprojektowane do subklasowania. Książki takie jak "Efektywna Java" wyjaśniają dlaczego: próba przechwycenia każdej metody zmiany jej zachowania zakończy się niepowodzeniem, gdy superklasa ewoluuje.

W tym miejscu rozciąga się na i zastępuje prawie wszystkie metody, aby zmodyfikować jego zachowanie. Teraz, w Java 8, klasa bazowa ewoluowała.

Ponieważ Java 8, interfejsy mogą mieć default metod i sposobów, tak jak sort dodano, które mają tę zaletę, że w przeciwieństwie do Collections.sort implementacje mogą zastąpić metody i zapewnić implementację bardziej odpowiedni do konkretnego interface realizacji. Vector robi to z dwóch powodów: teraz umowa, że ​​wszystkie metody są również synchronized rozwija się również do sortowania, a zoptymalizowana implementacja może przekazać swoją wewnętrzną tablicę do metody Arrays.sort pomijając operację kopiowania znaną z poprzednich implementacji (ArrayList robi to samo).

Aby natychmiast uzyskać tę korzyść, nawet w przypadku istniejącego kodu, została dodana opcja Collections.sort. To deleguje do List.sort, który domyślnie deleguje do innej metody realizującej stare zachowanie kopiowania przez toArray i używając TimSort. Ale jeśli implementacja List przesłoni List.sort, wpłynie to również na zachowanie się Collections.sort.

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

, więc to błąd. https://github.com/WolfgangFahl/JPAJava8Sorting używa teraz wersji 2.6.0-M3 i jest powtarzalne, że jeśli zmienisz środowisko wykonawcze, zachowanie zmieni się na "nie sortowanie" podczas używania leniwego pobierania. –

+2

@Wolfgang Fahl: Oczywiście, to błąd. Próbowałem wytłumaczyć, że jest to błąd projektowy, który jest głębszy niż zwykłe niepowodzenie w sortowaniu. Oczywiste jest, że nowe metody 'removeIf (Predicate)', replaceAll (UnaryOperator) ',' forEach (Consumer) 'również zostaną złamane dla' IndirectList', tak samo jak cała obsługa strumienia, jak [wspomniane przez Stuarta Marksa] (http://stackoverflow.com/questions/26816650/java8-collections-sort-sometimes-does-not-sort-jpa-returned-lists/26841569?noredirect=1#comment42219547_26816650). Wszystkie algorytmy w 'Kolekcje', które używają tych nowych metod (teraz) również się popsują. – Holger

+1

@Wolfgang Fahl: i jest oczywiste, że dodanie wymaganych metod nadpisywania (bez zmiany dziedziczenia) byłoby tylko poprawką. O ile błąd projektowy podklasy "Vector" nie zostanie naprawiony, mogą wystąpić takie problemy z każdą kolejną wersją Java. Ale nie wiem, czy naprawienie prawdziwej przyczyny jest możliwe, ponieważ złamie kompatybilność z kodem, spodziewając się, że jest podklasą 'Vector' (rozsądny programista nie powinien tego robić, ponieważ istnieje interfejs' List' od ponad piętnastu lat). – Holger

3

Problem masz nie jest z rodzaju.

TimSort nazywa poprzez Arrays.sort który wykonuje następujące czynności:

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

Więc można zobaczyć rozmiar tablicy TimSort jest coraz jest 0 lub 1.

Arrays.sort jest wywoływana z Collections.sort, który wykonuje następujące czynności.

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

Powodem, dla którego twoja kolekcja nie jest sortowana jest to, że zwraca pustą tablicę. Tak więc używana kolekcja nie jest zgodna z interfejsem API kolekcji, zwracając pustą tablicę.

Mówisz, że masz warstwę trwałości. Wygląda na to, że problem polega na tym, że biblioteka, z której korzystasz, pobiera obiekty w leniwy sposób i nie wypełnia swojej tablicy, chyba że musi. Przyjrzyj się kolekcji, którą próbujesz sortować i zobacz, jak działa. Twój oryginalny test jednostkowy nie pokazał niczego, ponieważ nie próbował sortować tej samej kolekcji, która jest używana w produkcji.

+0

Twoja odpowiedź jest nieco na dobrej drodze. Zmieniłem moje pytanie, aby było więcej WZP/IndirectList specyficzne –

+0

I'v zaktualizowałem pytanie za pomocą testu JUnit i wskaźnika do przykładowego projektu na github. Lista pośrednia zachowuje się tak, jak wskazujesz. Myślę, że chętne fekwencje mogą naprawić rzeczy, a ja to wypróbuję. Nadal nie wyjaśnia to różnicy między zachowaniem JDK7 i JDK8. –

3

Poczekaj na naprawienie błędu https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236. Użyj zależności poniżej, gdy jest ona dostępna lub migawki.

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

Do następnie użyć obejście z pytaniem:

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

lub określić chętny pobierania w miarę możliwości:

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