spring-data icon indicating copy to clipboard operation
spring-data copied to clipboard

Wrong Wrapping of Lists in Queries Returning Arrays

Open jakobjoachim opened this issue 2 years ago • 4 comments

@Query("RETURN [1 , 2]")
List<Long> getOneTwo();

Throws an Exception:

org.springframework.data.mapping.MappingException: Can't convert result to type java.util.List!
...

I changed the return type a bit to find the actual issue and here it is:

@Query("RETURN [1 , 2]")
Object getOneTwo();

Throws no Exception and the actual result type is the correct ArrayList<Long>

@Query("RETURN [1 , 2]")
List<Object> getOneTwo();

Throws no Exception and the actual result type is ArrayList<ArrayList<Long>>

So it seems Spring Data tries to wrap the whole object in a list when the return type is a list. This Issue also occurs when using functions that return arrays like FLATTEN, UNION, CONCAT and so on

jakobjoachim avatar Jun 09 '22 15:06 jakobjoachim

The provided AQL query actually returns:

[
  [
    1,
    2
  ]
]

therefore it is mapped to ArrayList<ArrayList<Long>>. To return a flattened array that could be mapped to List<Long>, the query should be:

FOR i IN [1, 2] RETURN i

which returns:

[
  1,
  2
]

rashtao avatar Jun 10 '22 11:06 rashtao

Ohh my bad, I guess the confusion is that AQL always wraps a return in an array. Still weird that if Object is used as the return type the second list vanishes. That was what led me to this being a bug.

Thank you anyways :)

jakobjoachim avatar Jun 10 '22 12:06 jakobjoachim

I tested this some more because it was still bugging me. This is definitely a bug in arango spring data. See the following:

// AQL returns: [1]
// Java returns: 1
@Query("RETURN LENGTH([1])")
Long getsExtracted1();

// AQL returns: [[1]]
// Java returns: List<Long>
@Query("RETURN [1]")
Object getsExtracted2();
    
// AQL returns: [[1]]
// Java returns: List<List<Long>>
@Query("RETURN [1]")
List<Object> doesntGetExtracted();

// AQL returns: [[1]]
// Java throws: Method threw 'org.springframework.data.mapping.MappingException' exception: Can't convert result to type java.util.List!
@Query("RETURN [1]")
List<Long> throwsException1();

// AQL returns: [[1]]
// Java throws: Method threw 'org.springframework.data.mapping.MappingException' exception: Can't convert result to type java.util.List!
@Query("RETURN [1]")
List<List<Long>> throwsException2();

// AQL returns: [[1]]
// Java throws: Method threw 'org.springframework.data.mapping.MappingException' exception: Can't convert result to type java.util.List!
@Query("RETURN [1]")
List<List<List<Long>>> throwsException3();

// AQL returns: [1]
// Java returns: List<Long>
@Query("FOR i IN [1] RETURN i")
List<Long> doesntGetExtracted();

My findings:

  • AQL will always wrap results in an array
  • Spring Data is smart enough to remove that array in most cases (see first example)
  • When using generics (Long in this example) inside the List Spring Data has some issue
  • using the FOR i in [] RETURN i to remove the second array is enough so that spring data again can find the correct type
  • Spring Data should always remove one array when using RETURN and no array when using FOR x IN

jakobjoachim avatar Jun 10 '22 14:06 jakobjoachim

Thanks for clarifying, this seems indeed a bug.

rashtao avatar Jun 21 '22 10:06 rashtao

Investigating this further, I found that wrapped types returned by query methods are always recursively unwrapped by Spring Data in QueryExecutionConverters.unwrapWrapperTypes(), see https://github.com/spring-projects/spring-data-commons/blob/3.2.x/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java#L238-L264

This method unwraps the type until it finds a type assignable to the repository domain type. For example, this means that in case the return type of your query method is List<List<List<Long>>> then the deserialization target type will be Long.

The reason for this, is that returning nested arrays/lists from a query method is not a use case supported by Spring Data. You can find comments about it here: https://github.com/spring-projects/spring-data-commons/issues/2509#issuecomment-990706777

Return type inspection is subject to unwrap to the individual object type and so far we do not have the use-case of returning nested arrays/lists from a query method. Collection-like types are always assumed to represent a collection query.

To work around this, you can make your AQL queries return arrays of json objects (instead of arrays of arrays), create a class matching the structure of the returned data and set it as return type, for example:


    @Query("RETURN {\"result\": [1]}")
    List<FooResult> foo();

// ...


@Data
public class FooResult {
    private List<Long> result;
}

rashtao avatar Jan 29 '24 11:01 rashtao

Closing as not a bug. Feel free to reopen in case of further questions.

rashtao avatar Feb 06 '24 11:02 rashtao