Java8 Collections.sort(有时)不会对 JPA 返回的列表进行排序 [英] Java8 Collections.sort (sometimes) does not sort JPA returned lists

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

问题描述

Java8 在我的 JPA EclipseLink 2.5.2 环境中不断做奇怪的事情.我不得不删除问题 https://stackoverflow.com/questions/26806183/java-8-sorting-行为昨天,因为这种情况下的排序受到了一种奇怪的 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-behaviour yesterday 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.

仍然在带有 JPA Eclipselink 2.5.2 的 Java 8 中,以下代码有时无法在我的环境中排序(Linux、MacOSX,均使用构建 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(参见 jpa 返回哪些集合?)被认为是空的.

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.

https://github.com/WolfgangFahl/JPAJava8Sorting 有一个示例项目它具有与原始问题类似的结构.

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

它包含一个带有 JUnit 测试的 http://sscce.org/ 示例,该示例可以通过调用 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.

使用急切获取:

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

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

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/JPAJava8Sorting The 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 开始,接口可以有 default 方法,因此添加了像 sort 这样的方法,其优点是与 Collections.sort 不同,实现可以覆盖该方法并提供更适合特定 interface 实现的实现.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 返回的列表进行排序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆