Neo4j: Don't know how to map graph to Spring Data bean

Viewed 1230

I'm writing an app to capture configuration properties of other applications. Config is my primary domain object. There are hierarchical relationships between Config objects (one Config [:OVERRIDES] another Config). So, given a bottommost Config, I need to go all the way up the hierarchy to the topmost Config, and gather all the properties of Config objects along the way, merging them into one final Config. But I have no idea how to do this with Spring Data.

Here is the schema for 2 Config objects:

CREATE (c1:Config {name:'ConfigParent'});
SET c2.docker_tag='1.0.0';
SET c2.mem_limit='1gb';

MATCH (c1:Config {name: 'ConfigParent'}) 
CREATE (c2:Config {name:'ConfigChild'})-[:OVERRIDES]->(c1)
SET c2.docker_tag='1.0.1';

The ConfigChild properties need to be merged with ConfigParent, and any duplicate properties in ConfigParent will be overridden. So my final set of properties should be:

name='ConfigMerged'  //I don't know what this name would be?
docker_tag='1.0.1'
mem_limit='1gb'

There can be ANY key/value pair in a Config, so they can't be returned by name. I've come of with this CQL that I think does what I want:

MATCH p = SHORTESTPATH((c2:Config)<-[:OVERRIDES*]-(c1:Config)) 
WHERE c1.name = 'ConfigChild' and not (c2)-[:OVERRIDES]->() 
UNWIND NODES(p) as props return PROPERTIES(props);

The JSON response looks like so:

[
  {
    "keys": [
      "properties(props)"
    ],
    "length": 1,
    "_fields": [
      {
        "name": "ConfigParent",
        "docker_tag": "1.0.0",
        "mem_limit": "1gb"
      }
    ],
    "_fieldLookup": {
      "properties(props)": 0
    }
  },
  {
    "keys": [
      "properties(props)"
    ],
    "length": 1,
    "_fields": [
      {
        "name": "ConfigChild",
        "docker_tag": "1.0.1"
      }
    ],
    "_fieldLookup": {
      "properties(props)": 0
    }
  }
]

The problem is, I have no idea how to map this to POJOs. Do I need a Config domain object, or a Properties domain object? Is this the best CQL to achieve my goal?

UPDATE: I've come up with an annotated POJO for Config. But when I try to return it in code, properties are always empty, along with parentConfig.

@NodeEntity
public class Config {

    @Id 
    @GeneratedValue 
    private Long id;

    @Relationship(type = "OVERRIDES")
    private Config parentConfig;

    @Properties(allowCast=true)
    private Map<String, String> properties;

    ....
}

This is the basic query I'm testing, just to see if I can map to the POJO:

 @Query("MATCH (c1:Config) RETURN c1;")
 List<Config> findConfigAny(@Param("configName") String configName);
1 Answers

You are close to the solution but there is a technical limitation in Neo4j-OGM (the library used in Spring Data Neo4j for the object graph mapping): When using the @Properties annotation it will only persist and load prefixed (default attribute name) ones with the right delimiter (default .). So basically in your case it will only load properties that are prefixed with properties. and not all that get returned.

In Spring Data Neo4j there is also the possibility to provide a @QueryResult that can be seen as a DTO. You can mark a intermediate class with the annotation. Please make sure that this class is also part of your entity scan.

@QueryResult
public class ConfigDto {

  private String name;

  private Map<String, String> properties;

}

if you alter the return type of your SDN repository method also to this type

@Query("MATCH p = SHORTESTPATH((c2:Config)<-[:OVERRIDES*]-(c1:Config))"
 + " WHERE c1.name = 'Child' and not (c2)-[:OVERRIDES]->()"
 + " UNWIND NODES(p) as props return props.name as name, PROPERTIES(props) as properties")
List<ConfigDto> configs();

And than using this method, it will return:

ConfigDto{name='Child', properties={a.exclusive=1, name=parentConfig, a.override=parent value}}
ConfigDto{name='Child', properties={a.exclusiveChild=my value, name=Child, a.override=child value}}

Note that I wrote a test with persistence of an "original" Config object that has the properties mapped as you above and I just prefixed them with a.. You can see that also the name is in the property map returned from the query so the map holds all properties of a node.

Edit (Save part from the comments) With the solution above it is possible to load existing data from Neo4j.

It is not possible to persist arbitrary properties besides the @Properties solution but this will create "prefixed", "delimited" properties in you graph. E.g. with your code in the question you will get properties.docker_tag.

You could write your own properties converter for a Map, for example, and orient and take some ideas from https://github.com/neo4j/neo4j-ogm/blob/master/core/src/main/java/org/neo4j/ogm/typeconversion/MapCompositeConverter.java Here is the link to the documentation for converters and you should implement a CompositeAttributeConverter.

Neo4j-OGM is not meant to work with changing properties name all the time and it would also be complicated to decide what to save if a property with nameA exists in the domain class itself and is also defined in such a property map.

Related