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.
Related
I am using Spring Data Neo4j RX. And I have a query like this:
#Query("MATCH (a:Repo)-[:REPO_DEPEND_ON]->(b:Repo) WHERE a.name= $name RETURN a.name, b.name")
String[] getSingleRepoDependencyTo(String name);
I know the return type is wrong here, as it cannot be a String array. But how can I get the result properly, which contains two fields?
I have searched online for a long time but cannot find an answer. The "#QueryResult" annotation is not supported in this RX version yet.
Thanks for your help.
Assuming that you have a mapped #Node Repo with its relationships like
#Node
public class Repo {
// other things
String name;
#Relationship("REPO_DEPEND_ON") Repo repo;
}
and defining this method in a ...extends Neo4jRepository<Repo,...>,
you could use Projections.
public interface RepoProjection {
String getName();
DependingRepo getRepo();
/**
* nested projection
*/
interface DependingRepo {
String getName();
}
}
Important to keep in mind that the returned values should be the nodes and relationship to make it work this way.
You could also remove the custom query and do something like:
RepoProjection findByName(String name)
if you do not have the need for a findByName in this repository for the entity itself.
Take a look here: https://neo4j.github.io/sdn-rx/current/#projections.interfaces
It seems to list exactly what you want. From those docs:
interface NamesOnly {
String getFirstName();
String getLastName();
}
interface PersonRepository extends Neo4jRepository<Person, Long> {
List<NamesOnly> findByFirstName(String firstName);
}
There are some other variations too.
You can use annotation #QueryResult on your expected model. For instance you can do that in this way.
DTO:
import org.springframework.data.neo4j.annotation.QueryResult;
#QueryResult
public class SomeDto {
private int someInt;
private SomeObject sobj;
private double sdouble;
private AnotherObject anObj;
//getters setters
}
Neo4jRepository:
public interface DomainObjectRepository extends Neo4jRepository<DomainObject, Long> {
#Query("MATCH(n:SomeTable) RETURN someInt, sobj, sdouble, anObj") //Return a few columns
Optional<SomeDto> getSomeDto();
}
I'm having a problem querying based on an Enum property of my NodeEntity.
The NodeEntity in question is defined:
#NodeEntity(label = "Entity")
public class MyEntity {
#GraphId
private Long internalId;
....
private State state;
#Transient
public enum State {
STATEONE, STATETWO, STATETHREE
}
....
It saves without a problem, the state Enum represented perfectly, and I can query using other properties (Strings) with no problem at all. However the problem is the following query in a repository:
#Query("MATCH (entity:Entity {state:{0}})" +
"RETURN entity")
List<MyEntity> findByState(MyEntity.State state)
i.e. find all entities with the given state.
There's no exception, however using this simply returns a List of 0 Entities.
I've tried all kinds of variations on this, using a WHERE clause for example, with no luck.
The Entities are persisted properly, using findAll() in the same test returns the expected List of Entities with their states exactly as I would expect.
Any thoughts?
Not quite sure what the value #Transient adds to the enum. It is anyway not persistable as a node or relationship in Neo4j. It is sufficient to define the field as one that should persist with
private State state;
and leave off the #Transient annotation from the enum.
With it, SDN ignores the field sent to the derived query.
However, if you have a good reason to mark the enum #Transient, please do share it and we'll re-visit this case.
There is a general problems using spring data rest interface to search on enum fields. Just using the enum-to-string converter cannot work for search where you want to find if the value is IN a collection of values:
public interface AppointmentRepository extends Neo4jRepository<Appointment, Long> {
Page<Appointment> findByDayOfWeekIn(#Param("days") List<DayOfWeek> days, Pageable pageable);
}
The above does not work out of the box because neo4j will try to convert a List to your property type: DayOfWeek
In order to work around this I needed a custom converter that handles both requests providing collection of values (the search) and single values (the normal read and write entity):
#SuppressWarnings({ "unchecked", "rawtypes" })
public abstract class SearchQueryEnumConverter<T extends Enum> {
private Class<T> enumType;
public SearchQueryEnumConverter() {
enumType = (Class<T>) ((ParameterizedType) this.getClass()).getActualTypeArguments();
}
public Object toGraphProperty(Object value) {
if (Collection.class.isAssignableFrom(value.getClass())) {
List<T> values = (List<T>) value;
return values.stream().map(Enum::name).collect(Collectors.toList());
}
return ((Enum) value).name();
}
public Object toEntityAttribute(Object value) {
if (Collection.class.isAssignableFrom(value.getClass())) {
List<String> values = (List<String>) value;
return values.stream().map(v -> (T) T.valueOf(enumType, v)).collect(Collectors.toList());
}
return (T) T.valueOf(enumType, value.toString());
}
}
The abstract converter can be reified by all enums, and used as parameter of the #Convert annotation:
public enum EnumType {
VALUE_A, VALUE_B;
public static class Converter extends SearchQueryEnumConverter<EnumType> implements AttributeConverter {
}
}
#NodeEntity
public Entity {
#Property
#Convert(EnumType.Converter.class)
EnumType type;
}
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 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