4

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);
CNDyson
  • 1,687
  • 7
  • 28
  • 63
  • You tagged this with spring-data-neo4j; So don't you already have a spring annotated pojo that represents this? And then just return the path from the config node with no parent to the config node with no child. Unless you need to collapse the values in the cypher, in which case why does your neo4j data not reflect the actual objects you are using? – Tezra Sep 20 '18 at 14:32
  • No, I don't. I can't come up with the right pojo. That's why I'm asking questions. – CNDyson Sep 21 '18 at 18:57
  • I haven't used spring-data-neo4j specifically, but the point of the spring-data series is to be able to easily access/save your data without needing to know what the database looks like. You should probably read more into spring-data-neo4j, and create your POJO's first. If you are not the owner of the data in Neo4j, I'm not sure how you would be able to use spring-data-neo4j on it. If spring-data-neo4j isn't important to the answer, and you will accept the Cypher returning the merged map instead of a POJO, than I can help. (or the list of settings in order if you need to preserve that info) – Tezra Sep 21 '18 at 19:05
  • Please see my update & bounty..., – CNDyson Sep 21 '18 at 19:07
  • Can you share your `find()` code? The annotated POJO looks correct. Also, I expect a failed find to return null, do you have an empty Config node in your database? – Tezra Sep 21 '18 at 19:17
  • No, I'm sorry, the id is returned, but `parentConfig` and `properties` are null. `findAll` finds all of the config nodes, but they're returned as `LinkedHashMap` objects, not `Config` objects. – CNDyson Sep 21 '18 at 19:25
  • I've updated to show my finder method. – CNDyson Sep 21 '18 at 19:32
  • Since you don't use the param in the Cypher, `findConfigAny` is effectively `findConfigAll`. Change the query to `"MATCH (c1:Config) WHERE c1.name={configName} RETURN c1;"`, and change the return type to just Config or Iterable (I don't think list is supported). Let me know how that works out for you. – Tezra Sep 21 '18 at 19:44
  • Using your query didn't change anything. I still get `id`, but `parentConfig` and `properties` are null. – CNDyson Sep 21 '18 at 19:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/180550/discussion-between-tezra-and-user1660256). – Tezra Sep 21 '18 at 20:06

1 Answers1

2

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.

meistermeier
  • 7,942
  • 2
  • 36
  • 45
  • Thank you! This works as expected using the DTO. How do I persist though? Using `Config` or `ConfigDto`? – CNDyson Sep 24 '18 at 16:07
  • I'm trying to write a test for insert. I extended `Neo4jRepository`, but I don't see an insert method. :-( – CNDyson Sep 24 '18 at 16:45
  • Ignore my prev comment - Can you show me what insert should look like? Or should I start another question? – CNDyson Sep 24 '18 at 16:58
  • Using a "properties" prefix, I issue this query but the node I just created isn't returned. `MATCH (c1:Config) WHERE c1.properties.name = {configName} RETURN c1`. And the query doesn't work at all in the CQL web browser. It doesn't like the "." prefix. I'm so confused.... Would love to start a chat with you! I'm at -4 GMT. – CNDyson Sep 24 '18 at 18:01