DEV Community

loading...

SDN/RX Cypher tips

Haris Secic
software developer doing some architecture
・4 min read

I spent some time working on a project using Neo4j and Spring with Kotlin. I decided to go with WebFlux as it's reactive/all that non-blocking stuff. Now as SDN/RX is fairly new (out of beta since April 2020 I think) there has been some confusion, given that I used OGM for previous stuff I unintentionally tried to combine two approaches and it didn't work as expected. Trying to adjust to new framework took a bit and I realised some features were not only changed but missing completely.

Embedded objects (like in OGM) problem

Embedded objects at the time of writting are not yet supported as in OGM. This means that nesting object in another one inside of the code will produce relationship rather than the embedded object. For status update follow this GitHub issue

One to one implicit relationship

As stated in previous point, if you have complex/custom types for properties in your class, those will be resolved as Node to Node relatioship.
Having embedded objects represented as relationships can make custom queries a bit hard if your going with annotation approach. So let's say you have

data class LanguageStuff(
    @Id @GeneratedValue val id: Long,
    val en: String,
    val ba: String
)
data class Stuff(
   @Id @GeneratedValue val id: Long,
   val serial: String,
   //This one will be automatically generated as related node
   val description: LanguageStuff,
   //and this one
   val name: LanguageStuff
)

Even though there's no @Relationship attribute the description property will be generated as separate Node in Neo4j and therefor, will be fetched automatically each time we use common methods of querying data like letting spring resolve method name and try to generate query or generating Cypher with tools provided by SDN/RX as Cypher DSL which you can read more about here.

Custom queries using annotations

This all works well when you only need default behaviour. If you need annotated method with query including custom Cypher it gets a bit tricky to spot some things.

I did put name property intentionally using the same type as description. This is due to missing detailed note about caching and how objects are resolved by SDN/RX. Given repository:

interface Stuffpository : ReactiveNeo4jRepository<Stuff,Long> {
    @Query("MATCH (s:Stuff) RETURN s")
    fun customCypherStuff()
}

You might forget about the fact that Kotlin needs ? if things can be nullable. And there you go with the exception of could not be mapped. If you need those values you would want to fetch name and description represented with LanguageStuff data class. If your lazy enough like me you would try something like @Query("MATCH p=(s:Stuff)-[*]->(:LanguageStuff) RETURN p") and of course this doesn't work because it would be too much magic to resolve paths, but it's fun to break stuff and boring to read the docs.

Cypher "spread" operator and join properties

But if you think a bit harder you might end up with something like:

MATCH (s:Stuff)
MATCH (s)-[*]->(desc:LanguageStuff)
MATCH (s)-[*]->(name:LanguageStuff)
RETURN s{.*,description:desc,name:name}

inside your @Query value. If you're reading this by accident or you are new to this and don't know much about Cypher, might be a good note to point out that s{.*} is something like spread operator .* where each property/attribute from given s will be put to new object {}. As s only contains id and serial as real properties and name and description are related nodes, only first two will be spread from main node. Second two are explicitly set. Again this doesn't get the result

SDN/RX "hidden" naming

Graphs are not tables. There's no "real" one to one relationship so mapping nodes to data class fails. Then we can use arrays which solves problem up to some point

MATCH (s:Stuff)
MATCH (s)-[*]->(desc:LanguageStuff)
MATCH (s)-[*]->(name:LanguageStuff)
RETURN s{.*,Stuff_DESCRIPTION_LanguageStuff:[desc],Stuff_NAME_LanguageStuff:[name]}

Why the ugly names? SDN/RX by default maps relationships in naming format of ClassName_CAPITAL LETTERS OF PROPPERTY NAME IF RELATIONSHIP NAME IS NOT SET_PropertyClassName. So SDN/RX actually maps the relationships and generates special names for them for embedded objects (for now!). And therefor, queries need to fetch array named with relationship type where SDN/RX kicks in and resolves where to map what BUT we face yet another problem!

SDN/RX auto generated ID problem

You get things mapped but you will notice that both name and the description have the same value.
My best guess is that SDN/RX does some caching by type and both properties are the same type, but also, both properties have ID generated in default manner as a Long. This means that ID is not stored as separate/special property in the Neo4j Node but you need to actually resolve it by using id(node). This may be done in the background for automated way of generating queries but when you write a custom one you need to remember to know how does the mapper work since you haven't put in any details like relationship name or such.
I'm unaware am I right about "caching" where it could be that it's using type aware Map but it does ignore rest of the objects of the same type. The main problem is that Id is not being fetched so we end up with:

MATCH (s:Stuff)
MATCH (s)-[*]->(desc:LanguageStuff)
MATCH (s)-[*]->(name:LanguageStuff)
RETURN s{.*,Stuff_DESCRIPTION_LanguageStuff:[desc{.en,.ba,__internalNeo4jId__: id(desc)}],Stuff_NAME_LanguageStuff:[name{.en,.ba,__internalNeo4jId__:id(name)}]}

__internalNeo4jId__ is name to use for IDs generated and used by Neo4j to tell SDN/RX to actually map it. This way SDN/RX finally maps correct values to correct places.

As said, I'm unaware how does wrongful mapping of different objects of the same type happens but it does for SDN/RX 1.0.1 version when using generated IDs by default method. I guess using custom generator will prevent this as ID would be stored as separate attributed and then mapped back again automatically when doing object graph mapping.

That's it for now

Discussion (0)