eclipselink icon indicating copy to clipboard operation
eclipselink copied to clipboard

JPQL queries that select ElementCollection attributes have incorrect results

Open ajaypaul-ibm opened this issue 1 year ago • 8 comments

Describe the bug When JPQL queries select an attribute that is a ElementCollection (if using annotations) or element-collection (if using XML) and getResultList is used to obtain the result, instead of returning a List of one or more Collection(s), EclipseLink appears to be combining all of the collection values into a single list of values and returning it to the caller.

This can be surfaced to the application in a variety of ways, such as:


DataJPATestServlet.testCollectionAttribute: expected:<[507]> but was:<507>
at test.jakarta.data.jpa.web.DataJPATestServlet.testCollectionAttribute(DataJPATestServlet.java:439)

In the above case, the result list contained an Integer rather than a Set<Integer>, despite issuing a query SELECT o.areaCodes FROM City o WHERE (o.name=?1 AND o.stateName=?2) on o.areaCodes which is a collection.

Exception [EclipseLink-0] (Eclipse Persistence Services - 4.0.2.v202306161219): org.eclipse.persistence.exceptions.JPQLException
Exception Description: Problem compiling [SELECT NEW test.jakarta.data.jpa.web.AreaInfo(o.name, o.stateName, o.areaCodes) FROM City o WHERE (o.stateName=?1) ORDER BY o.name].
[67, 78] The state field path 'o.areaCodes' cannot be resolved to a collection type. 

In the above case, it complains about the value being used for o.areaCodes not being a collection, when it ought to be a collection because o.areaCodes is defined as a collection on the entity.

Steps to Reproduce This fails a variety of test cases so there are various places where you can reproduce it.

One way: uncomment the line @ElementCollection(fetch = FetchType.EAGER) on test.jakarta.data.jpa.web.City and run the io.openliberty.data.internal_fat_jpa test bucket. The test testCollectionAttribute will start failing.

Another way: uncomment the lines of testRecordWithEmbeddables in test.jakarta.data.web.DataTestServlet and run the io.openliberty.data.internal_fat test bucket.

Another way: uncomment the lines of testEmbeddableCollection in test.jakarta.data.jpa.web.DataJPATestServlet and run the io.openliberty.data.internal_fat_jpa test bucket.

Expected behavior JPQL queries for an attribute that is a Set<Integer> should return a result list that contains Set<Integer>, not Integer. JPQL queries that construct a new class instance with an attribute that is a Set<Integer> as a parameter should receive a Set<Integer> and successfully construct an instance, not be rejected saying that the collection attribute is not a collection attribute.

Diagnostic information:

OpenLiberty Version: latest
Affected feature(s) persistence-3.1
Java Version:
java.home = /Users/njr/drivers/jdk-21.jdk/Contents/Home
java.version = 21
java.runtime = OpenJDK Runtime Environment (21+35-2513)
os = Mac OS X (14.5; aarch64) (en_US)

server.xml configuration (WITHOUT sensitive information like passwords) see test buckets

ajaypaul-ibm avatar Jul 02 '24 06:07 ajaypaul-ibm

I am looking into this issue

Riva-Tholoor-Philip avatar Aug 20 '24 16:08 Riva-Tholoor-Philip

Please attach there entities code.

rfelcman avatar Dec 03 '24 13:12 rfelcman

Hi @rfelcman Please find the entity below :

@Entity
@IdClass(CityId.class)
public class City {
    @ElementCollection(fetch = FetchType.EAGER)
    public Set<Integer> areaCodes;

    @Version
    long changeCount;

    @Id
    public String name;

    public int population;

    @Id
    public String stateName;

    public static City of(String name, String state, int population, Set<Integer> areaCodes) {
        City inst = new City();
        inst.name = name;
        inst.stateName = state;
        inst.population = population;
        inst.areaCodes = areaCodes;
        return inst;
    }

    @Override
    public String toString() {
        return "City of " + name + ", " + stateName + " pop " + population + " in " + areaCodes + " v" + changeCount;
    }
}

ajaypaul-ibm avatar Dec 04 '24 13:12 ajaypaul-ibm

Hi @rfelcman Please find the recreate details :

@Test
  public void testElementCollection() throws Exception {
      ECEntity e1 = new ECEntity();
      e1.setId("EC1");
      e1.setIntArray(new int[] { 14, 12, 1 });
      e1.setLongList(new ArrayList<>(List.of(14L, 12L, 1L)));
      e1.setLongListEC(new ArrayList<>(List.of(14L, 12L, 1L)));
      e1.setStringSet(Set.of("fourteen", "twelve", "one"));
      e1.setStringSetEC(Set.of("fourteen", "twelve", "one"));
     
      ECEntity e2 = new ECEntity();
      e2.setId("EC2");
      e2.setIntArray(new int[] { 14, 12, 2 });
      e2.setLongList(new ArrayList<>(List.of(14L, 12L, 2L)));
      e2.setLongListEC(new ArrayList<>(List.of(14L, 12L, 2L)));
      e2.setStringSet(Set.of("fourteen", "twelve", "two"));
      e2.setStringSetEC(Set.of("fourteen", "twelve", "two"));


      tx.begin();
      em.persist(e1);
      em.persist(e2);
      tx.commit();
       // Test JPQL queries
  String jpql;
  List<?> results;
  // Query for intArray
  tx.begin();
  try {
      jpql = "SELECT intArray FROM ECEntity WHERE id=?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC1")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }

  // Query for longList
  tx.begin();
  try {
      jpql = "SELECT longList FROM ECEntity WHERE id=?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC1")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  // Query for stringSet
  tx.begin();
  try {
      jpql = "SELECT stringSet FROM ECEntity WHERE id=?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC1")
                  .getResultList();
      logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  tx.begin();
  try {
      jpql = "SELECT longListEC FROM ECEntity WHERE id=?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC1")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  // Query for longListEC
  tx.begin();
  try {
      jpql = "SELECT longListEC FROM ECEntity WHERE id LIKE ?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC%")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  tx.begin();
  try {
      jpql = "SELECT longList FROM ECEntity WHERE id LIKE ?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC%")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  // Query for stringSetEC
  tx.begin();
  try {
      jpql = "SELECT stringSetEC FROM ECEntity WHERE id LIKE ?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC%")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }
  tx.begin();
  try {
      jpql = "SELECT stringSet FROM ECEntity WHERE id LIKE ?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC%")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }

  tx.begin();
  try {
      jpql = "SELECT stringSetEC FROM ECEntity WHERE id=?1";
      results = em.createQuery(jpql)
                  .setParameter(1, "EC1")
                  .getResultList();
                  logQueryResults(jpql,results);
      tx.commit();
  } catch (Exception e) {
      tx.rollback();
      throw e;
  }

  }
  public void logQueryResults(String jpql, Collection<?> results) {
      System.out.println();
      System.out.println(jpql);
      System.out.println("getResultList returned a " + results.getClass().getTypeName());
      if (!results.isEmpty()) {
          System.out.println("    elements are of type " + results.iterator().next().getClass().getTypeName());
      } else {
          System.out.println("    elements are of type <empty>");
      }
      StringBuilder s = new StringBuilder();
          boolean first = true;
          for (Object element : results) {
              if (first)
                  first = false;
              else
                  s.append(", ");
              if (element instanceof int[])
                  s.append(Arrays.toString((int[]) element));
              else
                  s.append(element);
          }
          System.out.println("            contents are [" + s.toString() + "]");
  }

ECEntity.java is like below :

package io.openliberty.jpa.data.tests.models;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;

/**
 * Entity with and without ElementCollection attributes.
 */
@Entity
public class ECEntity {

    @Id
    String id;

    int[] intArray = new int[] {};

    ArrayList<Long> longList = new ArrayList<>();

    @ElementCollection(fetch = FetchType.EAGER)
    ArrayList<Long> longListEC = new ArrayList<>();

    Set<String> stringSet = new HashSet<>();

    @ElementCollection(fetch = FetchType.EAGER)
    Set<String> stringSetEC = new HashSet<>();

    public String getId() {
        return id;
    }

    public int[] getIntArray() {
        return intArray;
    }

    public ArrayList<Long> getLongList() {
        return longList;
    }

    public ArrayList<Long> getLongListEC() {
        return longListEC;
    }

    public Set<String> getStringSet() {
        return stringSet;
    }

    public Set<String> getStringSetEC() {
        return stringSetEC;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setIntArray(int[] intArray) {
        this.intArray = intArray;
    }

    public void setLongList(ArrayList<Long> longList) {
        this.longList = longList;
    }

    public void setLongListEC(ArrayList<Long> longListEC) {
        this.longListEC = longListEC;
    }

    public void setStringSet(Set<String> stringSet) {
        this.stringSet = stringSet;
    }

    public void setStringSetEC(Set<String> stringSetEC) {
        this.stringSetEC = stringSetEC;
    }
}

This demonstrates that EclipseLink is allowing ElementCollection attributes to be returned by JPQL queries, but with results that are both inconsistent with non-ElementCollection attributes of the same type, and are also incorrect. Especially when returning multiple results which get erroneously combined together into a single collection!

Here is the output of this test. Entity attribute names ending in "EC" use ElementCollection. The others have the same types but don't use ElementCollection:

SELECT intArray FROM ECEntity WHERE id=?1
getResultList returned a java.util.Vector
    elements are of type int[]
            contents are [[14, 12, 1]]

SELECT longList FROM ECEntity WHERE id=?1
getResultList returned a java.util.Vector
    elements are of type java.util.ArrayList
            contents are [[14, 12, 1]]

SELECT stringSet FROM ECEntity WHERE id=?1
getResultList returned a java.util.Vector
    elements are of type java.util.ImmutableCollections$SetN
            contents are [[fourteen, twelve, one]]

SELECT longListEC FROM ECEntity WHERE id=?1
getResultList returned a java.util.Vector
    elements are of type java.lang.Long
            contents are [14, 12, 1]

SELECT longListEC FROM ECEntity WHERE id LIKE ?1
getResultList returned a java.util.Vector
    elements are of type java.lang.Long
            contents are [14, 12, 1, 14, 12, 2]

SELECT longList FROM ECEntity WHERE id LIKE ?1
getResultList returned a java.util.Vector
    elements are of type java.util.ArrayList
            contents are [[14, 12, 1], [14, 12, 2]]

SELECT stringSetEC FROM ECEntity WHERE id LIKE ?1
getResultList returned a java.util.Vector
    elements are of type java.lang.String
            contents are [fourteen, twelve, one, fourteen, twelve, two]

SELECT stringSet FROM ECEntity WHERE id LIKE ?1
getResultList returned a java.util.Vector
    elements are of type java.util.ImmutableCollections$SetN
            contents are [[fourteen, twelve, one], [fourteen, twelve, two]]

SELECT stringSetEC FROM ECEntity WHERE id=?1
getResultList returned a java.util.Vector
    elements are of type java.lang.String
            contents are [fourteen, twelve, one]

<<< END: testElementCollection

If EclipseLink wants to reject this outright because the Jakarta Persistence specification does not require element collections to be selected by JPQL queries, that would be perfectly fine. But if EclipseLink doesn't reject it, it at least needs to get the behavior correct and return valid results. PR for reference : https://github.com/OpenLiberty/open-liberty/pull/30578

ajaypaul-ibm avatar Jan 22 '25 07:01 ajaypaul-ibm

Hi @rfelcman Please find the DDL statements below as you mentioned :

  • CREATE TABLE ECENTITY (ID VARCHAR(255) NOT NULL, INTARRAY BLOB(2147483647), LONGLIST BLOB(2147483647), STRINGSET BLOB(2147483647), PRIMARY KEY (ID))
  • CREATE TABLE ECEntity_LONGLISTEC (ECEntity_ID VARCHAR(255) /* */ , LONGLISTEC BIGINT)
  • CREATE TABLE ECEntity_STRINGSETEC (ECEntity_ID VARCHAR(255) /* */ , STRINGSETEC VARCHAR(255))
  • ALTER TABLE ECEntity_LONGLISTEC ADD CONSTRAINT CnttyLNGLSTCCnttyD FOREIGN KEY (ECEntity_ID) REFERENCES ECENTITY (ID)
  • ALTER TABLE ECEntity_STRINGSETEC ADD CONSTRAINT CnttySTRNGSTCnttyD FOREIGN KEY (ECEntity_ID) REFERENCES ECENTITY (ID)

ajaypaul-ibm avatar Feb 06 '25 15:02 ajaypaul-ibm

I don't think that test case is correct. There are compared:

  • BLOBs int[] intArray - INTARRAY BLOB(2147483647) where Java array is (de)serialized into DB and is part of the Entity
  • Collections where each item is mapped to cell in the table but mapped by @ElementCollection. @ElementCollection marks a collection. This does not necessarily mean that this collection references a 1-n join. This is not same thing like @OneToMany. JPQL Query like SELECT longListEC FROM ECEntity WHERE id LIKE ?1 is transformed into SQL SELECT t1.STRINGSETEC FROM ECENTITY t0 LEFT OUTER JOIN ECEntity_STRINGSETEC t1 ON (t1.ECEntity_ID = t0.ID) WHERE t0.ID LIKE ? and EclipseLink doesn't have info about Ids. Don't forget, that You will get collection of simple types (Strings, etc.) or a collection of embeddable elements but not a collection of entities.

rfelcman avatar Mar 12 '25 10:03 rfelcman

Hi @rfelcman We already know that the test case is doing something that is not permitted by EclipseLink/JPA in querying for an ElementCollection value in JPQL. That isn't the point. The point is that EclipseLink didn't reject it and instead ran a query and returned wrong data to the application.

ajaypaul-ibm avatar Mar 12 '25 17:03 ajaypaul-ibm

TCK https://github.com/jakartaee/platform-tck/tree/main/tcks/apis/persistence/persistence-outside-container

rfelcman avatar May 27 '25 13:05 rfelcman