目录
一.pom.xml中添加spring-data-neo4j依赖
二.数据库连接配置文件neo4j.properties
三.日志打开Cypher的DEBUG信息,便于调试
四.JAVA代码
4.1 Neo4jConfiguration.java为配置类
4.2 Const.java为常量定义,这里列举了关系类型
4.3 Neo4jEntity.java图实体基类,把图实体的ID属性放在基类
4.4 RelActedInDTO.java为业务DTO根据输入输出设计自己定义
4.5 Movie.java定义了电影节点和对应的关系
4.6 Person.java定义了人节点和对应的关系
4.7 RelActedIn.java定义了参演关系
4.8 RelDirected.java定义了导演关系
4.9 RelFollows.java定义了跟随关系
4.10 RelReviewed.java定义了评介关系
4.11 MovieRepository.java定义了电影为主的图数据访问方法
4.12 PersonRepository.java定义了人为主的图数据访问方法
4.13 MovieService.java定义了服务接口
4.14 MovieServiceImpl.java定义了服务实现
4.15 MovieController.java定义了控制器
五.测试
5.1 取得指定电影的参演演员和角色
5.2 为电影添加演员,如果电影或演员不存在会自动创建对应的节点
• 实验中使用独立的Neo4j服务器,采用bolt协议访问。如果是使用嵌入式neo4j或者使用http协议,需要添加其他依赖。 • 5.3.4.RELEASE版本的spring-data-neo4j需要至少 Neo4j 3.4以上 Spring Framework 5.2.9.RELEASE以上
<!-- Neo4j --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-neo4j</artifactId> <version>5.3.4.RELEASE</version> </dependency>• SpringMVC的时候在log4j2.xml里的Loggers里添加
<Logger name="org.neo4j.ogm.drivers.bolt" level="DEBUG" additivity="false"> <AppenderRef ref="Console"/> </Logger>• Springboot的时候只需要在application.properties里添加"logging.level.org.neo4j.ogm.drivers.bolt=DEBUG"
实验中采用Neo4j 4.0自带的Movie Graph为数据 代码结构如下
sessionFactory和transactionManager都改成了带neo4j开头的名字,可以在同一应用中有关系型数据库访问框架时并存使用。@EnableNeo4jRepositories的注解属性需要做相应的修改。
package com.study.neo4j.config; import org.neo4j.ogm.config.ClasspathConfigurationSource; import org.neo4j.ogm.config.ConfigurationSource; import org.neo4j.ogm.session.SessionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.transaction.Neo4jTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableNeo4jRepositories(sessionFactoryRef = "neo4jSessionFactory", transactionManagerRef="neo4jTransactionManager", basePackages = "com.study.neo4j.dao") @EnableTransactionManagement public class Neo4jConfiguration { @Bean public SessionFactory neo4jSessionFactory() { // with domain entity base package(s) return new SessionFactory(configuration(), "com.study.neo4j.bean"); } @Bean public org.neo4j.ogm.config.Configuration configuration() { ConfigurationSource properties = new ClasspathConfigurationSource("neo4j.properties"); org.neo4j.ogm.config.Configuration configuration = new org.neo4j.ogm.config.Configuration.Builder(properties).build(); return configuration; } @Bean public Neo4jTransactionManager neo4jTransactionManager() { return new Neo4jTransactionManager(neo4jSessionFactory()); } }三个关系类型都以电影节点为结束节点
package com.study.neo4j.bean.node; import java.util.List; import org.neo4j.ogm.annotation.NodeEntity; import org.neo4j.ogm.annotation.Relationship; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.rel.RelDirected; import com.study.neo4j.bean.rel.RelReviewed; import com.study.neo4j.bean.rel.RelActedIn; @NodeEntity public class Movie extends Neo4jEntity { private String title; private String tagline; private Integer released; @Relationship(type = Const.REL_TYPE_ACTEDIN, direction=Relationship.INCOMING) private List<RelActedIn> relActedIns; @Relationship(type = Const.REL_TYPE_DIRECTED, direction=Relationship.INCOMING) private List<RelDirected> relDirecteds; @Relationship(type = Const.REL_TYPE_REVIEWED, direction=Relationship.INCOMING) private List<RelReviewed> relRevieweds; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getTagline() { return tagline; } public void setTagline(String tagline) { this.tagline = tagline; } public Integer getReleased() { return released; } public void setReleased(Integer released) { this.released = released; } public List<RelActedIn> getRelActedIns() { return relActedIns; } public void setRelActedIns(List<RelActedIn> relActedIns) { this.relActedIns = relActedIns; } public List<RelDirected> getRelDirecteds() { return relDirecteds; } public void setRelDirecteds(List<RelDirected> relDirecteds) { this.relDirecteds = relDirecteds; } public List<RelReviewed> getRelRevieweds() { return relRevieweds; } public void setRelRevieweds(List<RelReviewed> relReviewed) { this.relRevieweds = relReviewed; } @Override public String toString() { return "Movie [title=" + title + ", tagline=" + tagline + ", released=" + released + ", relActedIns=" + relActedIns + ", relDirecteds=" + relDirecteds + ", relReviewed=" + relRevieweds + ", id=" + id + "]"; } }三个关系类型以人节点为结束节点 relFollows和relFollowsBy定义了同一种关系类型,对于单个节点来说又分为以本人为结束节点的关系和以本人为开始节点的关系
package com.study.neo4j.bean.node; import java.util.List; import org.neo4j.ogm.annotation.NodeEntity; import org.neo4j.ogm.annotation.Relationship; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.rel.RelActedIn; import com.study.neo4j.bean.rel.RelDirected; import com.study.neo4j.bean.rel.RelFollows; import com.study.neo4j.bean.rel.RelReviewed; @NodeEntity public class Person extends Neo4jEntity { private String name; private Integer born; @Relationship(type = Const.REL_TYPE_ACTEDIN) private List<RelActedIn> relActedIns; @Relationship(type = Const.REL_TYPE_DIRECTED) private List<RelDirected> relDirecteds; @Relationship(type = Const.REL_TYPE_REVIEWED) private List<RelReviewed> relRevieweds; @Relationship(type = Const.REL_TYPE_FOLLOWS) private List<RelFollows> relFollows; @Relationship(type = Const.REL_TYPE_FOLLOWS, direction=Relationship.INCOMING) private List<RelFollows> relFollowsBy; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getBorn() { return born; } public void setBorn(Integer born) { this.born = born; } public List<RelActedIn> getRelActedIns() { return relActedIns; } public void setRelActedIns(List<RelActedIn> relActedIns) { this.relActedIns = relActedIns; } public List<RelDirected> getRelDirecteds() { return relDirecteds; } public void setRelDirecteds(List<RelDirected> relDirecteds) { this.relDirecteds = relDirecteds; } public List<RelReviewed> getRelRevieweds() { return relRevieweds; } public void setRelRevieweds(List<RelReviewed> relRevieweds) { this.relRevieweds = relRevieweds; } public List<RelFollows> getRelFollows() { return relFollows; } public void setRelFollows(List<RelFollows> relFollows) { this.relFollows = relFollows; } public List<RelFollows> getRelFollowsBy() { return relFollowsBy; } public void setRelFollowsBy(List<RelFollows> relFollowsBy) { this.relFollowsBy = relFollowsBy; } @Override public String toString() { return "Person [name=" + name + ", born=" + born + ", relActedIns=" + relActedIns + ", relDirecteds=" + relDirecteds + ", relRevieweds=" + relRevieweds + ", relFollows=" + relFollows + ", relFollowsBy=" + relFollowsBy + ", id=" + id + "]"; } }分别使用@StartNode和@EndNode注解表示关系的开始节点和结束节点 为了避免Json序列化时候的无限循环,使用@JsonIgnore注解取消对开始节点和结束节点的序列化,并在toString中不做开始节点和结束节点的toString
package com.study.neo4j.bean.rel; import java.util.List; import org.neo4j.ogm.annotation.EndNode; import org.neo4j.ogm.annotation.Property; import org.neo4j.ogm.annotation.RelationshipEntity; import org.neo4j.ogm.annotation.StartNode; import com.fasterxml.jackson.annotation.JsonIgnore; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.node.Movie; import com.study.neo4j.bean.node.Person; @RelationshipEntity(type = Const.REL_TYPE_ACTEDIN) public class RelActedIn extends Neo4jEntity { @Property private List<String> roles; @StartNode @JsonIgnore private Person person; @EndNode @JsonIgnore private Movie movie; public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } public Movie getMovie() { return movie; } public void setMovie(Movie movie) { this.movie = movie; } @Override public String toString() { return "RelActedIn [roles=" + roles + ", person.id=" + person==null?null:person.getId() + ", movie.id=" + movie==null?null:movie.getId() + ", id=" + id + "]"; } }分别使用@StartNode和@EndNode注解表示关系的开始节点和结束节点 为了避免Json序列化时候的无限循环,使用@JsonIgnore注解取消对开始节点和结束节点的序列化,并在toString中不做开始节点和结束节点的toString
package com.study.neo4j.bean.rel; import org.neo4j.ogm.annotation.EndNode; import org.neo4j.ogm.annotation.RelationshipEntity; import org.neo4j.ogm.annotation.StartNode; import com.fasterxml.jackson.annotation.JsonIgnore; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.node.Movie; import com.study.neo4j.bean.node.Person; @RelationshipEntity(type = Const.REL_TYPE_DIRECTED) public class RelDirected extends Neo4jEntity { @StartNode @JsonIgnore private Person person; @EndNode @JsonIgnore private Movie movie; public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } public Movie getMovie() { return movie; } public void setMovie(Movie movie) { this.movie = movie; } @Override public String toString() { return "RelActedIn [person.id=" + person==null?null:person.getId() + ", movie.id=" + movie==null?null:movie.getId() + ", id=" + id + "]"; } }分别使用@StartNode和@EndNode注解表示关系的开始节点和结束节点 为了避免Json序列化时候的无限循环,使用@JsonIgnore注解取消对开始节点和结束节点的序列化,并在toString中不做开始节点和结束节点的toString
package com.study.neo4j.bean.rel; import org.neo4j.ogm.annotation.EndNode; import org.neo4j.ogm.annotation.RelationshipEntity; import org.neo4j.ogm.annotation.StartNode; import com.fasterxml.jackson.annotation.JsonIgnore; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.node.Person; @RelationshipEntity(type = Const.REL_TYPE_FOLLOWS) public class RelFollows extends Neo4jEntity { @StartNode @JsonIgnore private Person person; @EndNode @JsonIgnore private Person followPerson; public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } public Person getFollowPerson() { return followPerson; } public void setMovie(Person followPerson) { this.followPerson = followPerson; } @Override public String toString() { return "RelActedIn [person.id=" + person==null?null:person.getId() + ", followPerson.id=" + followPerson==null?null:followPerson.getId() + ", id=" + id + "]"; } }分别使用@StartNode和@EndNode注解表示关系的开始节点和结束节点 为了避免Json序列化时候的无限循环,使用@JsonIgnore注解取消对开始节点和结束节点的序列化,并在toString中不做开始节点和结束节点的toString
package com.study.neo4j.bean.rel; import org.neo4j.ogm.annotation.EndNode; import org.neo4j.ogm.annotation.RelationshipEntity; import org.neo4j.ogm.annotation.StartNode; import com.fasterxml.jackson.annotation.JsonIgnore; import com.study.neo4j.bean.Const; import com.study.neo4j.bean.Neo4jEntity; import com.study.neo4j.bean.node.Movie; import com.study.neo4j.bean.node.Person; @RelationshipEntity(type = Const.REL_TYPE_REVIEWED) public class RelReviewed extends Neo4jEntity { private Integer rating; private String summary; @StartNode @JsonIgnore private Person person; @EndNode @JsonIgnore private Movie movie; public Integer getRating() { return rating; } public void setRating(Integer rating) { this.rating = rating; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } public Movie getMovie() { return movie; } public void setMovie(Movie movie) { this.movie = movie; } @Override public String toString() { return "RelReviewed [rating=" + rating + ", summary=" + summary + ", person.id=" + person==null?null:person.getId() + ", movie.id=" + movie==null?null:movie.getId() + ", id=" + id + "]"; } }findByTitle为自动生成型方法,函数的查询语句会根据方法名自动生成,这个功能很有意思,后面再另起一篇记载自动生成都可以有哪些要素。查询的默认深度为1,即查询本节点以及和本节点有直接关系的节点,查询的关系类型为Movie.java中定义的关系类型。可以使用@Depth注解直接改变当前方法的查询深度,也可以使用(@Depth int depth)定义为方法的一个参数,根据传入参数决定查询深度,0为只查询本节点不查询关系,-1为无限延申。 其他函数为自定义查询,没有深度的问题,只返回自己Cypher写的return内容。 查询可以返回分页数据,例如getActorsThatActInMovieFromTitle。 如果要直接返回指定关系类型,需要Cypher里return (n1)-[r]->(n2)类似的路径。
package com.study.neo4j.dao; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.neo4j.annotation.Depth; import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.repository.Neo4jRepository; import com.study.neo4j.bean.node.Movie; import com.study.neo4j.bean.node.Person; import com.study.neo4j.bean.rel.RelActedIn; public interface MovieRepository extends Neo4jRepository<Movie,Long> { // returns the node with id equal to idOfMovie parameter @Query("MATCH (n) WHERE id(n)=$0 RETURN n") Movie getMovieFromId(Integer idOfMovie); // returns the nodes which have a title according to the movieTitle parameter @Query("MATCH (movie:Movie {title:$0}) RETURN movie") Movie getMovieFromTitle(String movieTitle); //@Depth(value=2) Movie findByTitle(String title); // returns a Page of Person that have a ACTED_IN relationship to the movie node with the title equal to movieTitle parameter. @Query(value = "MATCH (movie:Movie {title:$0})<-[:ACTED_IN]-(actor) RETURN actor ORDER BY actor.name", countQuery= "MATCH (movie:Movie {title:$0})<-[:ACTED_IN]-(actor) RETURN count(actor)") Page<Person> getActorsThatActInMovieFromTitle(String movieTitle, PageRequest page); // returns a Slice of Person that have a ACTED_IN relationship to the movie node with the title equal to movieTitle parameter. @Query("MATCH p=(movie:Movie {title:$0})<-[relActedIn:ACTED_IN]-(actor) RETURN p") List<RelActedIn> getActorsAndRolesThatActInMovieFromTitle(String movieTitle); // returns users who directed a movie @Query("MATCH (movie:Movie {title:$0})<-[DIRECTED]-(user) RETURN user") List<Person> getUsersWhoRatedMovieFromTitle(String movieTitle); }@Transactional(value="neo4jTransactionManager")指定使用Neo4jConfiguration.java里定义的事务管理器。如果本应用中没有其他事务管理器可以去除括号里的内容 Neo4jRepository.save()保存数据时,默认深度是-1,所有movie的属性、movie对应的关系、movie对应的人、人对应的关系......,会自动判断查询出来之后有没有发生变化,如果有变化,会自动更新。也可以手动指定保存时的深度,0为只保存movie的属性,不管对应的关系以及关系连接的节点,1为保存movie的属性、关系以及关系连接的节点的属性。
package com.study.neo4j.service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.study.neo4j.bean.RelActedInDTO; import com.study.neo4j.bean.node.Movie; import com.study.neo4j.bean.node.Person; import com.study.neo4j.bean.rel.RelActedIn; import com.study.neo4j.bean.rel.RelDirected; import com.study.neo4j.bean.rel.RelReviewed; import com.study.neo4j.dao.MovieRepository; import com.study.neo4j.dao.PersonRepository; @Service("movieService") @Transactional(value="neo4jTransactionManager") public class MovieServiceImpl implements MovieService { @Autowired private MovieRepository movieRepository; @Autowired private PersonRepository personRepository; @Override public Iterable<Movie> findAll() { return movieRepository.findAll(); } @Override public Movie getMovie(String movieTitle) { return movieRepository.getMovieFromTitle(movieTitle); } @Override public List<Person> getActorsByMovieTitle(String movieTitle) { PageRequest pageRequest = PageRequest.of(0, 3); Page<Person> person = movieRepository.getActorsThatActInMovieFromTitle(movieTitle, pageRequest); return person.toList(); } @Override public List<Person> getActorsAndRolesByMovieTitle(String movieTitle) { List<Person> listRet = new ArrayList<Person>(); List<RelActedIn> list = movieRepository.getActorsAndRolesThatActInMovieFromTitle(movieTitle); for (RelActedIn relActedIn : list) { listRet.add(relActedIn.getPerson()); } return listRet; } @Override public RelActedInDTO addActors(RelActedInDTO relActedInDTO) { // 查找电影,没有就使用请求参数新建 Movie movie = movieRepository.findByTitle(relActedInDTO.getMovie().getTitle()); if (movie == null) { movie = relActedInDTO.getMovie(); movie.setRelActedIns(new ArrayList<RelActedIn>(1)); } // 查新人,没有就使用请求参数新建 Person person = personRepository.findByName(relActedInDTO.getPerson().getName()); if (person == null) { person = relActedInDTO.getPerson(); person.setRelActedIns(new ArrayList<RelActedIn>(1)); } RelActedIn relActedIn = null; // 查询是否已经参演 for (RelActedIn relActedInTmp:movie.getRelActedIns()) { if (StringUtils.equals(person.getName(), relActedInTmp.getPerson().getName())) { relActedIn = relActedInTmp; break; } } if (relActedIn == null) { // 变更前没有参演,新建关系 relActedIn = relActedInDTO.getRelActedIn(); relActedIn.setMovie(movie); relActedIn.setPerson(person); movie.getRelActedIns().add(relActedIn); person.getRelActedIns().add(relActedIn); } else { // 改变参演角色列表 relActedIn.setRoles(relActedInDTO.getRelActedIn().getRoles()); } movieRepository.save(movie); // 设置返回值 RelActedInDTO retRelActedInDTO = new RelActedInDTO(); retRelActedInDTO.setMovie(movie); retRelActedInDTO.setPerson(person); retRelActedInDTO.setRelActedIn(relActedIn); return retRelActedInDTO; } @Override public Map<String, List<Person>> getPersonsByMovieTitle(String movieTitle) { Movie movie = movieRepository.findByTitle(movieTitle); Map<String, List<Person>> retMap = new HashMap<String, List<Person>>(); // 取得参演人员 List<Person> list = new ArrayList<Person>(); for (RelActedIn relActed : movie.getRelActedIns()) { list.add(relActed.getPerson()); } retMap.put("actedIns", list); // 取得导演人员 list = new ArrayList<Person>(); for (RelDirected relDirected : movie.getRelDirecteds()) { list.add(relDirected.getPerson()); } retMap.put("directeds", list); // 取得评介人员 list = new ArrayList<Person>(); for (RelReviewed relReviewed : movie.getRelRevieweds()) { list.add(relReviewed.getPerson()); } retMap.put("revieweds", list); return retMap; } }请求地址 /movie/getActorsAndRolesByMovieTitle 请求Json内容:
{"title":"The Matrix"}返回内容:
{ "code": 0, "count": 0, "data": [ { "id": 8, "name": "Emil Eifrem", "born": 1978, "relActedIns": [ { "id": 7, "roles": [ "Emil" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null }, { "id": 3, "name": "Laurence Fishburne", "born": 1961, "relActedIns": [ { "id": 2, "roles": [ "Morpheus" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null }, { "id": 4, "name": "Hugo Weaving", "born": 1960, "relActedIns": [ { "id": 3, "roles": [ "Agent Smith" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null }, { "id": 2, "name": "Carrie-Anne Moss", "born": 1967, "relActedIns": [ { "id": 1, "roles": [ "Trinity" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null }, { "id": 1, "name": "Keanu Reeves", "born": 1964, "relActedIns": [ { "id": 0, "roles": [ "Neo" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null } ], "msg": "操作成功" }请求地址 /movie/addActors 请求Json内容:
{ "movie": { "title": "这是一个新电影", "tagline": "这是电影宣传词", "released": 2020 }, "person": { "name": "李四", "born": 1986 }, "relActedIn": { "roles": [ "大铃铛", "路人乙", "大坏蛋" ] } }返回内容:
{ "code": 0, "count": 0, "data": { "relActedIn": { "id": 254, "roles": [ "大铃铛", "路人乙", "大坏蛋" ] }, "movie": { "id": 172, "title": "这是一个新电影", "tagline": "这是电影宣传词", "released": 2020, "relActedIns": [ { "id": 257, "roles": [ "路人丙2" ] }, { "id": 256, "roles": [ "路人丙" ] }, { "id": 253, "roles": [ "小铃铛", "路人甲" ] }, { "id": 254, "roles": [ "大铃铛", "路人乙", "大坏蛋" ] } ], "relDirecteds": null, "relRevieweds": null }, "person": { "id": 174, "name": "李四", "born": 1986, "relActedIns": [ { "id": 254, "roles": [ "大铃铛", "路人乙", "大坏蛋" ] } ], "relDirecteds": null, "relRevieweds": null, "relFollows": null, "relFollowsBy": null } }, "msg": "操作成功" }多次更改后的图: