Best Practices for Executing multiple neo4j cypher queries in Java in one transaction - using Async API

I have written test code to connect to neo4j server from java app and execute two cypher queries in ONE Transaction.I did not find detailed example for executing two queries and retrieving result in neo4j Documentation

Though the code works, my question is:

  • Are these the best practices to use neo4j(AsyncSession, Transaction) and java(CompletionStage/ CompletableFuture) interface?
  • Are the queries executed Asynchronously if I use CompletableFuture.get() at the end?

Other details:

  • Neo4j version: v4.2.1
  • Neo4j Driver version: 4.1.1
  • Java jdk v8

Please find attached java test code.

Neo4jConnectorTest.java.txt (7.0 KB)

Disclaimer: I am not super familiar with these Java APIs

I noticed a few things.

The most important one, in my opinion, is that you could rewrite everything into a single Cypher query with UNWIND $product_ids AS id MERGE (p:Product {id: id }) RETURN p.id and retrieve each row, instead of composing several queries' result, that would probably simplify the code quite a bit. Note that the initial ON CREATE clause is not needed, because MERGE already CREATEs everything defined in the pattern if the pattern is not MATCHed in the graph (in that case, the id attribute is part of the pattern so it will be created if absent).

Then, a few technical details that I noticed:

Before calling java.util.concurrent.CompletableFuture#allOf, you should convert every CompletionStage to CompletableFuture with java.util.concurrent.CompletionStage#toCompletableFuture (it might be a no-op today, but that's not guaranteed in the future). Indeed, the toArray call is otherwise unsafe.

I also wonder if the code could not be simplified if you call join on the allOf result, surrounding the call with a try-catch:

                    try {
                        CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
                        // all queries have returned: now, extract results & commit tx
                    } catch (CancellationException | CompletionException e) {
                        return tx.rollbackAsync();
                    }

Thank you @florent.biville1 for the response,

Using join has simplified the code a lot and I have updated the implementation to convert CompletionStage to CompletableFuture now.

Also, our actual queries are quite different, I used those queries just to test (Apologies, if it was not clear earlier). Sharing few actual queries:

 MATCH (entity:Entity { id: 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91'})  WITH entity
	
	MERGE (alert:Alert {  id: 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91' ,taskId: 1161 ,alertType: 'IdentityStatusValidation' ,entityGlobalId: 'c5abf4a2-aecf-4bb4-a830-98a2af92cd91' })  
	ON CREATE SET 
	alert.id = 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91'  ,alert.taskId= 1161  ,alert.alertType= 'IdentityStatusValidation'  ,alert.entityGlobalId= 'c5abf4a2-aecf-4bb4-a830-98a2af92cd91' ,alert.raisedTime= 1610010155452  ,alert.summary= 'IdentityStatusValidation has failed, Please check task messages for more details.'  ,alert.messages=  '[{"message":"User ( [MMISHRA] ) could not be found. A valid and active User ID must be provided.","severity":"WARNING"}]'
	ON MATCH SET 
	alert.id = 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91'  ,alert.taskId= 1161  ,alert.alertType= 'IdentityStatusValidation'  ,alert.entityGlobalId= 'c5abf4a2-aecf-4bb4-a830-98a2af92cd91'	,alert.updateTime= 1610340489245  ,alert.summary= 'IdentityStatusValidation has failed, Please check task messages for more details.'  ,alert.messages=  '[{"message":"User ( [MMISHRA] ) could not be found. A valid and active User ID must be provided.","severity":"WARNING"}]'
	
	MERGE (entity)-[r:WITH_ALERT]->(alert)

I haved attached the updated code.Neo4jConnectorTest.java-updated.txt (6.0 KB)

Nice, it's good to know you could simplify the code :slight_smile:

My initial remark about MERGE and ON CREATE/ON MATCH still applies: you should not include attributes in these clauses that are already part of the MERGE pattern (i.e. remove id, taskId, alertType and entityGlobalId from the ON xxx SET clauses).

Moreover, if you need to set properties in both ON CREATE/ON MATCH cases, you can simply write MERGE... SET (without ON CREATE/ON MATCH), it will set the properties all the time (which could be useful for the messages and summary).

In short, your example query would become:

MATCH (entity:Entity { id: 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91'})	
MERGE (alert:Alert { 
	id: 'Default/CAMR-Finance25/Account/c5abf4a2-aecf-4bb4-a830-98a2af92cd91',
	taskId: 1161,
	alertType: 'IdentityStatusValidation',
	entityGlobalId: 'c5abf4a2-aecf-4bb4-a830-98a2af92cd91'
})  
	ON CREATE SET alert.raisedTime = 1610010155452
	ON MATCH SET  alert.updateTime = 1610340489245
	SET alert.summary= 'IdentityStatusValidation has failed, Please check task messages for more details.' , 
		alert.messages=  '[{"message":"User ( [MMISHRA] ) could not be found. A valid and active User ID must be provided.","severity":"WARNING"}]'
MERGE (entity)-[r:WITH_ALERT]->(alert)
1 Like