本文介绍了Java8 Collections.sort(有时)不会对JPA返回的列表进行排序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Java8在我的JPA EclipseLink 2.5.2环境中继续做着奇怪的事情。我不得不删除
昨天因为在这种情况下的排序受到奇怪的JPA行为的影响 - 我通过强制执行最后排序之前的第一个排序步骤找到了解决方法。

Java8 keeps doing strange things in my JPA EclipseLink 2.5.2 environment. I had to delete the question https://stackoverflow.com/questions/26806183/java-8-sorting-behaviouryesterday since the sorting in that case was influenced by a strange JPA behaviour - I found a workaround for that one by forcing the first sort step before doing the final sort.

仍然在Java 8中使用JPA Eclipselink 2.5.2以下代码有时候在我的环境中不排序(Linux,MacOSX,都使用build 1.8.0_25-b17)。它在JDK 1.7环境中按预期工作。

Still in Java 8 with JPA Eclipselink 2.5.2 the following code some times does not sort in my environment (Linux, MacOSX, both using build 1.8.0_25-b17). It works as expected in the JDK 1.7 environment.

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测试调用时,上述函数正常工作。
在生产环境中进行部署时,我会得到一个日志条目:

When called from a JUnit test the above function works correctly.When debbuging in a production environment I get a log entry:

INFORMATION: sorting 34 by modification date

但在TimSort中,带有nRemaining的返回语句<命中2 - 所以没有排序。 JPA提供的IndirectList(参见)被认为是空的。

but in TimSort the return statement with nRemaining < 2 is hit - so no sorting happens. The IndirectList (see What collections does jpa return?) supplied by JPA is considered to be empty.

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运行时。

Please note - I can't change the software to Java8 source compliance yet. The current environment is a Java8 runtime.

我对这种行为感到很惊讶 - 测试用例在生产环境中运行时出现问题尤其令人讨厌。

I am suprised about this behaviour - it's especially annoying that the testcase runs correctly while in production environment there is a problem.

上有一个示例项目b $ b具有与原始问题相当的结构。

There is an example project at https://github.com/WolfgangFahl/JPAJava8Sortingwhich has a comparable structure as the original problem.

它包含 http://sscce.org/使用JUnit测试的示例,通过调用em.clear()从而分离所有对象并强制使用IndirectList,从而使问题可重现。请参阅下面的JUnit案例以供参考。

It contains a http://sscce.org/ example with a JUnit test which makes the issue reproducible by calling em.clear() thus detaching all objects and forcing the use of an IndirectList. See this JUnit case below for reference.

急切提取:

With eager fetching:

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

单位案例有效。如果使用FetchType.LAZY或者在JDK 8中省略了fetch类型,则行为可能与JDK 7中的行为不同(我现在必须检查它)。
为什么会这样?
此时我假设需要在列表上指定Eager提取或迭代一次,以便在排序之前进行基本上手动提取。 还能做些什么?

The Unit case works. If FetchType.LAZY is used or the fetch type is omitted in JDK 8 the behaviour might be different than in JDK 7 (I'll have to check this now). Why is that so?At this time I assume one needs to specify Eager fetching or iterate once over the list to be sorted basically fetching manually before sorting. What else could be done?

JUnit测试

persistence.xml和pom.xml可以从 https://github.com/WolfgangFahl/JPAJava8Sorting
测试可以使用MYSQL数据库运行,也可以使用DERBY运行内存(默认)

persistence.xml and pom.xml can be taken from https://github.com/WolfgangFahl/JPAJava8SortingThe test can be run with a MYSQL database or in-memory with 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 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<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());
  }
}


推荐答案

好,这是一个完美的教学游戏,告诉你为什么程序员不应该扩展不是为子类设计的类。像Effective Java这样的书籍告诉你原因:当超类演变时,试图拦截每个方法来改变它的行为都会失败。

Well, this is a perfect didactic play telling you why programmers shouldn’t extend classes not designed for being subclassed. Books like "Effective Java" tell you why: the attempt to intercept every method to alter its behavior will fail when the superclass evolves.

这里, IndirectList 扩展 Vector 并覆盖几乎所有方法来修改其行为,这是一种清晰的反模式。现在,使用Java 8,基类已经发展。

Here, IndirectList extends Vector and overrides almost all methods to modify its behavior, a clear anti-pattern. Now, with Java 8 the base class has evolved.

从Java 8开始,接口可以有默认方法等等添加了 sort 等方法,其优点是,与 Collections.sort 不同,实现可以覆盖该方法并提供实现更适合特定的接口实现。 Vector 这样做有两个原因:现在所有方法都是 synchronized 的合同也扩展到排序和优化实现可以将其内部数组传递给 Arrays.sort 方法,跳过前面实现中已知的复制操作( ArrayList 同样)。

Since Java 8, interfaces can have default methods and so methods like sort were added which have the advantage that, unlike Collections.sort, implementations can override the method and provide an implementation more suitable to the particular interface implementation. Vector does this, for two reasons: now the contract that all methods are synchronized expands to sorting as well and the optimized implementation can pass its internal array to the Arrays.sort method skipping the copying operation known from previous implementations (ArrayList does the same).

为了立即获得现有代码的好处, Collections.sort 已经过改装。它委托给 List.sort ,它默认委托给另一个实现复制旧行为的方法,通过 toArray 并使用 TimSort 。但是如果 List 实现覆盖 List.sort ,它将影响 Collections.sort的行为

To get this benefit immediately even for existing code, Collections.sort has been retrofitted. It delegates to List.sort which will by default delegate to another method implementing the old behavior of copying via toArray and using TimSort. But if a List implementation overrides List.sort it will affect the behavior of Collections.sort as well.

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

这篇关于Java8 Collections.sort(有时)不会对JPA返回的列表进行排序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!