Skip to content

Support collections of entities as parameters to custom repository queries. #2292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
michael-simons opened this issue Jun 16, 2021 · 16 comments
Assignees
Labels
type: enhancement A general enhancement

Comments

@michael-simons
Copy link
Collaborator

We do support single entities being converted to a Neo4j map when used with custom queries as documented here: https://docs.spring.io/spring-data/neo4j/docs/6.1.1/reference/html/#custom-queries.parameters.

We don't support collections thereof: https://stackoverflow.com/questions/67992685/spring-data-neo4j-6-query-passing-list-param-throws-converternotfoundexception.

This should be fixed.

@michael-simons michael-simons added the type: enhancement A general enhancement label Jun 16, 2021
@michael-simons michael-simons added this to the 6.1.2 (2021.0.2) milestone Jun 16, 2021
@michael-simons michael-simons self-assigned this Jun 16, 2021
michael-simons added a commit that referenced this issue Jun 16, 2021
…ository queries.

This adds support for collections of entities as parameters.
As of know, we try to figure out the common element of that collection and see if we have fitting entity type. If so, all elements are converted into `Map<String, Object>` and put into a list.

Another approach would be trying to figure out the declared, resolvable type and than the generic type of a collection, but that would require much bigger changes in the infrastructure.

An additional benefit: Heterogenous collections are supported as well to some extend.

If this solution posses to be too slow, we can still investigate the generics approach.

This closes #2292.
@venkat125
Copy link

venkat125 commented Jul 10, 2021

I just trying out the SDN 6.1.2 for custom list params. where as, i'm getting error while accessing the properties of the collection as below:

[
    {
        "description": "some text",
        "language": {
            "name": "english"
        }
    },
    {
        "description": "some text",
        "language": {
            "name": "french"
        }
    }
]

the above array is the List relations.

@Query("UNWIND $relations As rel WITH rel " +
           "MATCH (f:PERSON {id: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.language.name}) " +
            "RETURN f, collect(r), collect(t)")
    UpdatedEntity updateRel(@Param("from") String from,
                            @Param("relations") List<KnowsEntity> relations);

Throws NPE:
2021-07-10 15:38:22.451 ERROR 9788 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

java.lang.NullPointerException: null
at *.service.ServiceImpl.test(ServiceImpl.java:98) ~[classes/:na]

may i know how to access the collection variables after UNWIND?
@michael-simons

@michael-simons
Copy link
Collaborator Author

Hi @venkat125
There are a couple of issues:

  • Your query does not access the properties we write
  • I assume KnowsEntity is annotated with @RelationshipProperties, right? If so, than we don't have a bug exactly on our side, but a missing feature: If such a thing is passed as top level parameter, we don't extract the target. I'm gonna fix this and add a test with what I think is your model.

@michael-simons
Copy link
Collaborator Author

@venkat125
Copy link

venkat125 commented Jul 14, 2021

Yep, i tried referring the test.
wherein, i observed following scenarios

  • using MATCH query

@ Query("UNWIND $relations As rel WITH rel " +
"MERGE (f:PERSON {id: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel. __ properties __ . __ target __ . __ id __}) " +
"RETURN f, collect(r), collect(t)")
Person testMethod(@param("from") String from,
@param("relations") List relations);

throws:

org.neo4j.driver.exceptions.ClientException: Cannot merge the following node because of null property value for 'name': (t:LANGUAGE {name: null})

  • and when tried mirroring the test with CREATE gives

org.springframework.dao.IncorrectResultSizeDataAccessException: Expected a result with a single record, but this result contains at least one more. Ensure your query returns only one record.

  • and with the MATCH gives null on root Node

@michael-simons
Copy link
Collaborator Author

You have to adapt the properties you refer to. In my test, the field annotated with @Id is the value of the language name. If you have it on a normal property (not annotated with @Id), you'll have to use that in your query.

The failure happens in the query itself, not SDN6.

@venkat125
Copy link

venkat125 commented Jul 14, 2021

My Request payload for @ Param("relations")

[
  {
    "language": {
      "name": "english"
    }
  },
  {
    "language": {
      "name": "french"
    }
  }
]

mapped to following entity class

@ Node(primaryLabel = "PERSON") @ Data
public class Person extends PersonProfile {}

@ Data
public class PersonProfile {

    @ Id @GeneratedValue(GeneratedValue.UUIDGenerator.class)
    private String nid;

    @ Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
    @ Property("knows")
    private List<KnowsEntity> knows = new ArrayList<>();

}
@ RelationshipProperties @ EqualsAndHashCode(callSuper = true) @ Data @ Builder @ NoArgsConstructor @ AllArgsConstructor
public class KnowsEntity {
    @ Id @ GeneratedValue()
    private Long id;

    @ TargetNode
    private Language language;
}
@ Node(primaryLabel = "LANGUAGE") @ Data
public class Language {
    @ Id
    private String name;

}

since language name is the entity's primary identifier as it was in your test. so used __ id __ as in the test
i checked with rel.__properties__.__target__.__name__ too
repo query:

@ Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.__properties__.__target__.__id__}) " +
            "RETURN f, collect(r), collect(t)")
    Person testR(@Param("from") String from,
                            @Param("relations") List<KnowsEntity> relations);

It just returns null on root Object.
Please let me know if i did something wrong in implementation.
Thanks @michael-simons

@michael-simons
Copy link
Collaborator Author

michael-simons commented Jul 14, 2021

Should be something along these lines

@Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.__properties__.__target__. __properties__.name}) " +
            "RETURN f, collect(r), collect(t)")
    Person testR(@Param("from") String from,
                            @Param("relations") List<KnowsEntity> relations);

you need to deference the properties of the target node. The query doesn't return anything as null will not match anything ever. With a merge, things should work out as expected.

@michael-simons
Copy link
Collaborator Author

__properties__ is the map entry that will always point to the properties of an entity, __id__ will always point to the @Id column and __target__, well to the @Target.

@venkat125
Copy link

venkat125 commented Jul 14, 2021

yep that makes sense. Thanks.
But, tried with adding same language name also in the relationship for testing and in entity class as well.

[
  {
    "read": true
    "test": "english",
    "language": {
      "name": "english"
    }
  },
  {
    "read": false
    "test": "french",
    "language": {
      "name": "french"
    }
  }
]

To be precise,
The below one is working returning the desired result with Person and the languages he/she knows with query the read prop in relationship.

@ Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS {read: rel.__properties__.read}] -> (t:LANGUAGE) " +
            "RETURN f, collect(r), collect(t)")

and below other things not working (i mean doesn't return anything)
This rel.__properties__.test has the same value as rel.__properties__.__target__.__properties__.name

@ Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.__properties__.test}) " +
            "RETURN f, collect(r), collect(t)")

Or

@Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.__properties__.__target__. __properties__.name}) " +
            "RETURN f, collect(r), collect(t)")

Or
(as in the test, i also have name field with annotated @ Id)

@Query("UNWIND $relations As rel WITH rel " +
            "MATCH (f:PERSON {nid: $from}) - [r:KNOWS] -> (t:LANGUAGE {name: rel.__properties__.__target__. __id__}) " +
            "RETURN f, collect(r), collect(t)")

Not sure y the things after rel.__properties__.__target__ not working. I just followed same as you had in test case.

@venkat125
Copy link

venkat125 commented Jul 15, 2021

getting property values rel.__properties__.read are working only within the relationship and not on destination node.
Be it (t:LANGUAGE {name: rel.__properties__.test}) or (t:LANGUAGE {name: rel.__properties__.__target__.__properties__.name}) not working. Could you see anything wrong in my implementation.
And also i try to clone and run this testcase.

void listOfRelationshipPropertiesShouldBeUnwindable(@Autowired PersonService personService) {

throws error:

[main] 2021-07-15 23:08:53,513 INFO framework.data.neo4j.test.Neo4jExtension: 111 - Using Neo4j test container.
[main] 2021-07-15 23:08:53,836 WARN ners.utility.TestcontainersConfiguration: 309 - Attempted to read Testcontainers configuration file at file:/C:/Users//.testcontainers.properties but the file was not found. Exception message: FileNotFoundException: C:\Users<user>.testcontainers.properties (The system cannot find the file specified)
[main] 2021-07-15 23:09:25,731 ERROR ockerclient.DockerClientProviderStrategy: 208 - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
[main] 2021-07-15 23:09:25,731 ERROR ockerclient.DockerClientProviderStrategy: 210 - NpipeSocketClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause NoSuchFileException (\.\pipe\docker_engine)
[main] 2021-07-15 23:09:25,731 ERROR ockerclient.DockerClientProviderStrategy: 212 - As no valid configuration was found, execution cannot continue

Test ignored.

java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration

at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:215)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:207)
at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:136)
at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:178)
at org.testcontainers.LazyDockerClient.getDockerClient(LazyDockerClient.java:14)
at org.testcontainers.LazyDockerClient.authConfig(LazyDockerClient.java:12)

It seems i'm missing some local setup to run the tests.
May i get some reference to try this test case in my local please? @michael-simons
Note: i'm trying this with SDN 6.1.2 (spring-boot 2.5.2)

@michael-simons
Copy link
Collaborator Author

You can run them against a local instance without Docker by exporting these environment variables: SDN_NEO4J_URL=bolt://localhost:7687 and SDN_NEO4J_PASSWORD=secret with appropriate values.

@venkat125
Copy link

venkat125 commented Jul 18, 2021

Thanks @michael-simons it works when i build the object locally like the one in testcase.
https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#create-domain-spring-boot-project

I think the issue i'm facing is due to the way the Entity object is being built when parsing the http payload
whereas, i was using the

  • lombok annotations @AllArgsConstructor @NoArgsConstructor

Using above annotations the constructor includes all the fields including @Id field where the result is empty (null)

  • when i tried omitting the @Id field in the constructor like the one in the model from the test case in the link,
    i'm getting the below error on parsing @RequestBody

2021-07-19 00:19:02.353 ERROR 7084 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class *.KnowsEntity]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of *.KnowsEntity (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (PushbackInputStream); line: 5, column: 13] (through reference chain: *.Person["knows"]->java.util.ArrayList[0])] with root cause

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of *.KnowsEntity (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (PushbackInputStream); line: 5, column: 13] (through reference chain: *.Person["knows"]->java.util.ArrayList[0])

@michael-simons can you please suggests me to get the result with the way payload as input?
Please let me know if i need to change something.

@meistermeier
Copy link
Collaborator

You could also use the Wither https://projectlombok.org/api/lombok/experimental/Wither.html for the id field.
I think this should make it flexible enough to be ok with a non-id (but all other fields) constructor.

@venkat125
Copy link

venkat125 commented Jul 22, 2021

@RelationshipProperties @AllArgsConstructor
public class KnowsEntity {
    @Id @GeneratedValue
    private @With Long id;

    @Property
    private final Boolean read;

    @TargetNode
    private final Language language;
}

Hope this model u was referring to.
still the same issue on constructing object

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class *.KnowsEntity]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of *.KnowsEntity (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (PushbackInputStream); line: 3, column: 9] (through reference chain: java.util.ArrayList[0])] with root cause

@meistermeier
Copy link
Collaborator

Disclaimer: I do not have a right environment to check this out right now for myself.
Yes but you would have to exclude id from the AllArgsConstructor. (try this first)
BUT I don't know what Jackson means by saying: no Creators, like default constructor, exist Maybe you would also need to provide an empty constructor. As a consequence this would render the final definitions useless.

@venkat125
Copy link

venkat125 commented Jul 28, 2021

Tried with the following possible cases:

1. @RequiredArgsConstructor @RelationshipProperties
public class KnowsEntity {
    @Id @GeneratedValue private Long id;
    @Property private final Boolean read;
    @TargetNode private final Language language;
}
2. @RequiredArgsConstructor @AllArgsConstructor
public class KnowsEntity {
    @Id @GeneratedValue private Long id;
    @Property private final Boolean read;
    @TargetNode private final Language language;
}
3. @AllArgsConstructor
public class KnowsEntity {
    @Id @GeneratedValue private Long id;
    @Property @Getter @Setter private Boolean read;
    @TargetNode @Getter @Setter private Language language;
    public KnowsEntity() {
    }
}

also tried with @With which was earlier used as @Wither and also tried without lombok annotations too with setters except the id field and still getting error like could not construct instance
can you please check once in your local with using the json payload?
I'm not sure whether it needs a fix? Or i'm doing something wrong. It would be helpful once anyone from the team try and suggest.
Thanks @michael-simons , @meistermeier

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants