Unwinding and merging the results with dynamic labels in Neo4J Client for C# - neo4j

I am currently trying to unwind a list of objects that I want to merge to the database using the Neo4J Client. What I would like to do is unwind the list and create the nodes with a label generated based on a property from the items themselves instead of hardcoding a label name. From what I can find I have to use the APOC merge method to do so. However, I am unable to translate this to the Neo4J client. In the neo4J explanation they yield a node after the apoc.merge.node call and then return the node. However, I cannot simply return the node nor can I set the node (I got to the point of just messing about, and at one point I got the labels to work but it overwrote all properties with the last item in the list).
I seem to miss something fundamental but i'm not quite sure what. Does anyone here know how to do this with neo4J client (and if possible, give a bit of an explanation what is going on)? I am very new to the development world and I feel I am just missing a crucial piece of understanding when it comes to this..
The code that I tried that turned all properties into the last node's properties but at least created the labels as I expected:
public async void CreateBatchItems(List<TToDataBase> itemList)
{
await Client.Cypher
.Unwind(itemList, "row")
.Merge("(n)")
.With("row, n")
.Call("apoc.merge.node([n.Name], n)").Yield("node")
.Set("n += node")
.ExecuteWithoutResultsAsync();
}
Thank you in advance!
Edit:
Some clarification about the input:
The objects are actually very basic, as (at least for now), they merely contain a name and an objectID (and these object ID's are later used to create relations). So its a very basic class with two properties:
public class Neo4JBaseClass
{
public Neo4JBaseClass() { }
public Neo4JBaseClass(string name, string objectId)
{
Name = name;
ObjectId = objectId;
}
[JsonProperty(PropertyName = "ObjectId")]
public string ObjectId { get; set; }
[JsonProperty(PropertyName = "Name")]
public string Name { get; set; }
}
I have also tried a slight variation where this class also has the added property
[JsonProperty(PropertyName = "PropertyMap")]
public IProperty PropertyMap { get; set; }
where PropertyMap is another basic object holding the name and objectId. This seemed like a good idea for future proofing anyway, so the propertylist can be easily expanded without having to change the base object.

[EDITED]
The main issue is that Merge("(n)") matches any arbitrary node that already exists.
You have not shown the data structure for each element of itemList, so this answer will assume it looks like this:
{Name: 'SomeLabel', id: 123, Props: {foo: 'xyz', bar: true}}
With above data structure, this should work:
public async void CreateBatchItems(List<TToDataBase> itemList)
{
await Client.Cypher
.Unwind(itemList, "row")
.Call("apoc.merge.node([row.ObjectId], row.id)").Yield("node")
.Set("node += row.Props")
.ExecuteWithoutResultsAsync();
}
[UPDATE]
The data structure you added to your question is very different than what I had imagined. Since neither of the properties in a row is a map, .Set("node += row.Props") would generate an error.
Using your data structure for each row, this might work:
public async void CreateBatchItems(List<TToDataBase> itemList)
{
await Client.Cypher
.Unwind(itemList, "row")
.Merge("(n:Foo {id: row.ObjectId})")
.Set("n += row.Name")
.ExecuteWithoutResultsAsync();
}
This code assigns the node label Foo to all the generated nodes. A node should always have a label, which improves clarity and also tends to improve efficiency -- especially if you also create indexes. For example, an index on :Foo(id) would make the above query more efficient.
This code also assumes that the id property is supposed to contain a unique Foo node identifier.

Related

I want to add a Relationship between two already existing nodes in Neo4j - how to do that using Spring Data Neo4jRepository?

The cypher query to perform the above task would be as follows:
MATCH (a:Buyer), (b:Seller)
MERGE (a) -[:BUY {quantity: 150}]-> (b);
I want the equivalent Neo4jRepository function or some equivalent code that can serve the same above purpose. Please post the answer if you know some solution.
[Updates]
I have posted an answer below. But I am also expecting some other kind of solutions to this purpose. Please feel free to post alternative solutions as answers.
One Possible Way
If we have two Nodes/Entities as Buyer and Stock as POJO classes in SpringBoot Code, and if we are trying to add a relationship called [:HAS] between two such nodes then we can do the following.
#Node("Stock")
class Stock
{
#Id #GeneratedValue(...)
private Long id;
/* Other attributes here --- */
}
#Node("Buyer")
class Buyer
{
#Id #GeneratedValue(...)
private Long id;
/* Other Attribute Variables Here --- */
#Relationship(type="HAS")
private List<Stock> stockList;
public List<Stock> getStockList()
{
return stockList;
}
public void setStockList(List stockList)
{
this.stockList = stockList;
}
}
So we can do this to create the desired relationship.
buyer.getStockList().add(newStock);
buyerRepo.save(buyer);
Here buyerRepo is the object of a Repository that implements Neo4jRepository<Buyer, Long>

How to make node changes in a TransactionEventHandler that are returned within the same CREATE query

I am trying to implement a plugin for neo4j to add an autoincrement ID using GraphAware library. To this end, I've written the following classes:
public class ModuleBootstrapper implements RuntimeModuleBootstrapper
{
public RuntimeModule bootstrapModule(String moduleId, Map<String, String> config, GraphDatabaseService database)
{
return new MyModule(moduleId, config, database);
}
}
And:
public class MyModule extends BaseTxDrivenModule<Void>
{
int counter = 0;
public Void beforeCommit(ImprovedTransactionData transactionData)
throws DeliberateTransactionRollbackException
{
if (transactionData.mutationsOccurred()) {
for (Node newNode : transactionData.getAllCreatedNodes()) {
newNode.setProperty("id", counter++);
}
}
}
}
And for the testing I can execute:
CREATE (n);
And then:
MATCH (n) RETURN n;
And I can see the effect of my plugin as some id property added to the node. But when I run:
CREATE (n) RETURN n;
The returned node does not have the mentioned id property but again, when I match the node in a separate query, I see the things have worked out just fine. It's just that in the CREATE query, the returned node infos are the ones before my plugin modified them.
The questions are; why is that? Didn't I modify the nodes through my plugin within the transaction? Shouldn't the returned nodes be showing the modifications I've made on them? Is there any way I can make this happen?
While you're still within the transaction, the Cypher result is already computed and there is no clean way to add additional informations to it.
I guess a feature request on the neo4j repository could be cool but in total honesty this would require a serious change into the neo4j core codebase.
BTW, the incremental ID is already implemented in the graphaware-uuid plugin : https://github.com/graphaware/neo4j-uuid#specifying-the-generator-through-configuration

AutoFixture constrained string parameter

Is there a simple way to specify a list of possible values for the parameter orderBy? Not one by one please, otherwise I would not be making the question. I want to specify that orderby makes sense only if it is chosen from a predetermined list. Suppose the list is very large...still not random. This cannot be that hard...no single example of such a simple task.
[Test, AutoData]
public override void IndexReturnsView(int? pageIndex, int? pageSize, string orderBy, bool? desc)
{
.....
}
EDIT:
All I want is to read the possible values from a list as I would do with the ValueSource attribute. However, it seems not to work with AutoFixture. If I specified e.g. [ValueSource("GetOrderByColumnNames")] my test does not work anymore. I have no idea of what I am doing wrong. Unfortunately AutoFixture lacks useful documentation and the examples are very basic. Is there a working example of this scenario that I can use to guide myself here?
This has to be a very common situation, however I have been looking for days with no luck :(.
Appreciated!
If I understand the question correctly, the problem is that the orderBy value should be randomly selected from a list of predefined values, but that list might be too large to use with [InlineAutoData].
The easiest way to do this that I can think of is to introduce a helper type. This might actually be a valuable addition to the application code itself, as it makes the role of various values more explicit, but if not, you can always add the wrapper type to the test code base.
Something like this is the minimum you'll need:
public class OrderCriterion
{
public OrderCriterion(string value)
{
Value = value;
}
public string Value { get; }
}
If we also imagine that this class exposes a list of ValidValues, you can implement an AutoFixture Customization using the ElementsBuilder class:
public class OrderCriterionCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(
new ElementsBuilder<OrderCriterion>(OrderCriterion.ValidValues));
}
}
Then you create a data source attribute for your test code base:
public class TestConventionsAttribute : AutoDataAttribute
{
public TestConventionsAttribute() : base(
() => new Fixture().Customize(new OrderCriterionCustomization()))
{
}
}
This enables you to write a test like this, which passes:
[Theory, TestConventions]
public void IndexReturnsView(
int? pageIndex,
int? pageSize,
OrderCriterion orderBy,
bool? desc)
{
Assert.Contains(orderBy.Value, OrderCriterion.ValidValues.Select(x => x.Value));
}
Notice that instead of declaring the orderBy parameter as a string, you declare it as an OrderCriterion, which means that AutoFixture will be detect its presence, and the Customization then kicks in.
See also https://stackoverflow.com/a/48903199/126014

Dynamic Cypher Search Query

I know this may sound nonsense but I will try my best to explain the problem that I am facing with neo4jClient.
I developing a Social TaskManagment Software. and for the server side code and data Storage I choose to have neo4j (neo4jClient) and C# in place.
in Our software Imagine a User : (mhs) Post a Task of "#somebody please help me on #Neo4j #cypher" the task would be decorated with some fancy character including (# # + /) the obove post will be save in neo4j as graph like this :
(post:Post {Text : #somebody please help me on #Neo4j})-[Has_HashTag]-(neo4j:HashTag)
(post:Post)-[Has_HashTag]-(Cypher: HashTag)
(post:Post)-[Has_Author)-[mhs:User]
(post:Post)-[Has_MentionedUser)-[Somebody:User]
Now Imagine User #mhs is trying to search the Post that Have HashTag of #Neo4j and mentioned to #somebody
Here I am building (hand Craft) a Cypher Query Including the 2 seach Paramerts in Cypher with Some Fancy C# code which result the blow Cypher Query:
MATCH (nodes)-[r]-(post:Post),
(post:Post)-[:HAS_MentionedUsers]->(assignee1307989068:User),
(nodes)-[r]-(post:Post)-[:HAS_HashTags]->(Hashtag1482870844:HashTag)
WHERE (assignee1307989068.UserName = "somebody") AND (Hashtag1482870844.Value = "neo4j")
RETURN post AS Post, Collect(nodes) as nodes
ORDER BY post.creationDate
the above cypher will return a post with just all the nodes of the post that is not included in Where clause.
my question is how to include all the Nodes related to the Targeted (post) without including them in Return part of the cypher. Something like return (*).
The Other problem is How can I deserialize the result set into C# without knowing what shape it may have.
The Search method that I am producing the mentioned Cypher is as fallow:
public List<PostNode> Search(string searchterm)
{
List<string> where = new List<string>();
var tokenizedstring = searchterm.Split(' ');
var querystring = new StringBuilder();
var relatedNodes = new List<string>();
var q = new CypherFluentQuery(_graphClient) as ICypherFluentQuery;
foreach (var t in tokenizedstring)
{
_commandService.BuildPostQueystring(t, ref querystring, ref where, ref relatedNodes);
}
if (querystring[querystring.Length - 1] == ',')
querystring = querystring.Remove(querystring.Length - 1, 1);
q = q.Match(querystring.ToString());
int i = 1;
if (where.Count > 0)
q = q.Where(where[0]);
while (i < where.Count)
{
q = q.AndWhere(where[i]);
i++;
}
var rq = q.Return(
(post, nodes) => new PostNode
{
Post = post.As<Node<string>>(),
Nodes = nodes.CollectAs<string>()
})
.OrderBy("post.creationDate");
var results = rq.Results.ToList();
//foreach (var result in results)
//{
// //dynamic p = JsonConvert.DeserializeObject<dynamic>(result.Post.Data);
// //dynamic d = JsonConvert.DeserializeObject<dynamic>(result.Nodes.Data);
//}
return results;
}
}
//Some Helper Class just to cast the result.
public class PostNode
{
public Node<string> Post { get; set; }
public IEnumerable<Node<string>> Nodes { get; set; }
}
But as you may noticed it does not have the nodes that is included in the search term via Where Clause in Cypher Query.
I am really stopped here for a while, as I can not provide any decent solution for this. so your help may save me a lot.
may be i am totally in a wrong direction so please help in any way you can think of.
It appears that a list of UNKNOWN related nodes are being provisioned, so one of this:
Scenario A
It doesn't matter what EXACTLY those related nodes are, I just want them.
Question: What is intention of retrieving those unknown but related nodes? By answering this chances are this query could be improved for good.
Scenario B
These unknown related nodes are actually known! It's just they are not fully known at time of query execution however down the road somewhere in C# code we will have things like this
if (nodes.Any(_ => _ is HashTag) {...}
Question:
This type of behaviour requires to KNOW the type. Even with reflection or C# dynamic stuff that requirement is still there because Neo4jClient has no way of correlating a bag of JSON data coming from Neo4j into any local type. When Neo4jClient receives bulk of data over wire somehow it should know what type would YOU prefer to represent. This is why queries are always like this:
Return((a, p) => new
{
Author = a.As<Author>(), //we expect node content to be represented as Author
Post = p.As<Post>()
})
Neo4jClient does NOT preserve C# types inside your Neo4j database. It would have been nasty to do so. However, idea behind it is that you shouldn't find yourself desperate for it and if you do so then I would recommend looking for problem somewhere else i.e. why would you rely on your client library to describe your domain for you?

Employee not found with id null grails

Can someone help me with my situation. I have the following code.
class Employee {
Integer empId
String firstName
String lastName
static constraints = {
empId()
firstName()
lastName()
}
static mapping = {
id generator:'assigned', name:'empId'
version false
}
}
The code allows me to save employee through 'create' but gives the following error message
"Employee not found with id null" and also in the list, all the employees are listed but clicking on any Emp Id gives the same error. Please help. This is driving me nuts.
Rocky
Thanks for your reply. Like I mentioned I am able to save the empId in the database but get that message nonetheless and see the list with assigned ids (empId). The link points to employee/show with no number at end. However employee/show/22(empId) works fine. employee/edit/22 works too but update does not work.
I am not using any assigned sequence. Just some random integer. Maybe a better example would be to use SSN instead of empId.
Thank you once again.
You are a great help buddy. Appreciate your time and patience. I am not writing any special update or save (I am too new to dig too deep). Just using grails to generate-all. However, I did find a workaround. I changed the domain class to add variable id (Long) and added empId setter method to allocate the empId value to id. That did it. Here is my code.
class Employee {
Long id
Long empId
String firstName
String lastName
static constraints = {
empId()
firstName()
lastName()
}
static mapping = {
id generator:'assigned', name:'empId', column: 'emp_id'
version false
}
public void setEmpId(Long empId){
this.empId = empId
this.id = empId
}
}
Please feel free to suggest if you have a better way of doing that.
Regards
Rocky
If you are using the "assigned" sequence, then you have to assign the objects ids yourself before saving them. Otherwise your objects will be saved with a null or 'default 0' id. If you want GORM to assign an id for you, you need to use another type of generator, like "sequence" generator. It would be like:
id name: 'customId', generator: 'sequence', params: [sequence:'some_sequence']
More info on id generators here.

Resources