I have a simple relationship (Account)-[IdentifiedBy]->(Identity), defined like this
#RelatedTo(type = "IDENTIFIED_BY", direction = Direction.OUTGOING)
private Set<Identity> identities = new HashSet<Identity>();
When I load the Account and access its identities, all the identities are loaded, but all their properties except for ID are null. However, if I annotate the property with #Fetch, then the identities are loaded correctly, with all properties. Is this by design or am I missing something?
#NodeEntity
public class Account {
#GraphId Long nodeId;
#RelatedTo(type = "IDENTIFIED_BY", direction = Direction.OUTGOING)
//#Fetch
private Set<Identity> identities = new HashSet<Identity>();
public Set<Identity> getIdentities() {
return identities;
}
public void setIdentities(Set<Identity> identities) {
this.identities = identities;
}
}
#NodeEntity
public class Identity {
#GraphId Long nodeId;
private String identifier;
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
}
public interface AccountRepository extends GraphRepository<Account> { }
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration({"/test-context.xml"})
#Transactional
public class AccountTests {
#Autowired
protected AccountRepository accountRepository;
#Test
public void accountMustLoadItsIdentities() {
Account acct = accountRepository.save(new Account());
Identity id = new Identity();
id.setIdentifier("merlin");
acct.getIdentities().add(id);
accountRepository.save(acct);
acct = accountRepository.findOne(acct.nodeId);
id = acct.getIdentities().iterator().next();
assertEquals("merlin", id.getIdentifier());
}
}
The unit test fails on the last assertion, but succeeds if #Fetch on Account is uncommented.
Instead of using
account.getIdentities()
you should do something like the following:
this.neo4jTemplate.fetch(account.getIdentities())
Not using the #Fetch keyword does not automatically enable lazy loading. To lazy load your properties use the Neo4jTemplate as plotted above.
This is by design
We try to avoid loading the whole graph into memory by not following relationships eagerly. A dedicated #Fetch annotation controls instead if related entities are loaded or not. Whenever an entity is not fully loaded, then only its id is stored. Those entities or collections of entities can then later be loaded explicitly using the template.fetch() operation.
http://static.springsource.org/spring-data/data-graph/snapshot-site/reference/html/#reference:simple-mapping
Related
Is it possible to have value of a relationship between node as a user-defined class or it should be a primitive datatype. For example:
#RelationshipEntity(type = "ALIGNED_WITH")
public class AlignedWith {
#GraphId
private Long id;
private Set<DeptEndorsement> deptEndorsement = new HashSet<>();
#StartNode
private Term startTerm;
#EndNode
private Term endTerm;
// getters and setters
}
public class DeptEndorsement {
private String deptName;
private Integer endorsementCount;
// getters and setters
}
#NodeEntity
public class Term {
#GraphId
private Long id;
private String termName;
#Relationship(type = "ALIGNED_WITH", direction = Relationship.OUTGOING)
private List<AlignedWith> alignedWith = new ArrayList<>();
public void addAlignedWith(AlignedWith alignedWith) {
this.alignedWith.add(alignedWith);
}
// getters and setters
}
If you observe DeptEndorsement is a custom class which I want as value for relationship AlignedWith
Or, is it possible to have a HashMap (example: HashMap<String, Integer>) as the value for the relationship between nodes?
These are my finding for this question, supported datatypes in Neo4J are:
Boolean, Long, Double, String, List. Types that can be type casted are Short, Integer, Float (though you need to specify that you allow casting, check below if condition) Ref. MapCompositeConverter.java
Below condition was responsible to check if the type is supported by Neo4J or not, through in my case I did not provide allowCast to be true and thus java.lang.Integer was not castable:
if (isCypherType(entryValue) || (allowCast && canCastType(entryValue)))
private boolean isCypherType(Object entryValue) {
return cypherTypes.contains(entryValue.getClass()) || List.class.isAssignableFrom(entryValue.getClass());
}
I changed from java.lang.Integer to java.lang.Long and everything works fine. You can not use custom datatype as in my case DeptEndorsement
Below is how my properties look like for a relationship.
#Properties
private Map<String, Long> deptEndorsement = new HashMap<>();
#Properties annotation is available in neo4j-ogm-core 3.0.0+ Ref: Compatibility
I have implemented a base class for all entities in my application that can have a subscribers:
#NodeEntity
public abstract class Subscribable extends BaseEntity {
private final static String SUBSCRIBED_TO = "SUBSCRIBED_TO";
#Relationship(type = SUBSCRIBED_TO, direction = Relationship.INCOMING)
private Set<User> subscribers = new HashSet<>();
public Set<User> getSubscribers() {
return subscribers;
}
public void setSubscribers(Set<User> subscribers) {
this.subscribers = subscribers;
}
public boolean addSubscriber(User subscriber) {
return subscribers.add(subscriber);
}
}
Also I have a few concrete types that extend Subscribable class, for example Product, Tag, User etc
Right now in order to get all User subscribed to a particular Subscribable I use a following method with a Cypher query:
#Query("MATCH (sub:Subscribable)<-[:SUBSCRIBED_TO]-(u:User) WHERE id(sub) = {subscribableId} RETURN u")
Set<User> findAllUsersBySubscribableId(#Param("subscribableId") Long subscribableId);
The issue is that AFAIK different objects of different types can potentially have the same Id so looks like the approach to get all User based only on subscribableId(GraphId) can not be consistent and potentially can lead to a different results.
The idea is to supply additional parameter to findAllUsersBySubscribableId method, for example String type in order to specify what exact type I expect under provided subscribableId.. for example "Product"
Please help to extend the following Cypher query in order to support entity type:
MATCH (sub:Subscribable)<-[:SUBSCRIBED_TO]-(u:User) WHERE id(sub) = {subscribableId} RETURN u
I'm building a little test app as a way to learn Angular and refresh myself on a lot of the Spring stack. I have some minor experience with Neo4J, but the app idea has ground with a graph db like Neo4j.
The idea is pretty simple, a ui to create characters and stories, and relate the characters to the stories and each other, map their individual versions of a story and create some graphs that show the character interactions to help write the overall narrative.
I've got nodes for the characters and stories easily enough and the Spring stack is great for giving me rest easy to use rest endpoints for the nodes themselves. But I can't find any concrete examples of creating and maintaining the relationships between those nodes.
For instance, in Cypher, I can relate a character to a story and tell that being's involvement to the story as a relationship property with:
match(p:Being ),(s:Story ) where id(p) = 7 and id(s) = 16
create (p)-[r:TOOK_PART_IN{perspective:"I did not know Mr. Grey better than an acquaintance, though I knew others whom did. They were not made better because of their relationship with him."}]->(s) return r
Then with the mapping in Spring, the data I get back from the REST endpoint gives me my character and I can follow a link to get the stories that character is a part of. I don't see a way though to post or put to add or remove the character from stories.
I'm also only finding concrete examples in docs from Spring regarding nodes, not really with edges/relationships. Can anyone supply anything like that?
I am fully aware that Neo4J has it's own REST interface, and that is basically what Spring is consuming as well. The main purpose of this exercise is learning some new technology (Angular2/typescript) and refreshing my knowledge of the Spring stack
Thanks!
I'm not sure if anyone else has ever found a good or better answer to this, but here is what I had found to work. I have a spring boot project running, I'll post some of the most pertinent code and examples in this answer, but to see the whole REST service project, check https://github.com/jrspriggs/Chronicler
So, the purpose atm for the small app is to create characters/beings that take part in stories, create stories (featuring a title and slug line) and create a relationship between a being and a story with the being's perspective of the story attached to that relationship. This way it collects the various versions of the story from each character.
The neo4j instance is just a basic neo4j instance in Docker/Kitematic on my Windows laptop. Here are the models:
Being.java:
package com.chronicler.model;
import java.util.Iterator;
import java.util.Set;
import org.springframework.data.neo4j.annotation.Fetch;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
import org.springframework.data.neo4j.annotation.RelatedToVia;
#NodeEntity
public class Being {
public Long getId() {
return id;
}
#GraphId private Long id;
private String firstName;
private String lastName;
private boolean hero;
private boolean villain;
#RelatedToVia(type="TOOK_PART_IN")
#Fetch private Set<Involvement> involvements;
public Set<Involvement> getInvolvements() {
return involvements;
}
public void setInvolvements(Set<Involvement> involvements) {
this.involvements = involvements;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public boolean isHero() {
return hero;
}
public void setHero(boolean hero) {
this.hero = hero;
}
public boolean isVillain() {
return villain;
}
public void setVillain(boolean villain) {
this.villain = villain;
}
}
Story.java
package com.chronicler.model;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.NodeEntity;
#NodeEntity
public class Story {
public Long getId() {
return id;
}
#GraphId private Long id;
private String title;
private String slug;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
}
Involvement.java (relationship between being to story)
package com.chronicler.model;
import org.springframework.data.neo4j.annotation.EndNode;
import org.springframework.data.neo4j.annotation.Fetch;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.RelationshipEntity;
import org.springframework.data.neo4j.annotation.StartNode;
#RelationshipEntity(type="TOOK_PART_IN")
public class Involvement {
#GraphId private Long relationshipId;
#Fetch #StartNode private Being being;
#Fetch #EndNode private Story story;
private String perspective;
public Long getRelationshipId() {
return relationshipId;
}
public void setRelationshipId(Long relationshipId) {
this.relationshipId = relationshipId;
}
public Being getBeing() {
return being;
}
public void setBeing(Being being) {
this.being = being;
}
public Story getStory() {
return story;
}
public void setStory(Story story) {
this.story = story;
}
public String getPerspective() {
return perspective;
}
public void setPerspective(String perspective) {
this.perspective = perspective;
}
}
From there I have basically the base kind of repository rest resource classes set up for the spring data services. Those take care of the entities, but they fail to really address the relationship for me. What does is to implement a separate rest route to save it
BeingController.java:
package com.chronicler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.chronicler.model.Involvement;
import com.chronicler.repo.InvolvementRepository;
#RestController
public class BeingController {
#Autowired
InvolvementRepository involvementRepository;
#RequestMapping(value="/beingStory", method=RequestMethod.POST)
public Involvement createBeingStoryRelationship(#RequestBody Involvement involvement) {
involvementRepository.save(involvement);
return involvement;
}
}
From there, just posting to localhost:3000/beingStory with the following kind of json body will accurately create the relationship:
{
"character": {
"id": 17,
"firstName": "Dr. Victor",
"lastName": "Frankenstein",
"hero": true,
"villain": true
},
"story": {
"id": 15,
"title": "Frankenstein",
"slug": "A doctor plays god"
},
"relationshipId": 10,
"perspective": "I did a monstrous thing. I wanted to create life, but I created horrors... such unimaginable horrors, such that mankind has not ever imagined."
}
From that point then, you can walk the relationships from the person to the story. I'll have to add more in the future as I work through this sample app some more to implement the reverse relationship for seeing character involvements from a selected story, and I intend to add relationships between the characters.
I'm using Spring Data Neo4J 3.3.1 and I'm having difficulty updating nodes. I have code equivalent to this, and it has zero impact on the database:
#Transactional
public void editTitle(Long nodeId, String newTitle) {
MyNode existingNode = nodeRepository.findOne(nodeId);
existingNode.getSubObject().setTitle(newTitle);
nodeRepository.save(existingNode);
}
This is the MyNode class:
#NodeEntity
public class MyNode {
#Fetch
#RelatedTo(type="LINKED_TO", direction = Direction.OUTGOING)
private SubObject subObject;
public SubObject getSubObject() {
return subObject;
}
}
This is the SubObject class:
#NodeEntity
public class SubObject {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
I am able to save new nodes of type MyNode without a problem. I simply call nodeRepository.save(newNode); and it saves the MyNode AND the SubObject to the database at once.
I have confirmed that the transaction is being committed. I've followed the call stack all the way down to the NeoStoreTransaction class and observed the executeModified method processing a command, but the field I changed is not represented there.
For edits, you will need to save related nodes explicitly:
#Transactional
public void editTitle(Long nodeId, String newTitle) {
MyNode existingNode = nodeRepository.findOne(nodeId);
existingNode.getSubObject().setTitle(newTitle);
subObjectRepository.save(existingNode.getSubObject());
nodeRepository.save(existingNode);
}
That's because the save operation treats related nodes differently depending on whether they have already been persisted to the database.
For cases where the related node has not been persisted yet, the related node will be saved automatically.
For cases where the related node has already been persisted, e.g. when you're coming back later to make an edit, changes to properties on the related node will NOT be picked up.
I'm using Neo4J-2.0.1 and SDN - 3.0.0.RELEASE.
I have a NodeEntity as follow:
#NodeEntity
public class Group {
#GraphId
Long id;
#Indexed(unique = true, indexType = IndexType.SIMPLE)
public String name;
public String property1;
public String property2;
public Group() {
}
public Group(String str) {
name = str;
}
}
I have a groups repository :
public interface GroupsRepository extends GraphRepository<Group> {
Group getGroupByName(String name);
}
After the getGroupByName(...) method is invoked, the
ExecutingRestAPI.getNodeById(...)
method is invoked as the number of the properties that the Group has.
How can I avoid this kind of behaviour?
Are there any additional queries being executed under the hood?
You cannot avoid this behavior at the moment, it loads the dependent entities individually. The REST integration is currently not at all optimized.