SpringBoot+Neo4j+MySQL集成和操作实现
吐槽:公司领导突然说了一个之前一直没有听说过的数据库->neo4j图形数据库,当时一阵懵逼,一步步填坑过来,留下项目笔记,等一个有缘人。本文讲解了数据库安装、SpringBoot整合、常规CRUD、自定义分页查询、集成Mybatis-Plus、事务处理。
目录:
本文主要分为如下几个章节进行集成操作示例和填坑说明。
1、 neo4j数据库的下载安装
2、 SpringBoot集成neo4j数据库依赖引入和配置
3、 数据库节点CRUD操作
4、 自定义分页实现
5、 集成MySQL+Mybatis-Plus
6、多数据源事务处理
1、neo4j数据库的下载安装
数据库可以直接在官网下载,根据自己的系统选择,neo4j数据库的使用需要注意JDK的版本,3.*版本jdk要求为8,4.*版本jdk要求为11,本文以jdk8为例。
官网地址:neo4j官网地址社区版下载地址
下载好后windows直接解压(感兴趣的也可下载对应的docker镜像使用)
在对应的bin目录下运行cmd启动数据库输入(相关操作指令可自行了解)
neo4j console
指令运行成功后复制cmd中的地址打开即可访问数据库
如图:
浏览器访问该地址(会自动跳转到http://localhost:7474/browser/)默认会连接上本地运行的neo4j数据库,若未连接可手动连接如图:
初始账号和密码都是neo4j,第一次登录会提示修改密码,自行修改后记住密码即可。连接成功如图:
这样就可以直接在上面的命令行中输入对应的CQL语句进行操作了。
2、SpringBoot集成neo4j数据库依赖引入和配置
SpringBoot集成的neo4j数据库操作有很多版本,且每个版本对应的neo4j操作源码存在很大的差异,这点需要特别注意,版本不对应是无法集成成功的。本文采用springboot.version=2.5.3,对应的neo4j的驱动为6.1.3[此版本高出目前网络上大多资料版本,导致一直踩坑]。
pom关键配置如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/>
</parent>
<dependencies>
<!--neo4j-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<!--neo4j end-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
对应的application.yml数据库连接信息如图:
注意此处的uri地址,有bolt/http/https三种方式,这个在大家安装该数据库时会有了解。
3、数据库节点CRUD操作
实体类创建
其实和常见实体无太大差别,只是需要指定对应的数据库节点和标明字段为属性等。
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import java.io.Serializable;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月27日 9:09
*/
@Data
@Node("dongMan")
public class DongMan implements Serializable {
@Id
@GeneratedValue
@ApiModelProperty(value = "主键,neo4j数据库自生成值")
private Long id;
@Property
@ApiModelProperty(value = "姓名")
private String name;
@Property
@ApiModelProperty(value = "年龄")
private String age;
@Property
@ApiModelProperty(value = "性别")
private String sex;
}
创建Service
springboot-data-neo4j提供了和mybatis-plus类似的crud操作仓库,我们可以直接继承使用,也可自行编写实现。
service代码:
import org.springframework.data.domain.Page;
import work.order.system.entity.dongman.DongMan;
import work.order.system.entity.dongman.DongManRelation;
import java.util.List;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月27日 9:46
*/
public interface DongManService {
/**
* TODO(添加对象)
* @author zhaoPeng
* @date 2021/12/28
* @param dongMan
* @return work.order.system.entity.dongman.DongMan
*/
DongMan addDongMan(DongMan dongMan);
/**
* TODO(根据id查询对象)
* @author zhaoPeng
* @date 2021/12/28
* @param id
* @return work.order.system.entity.dongman.DongMan
*/
DongMan getInfoById(long id);
/**
* TODO(根据id删除对象)
* @author zhaoPeng
* @date 2021/12/28
* @param id
* @return void
*/
void delById(long id);
/**
* TODO(指定已存在的2个对象间的关系)
* @author zhaoPeng
* @date 2021/12/28
* @param from
* @param relation
* @param to
* @return void
*/
void createRelation(String from,String relation, String to);
/**
* TODO(生成指定节点的关系,基于关系数据生成)
* @author zhaoPeng
* @date 2021/12/28
* @param fromName
* @return void
*/
void createRelationByName(String fromName);
/**
* TODO(分页查询动漫人物信息[0为第一页])
* @author zhaoPeng
* @date 2021/12/28
* @param current
* @param pageSize
* @param Name
* @return org.springframework.data.domain.Page<work.order.system.entity.dongman.DongMan>
*/
Page<DongMan> getListByPage(int current, int pageSize, String Name);
/**
* TODO(获取数据库中所有的关系类型)
* @author zhaoPeng
* @date 2021/12/28
* @param
* @return java.util.List<java.lang.String>
*/
List<String> getAllRealationTypes();
/**
* TODO(判定是否存在该用户)
* @author zhaoPeng
* @date 2021/12/28
* @param id
* @return java.lang.Boolean
*/
Boolean existById(long id);
/**
* TODO(修改节点信息)
* @author zhaoPeng
* @date 2021/12/28
* @param dongMan
* @return work.order.system.entity.dongman.DongMan
*/
DongMan updateById(DongMan dongMan);
//查询指定用户的所有关系
List<DongMan> getRelationsByName(String name,String relation);
}
创建Dao
dao操作仓库代码其中有自定义操作集合Mybatis自定义SQL执行类似:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import work.order.system.entity.dongman.DongMan;
import work.order.system.entity.dongman.DongManRelation;
import java.util.List;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月27日 9:09
*/
@Repository
public interface DongManRepository extends Neo4jRepository<DongMan,Long> {
//一对一手动指定关系
@Query("match (n:dongMan {name:{0}}),(m:dongMan {name:{2}})"+
"create (n)-[:动漫人物关系{relation:{1}}]->(m)")
void createRelation(String from,String relation, String to);
//根据关系数据进行当前用户的所有关系生成
@Query("match (n:dongMan {name:{0}}),(m:dmRelation),(s:dongMan) where m.from={0} and s.name=m.to create(n)-[:动漫人物关系 {relation:m.relation}]->(s)")
void createRelationByName(String fromName);
//根据关系数据进行当前用户的所有关系生成
@Query("CALL db.relationshipTypes()")
List<String> getAllRealationTypes();
//修改
@Query("MATCH (n) WHERE id(n) = :#{#dongMan.id} SET n.name = :#{#dongMan.name},n.age = :#{#dongMan.age},n.sex = :#{#dongMan.sex} RETURN n")
DongMan updateById(@Param("dongMan") DongMan dongMan);
@Query("match (n:dongMan {name:{name}})-[r:`动漫人物关系`]->(m:dongMan) where r.relation={relation} return m")
List<DongMan> getRelationsByName(@Param("name")String name,@Param("relation")String relation);
@Query("MATCH (n:dongMan {name:'冯宝宝'}) RETURN n")
DongMan getTest();
}
创建ServiceImpl
需要注意的是,目前springboot-data-neo4j中的分页首页是从0开始的,不是从1开始的,很多人估计在这个地方郁闷了很久,查询第一页数据时输入1的页码,结果看不到数据。就是因为这个差别导致的!!!
接口实现层代码如下:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import work.order.system.dao.doc.DocSqlMapper;
import work.order.system.dao.dongman.DongManRepository;
import work.order.system.entity.doc.DocSql;
import work.order.system.entity.dongman.DongMan;
import work.order.system.entity.dongman.DongManRelation;
import work.order.system.service.DongManService;
import javax.annotation.Resource;
import java.util.List;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月27日 9:49
*/
@Service
@Transactional(value="transactionManager")
public class DongManServiceImpl implements DongManService {
@Resource
DongManRepository dongManRepository;
@Override
public DongMan addDongMan(DongMan dongMan) {
return dongManRepository.save(dongMan);
}
@Override
public DongMan getInfoById(long id) {
return dongManRepository.findById(id).get();
}
@Override
public void delById(long id) {
dongManRepository.deleteById(id);
}
@Override
public void createRelation(String from, String relation, String to) {
dongManRepository.createRelation(from,relation,to);
}
@Override
public void createRelationByName(String fromName) {
dongManRepository.createRelationByName(fromName);
}
@Override
public Page<DongMan> getListByPage(int current, int pageSize, String Name) {
Pageable pageable= PageRequest.of(current,pageSize);
return dongManRepository.findAll(pageable);
}
@Override
public List<String> getAllRealationTypes() {
return dongManRepository.getAllRealationTypes();
}
@Override
public Boolean existById(long id) {
return dongManRepository.existsById(id);
}
@Override
public DongMan updateById(DongMan dongMan) {
return dongManRepository.updateById(dongMan);
}
@Override
public List<DongMan> getRelationsByName(String name,String relation) {
return dongManRepository.getRelationsByName(name,relation);
}
}
创建Controller
提供服务访问入口进行服务消费
控制层代码如下:
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import work.order.system.entity.dongman.DongMan;
import work.order.system.entity.Result;
import work.order.system.service.DongManService;
import javax.annotation.Resource;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月27日 9:23
*/
@RestController
@RequestMapping("/dongMan")
@Api(tags="动漫人物操作测试")
public class DongManController {
@Resource
DongManService dongManService;
@PostMapping("/addDongMan")
@ApiOperation(value="添加对象节点")
public Result addDongMan(DongMan dongMan) {
return Result.success("操作成功!", dongManService.addDongMan(dongMan));
}
@PostMapping("/delById")
@ApiOperation(value="根据主键删除")
@ApiImplicitParam(name="id",value="主键",paramType="form")
public Result delById(Long id) {
dongManService.delById(id);
return Result.success("操作成功!");
}
@PostMapping("/updateDongMane")
@ApiOperation(value="修改节点信息")
public Result updateDongMane(DongMan dongMan) {
return Result.success("操作成功!", dongManService.updateById(dongMan));
}
@GetMapping("/getInfoById")
@ApiOperation(value="根据主键查询")
@ApiImplicitParam(name="id",value="主键",paramType="form")
public Result getInfoById(Long id) {
return Result.success("操作成功!",dongManService.getInfoById(id));
}
@GetMapping("/getAllRelationTypes")
@ApiOperation(value="获取所有的关系类型")
public Result getAllRelationTypes() {
return Result.success("操作成功!", dongManService.getAllRealationTypes());
}
@PostMapping("/addDMRelationShip")
@ApiOperation(value="指定两个节点的关系(两个节点须存在)")
@ApiImplicitParams({
@ApiImplicitParam(name="name",value="对象名:唐三-",paramType="form"),
@ApiImplicitParam(name="relation",value="关系[父亲]->",paramType="form"),
@ApiImplicitParam(name="to",value="对象名:唐昊",paramType = "form")
})
public Result addDMRelationShip(String name,String relation,String to) {
//直接指定关系
dongManService.createRelation(name, relation, to);
return Result.success("操作成功!" );
}
@GetMapping("/getRelationsByName")
@ApiOperation(value="获取指定节点指定关系信息")
@ApiImplicitParams({
@ApiImplicitParam(name="name",value="对象名:唐三",paramType="form"),
@ApiImplicitParam(name="relation",value="具体关系",paramType = "form")
})
public Result getRelationsByName(String name,String relation) {
return Result.success("操作成功!" , dongManService.getRelationsByName(name,relation));
}
}
效果演示
运行项目进行接口操作验证,我这边使用swagger进行测试,(因为我数据库中已经有测试数据了,我们先看下数据库中的数据)。
源数据库中的数据如图:
共有18条记录,我们验证添加节点操作,新增武庚和逆天而行。如图:
创建节点时会根据对应实体上的注解指定的节点名进行创建(也可以先手动在数据库中创建好节点)。
创建节点指令:
create (n:dongMan {name:'李四',sex:'男',age:'22'})
数据库中如图:
其他的修改和删除操作就不一一演示了,下面演示下给这两个节点添加关系。
基础的操作就实现了。
4、 自定义分页实现
在controller中添加如下代码:
@GetMapping("/getListByPage")
@ApiOperation(value="分页查询")
@ApiImplicitParams({
@ApiImplicitParam(name="current",value="起始页0为第一页",paramType="form"),
@ApiImplicitParam(name="pageSize",value="页数量",paramType = "form")
})
public Result getListByPage(int current,int pageSize) {
return Result.success("操作成功!",dongManService.getListByPage(current,pageSize,""));
}
在swagger中查看效果
该分页实现为源码中的PagingAndSortingRepository仓库实现的,但是源码中只有一个排序操作没有关于参数指定的分页查询实现,即需要我们自己手动实现自定义分页操作。
自定义分页实现service加入如下代码:
/**
* TODO(查看当前用户的关系)
* @author zhaoPeng
* @date 2021/12/29
* @param name
* @return java.util.List<work.order.system.entity.dongman.DongMan>
*/
Page<DongManRelation> getRelationsByName(int current, int pageSize,String name);
dao加入如下代码:
@Query(value="match (n:dongMan {name:{name}})-[r:`动漫人物关系`]->(m:dongMan) return id(n) as pid, n.name as name,r.relation as relation" +
",m as dongMan skip {skip} limit {pageSize}"
,countQuery = "match (n:dongMan {name:{name}})-[r:`动漫人物关系`]->(m:dongMan) return count(r)")
Page<DongManRelation> getRelationsByName(@Param("name")String name,@Param("skip")int ship,@Param("pageSize")int pageSize,Pageable pageable);
//不传递pageable分页无效
对应的CQL需要指定skip和limit的值一级获取总条数的SQL,且必须接收Pageable。
serviceImpl加入如下代码:
@Override
public Page<DongManRelation> getRelationsByName(int current, int pageSize,String name) {
Pageable pageable= PageRequest.of(current,pageSize);
return dongManRepository.getRelationsByName(name,current*pageSize,pageSize,pageable);
}
controller加入如下代码:
@GetMapping("/getRelations")
@ApiOperation(value="获取指节点关系信息")
@ApiImplicitParams({
@ApiImplicitParam(name="name",value="对象名:唐三",paramType="form"),
@ApiImplicitParam(name="current",value="起始页0为第一页",paramType="form"),
@ApiImplicitParam(name="pageSize",value="页数量",paramType = "form")
})
public Result getRelations(int current,int pageSize,String name) {
return Result.success("操作成功!" , dongManService.getRelationsByName(current,pageSize,name));
}
效果如图:
这样就满足了自定义条件分页查询的实现了。
5、 集成MySQL+Mybatis-Plus
pom关键添加如下信息:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
application.yml添加如下信息:
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxxxxxx:3306/xxx?useUnicode=true&characterEncoding=utf-8
username: root
password: xxxx
#mybatis-plus设置
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml # Mapper文件的位置
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志的实现类(打印SQL)
map-underscore-to-camel-case: true # 下划线转驼峰
添加好如上信息后直接按照SpringBoot集成Mybatis-plus的crud操作实现即可进行mysql数据库的操作,无需进行数据源切换(目前未明确为何可自行寻找到数据源进行数据库操作,欢迎大家留言讨论)。这里就直接演示MySQL的操作了,如图:
注意哈:mybatis的分页首页为1,neo4j的首页为0。
6、多数据源事务处理
在实际的操作中很有可能用到事务的回滚功能,集成了MySQL和neo4j数据库后,只需对事务进行简单配置即可实现。
首先启用事务,在启动类上添加@EnableTransactionManagement注解。因为集成了不同的数据库,直接启用事务,系统无法辨别具体的回滚操作在那个数据库执行,我的实现如下,添加事务配置类:
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
/**
* @className:
* @author: zhaopeng
* @description: TODO(......)
* @date: 2021年12月29日 10:43
*/
@Configuration
public class TransactionConfig {
/**
* TODO(mySQL数据库事务,根据数据源控制)
* @author zhaoPeng
* @date 2021/12/29
* @param dataSource
* @return org.springframework.jdbc.datasource.DataSourceTransactionManager
*/
@Bean("mysqlTransaction")
public DataSourceTransactionManager jpaTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* TODO(neo4j数据库事务操作 避坑所在[bean必须是这个名字transactionManager])
* @author zhaoPeng
* @date 2021/12/29
* @param driver
* @return org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager
*/
@Bean("transactionManager")
public Neo4jTransactionManager neo4jTransactionManager(Driver driver) {
return new Neo4jTransactionManager(driver);
}
}
此处一定要注意neo4j的事务必须命名为transactionManager,否则会出错。这个地方坑了很长时间。
在对应的serviceImpl类或者具体的实现方法上加入事务注解,指定使用那个事务进行实现。依据数据库进行区分,
mysql数据库操作的添加@Transactional(value=“mysqlTransaction”)
neo4j数据库操作的添加@Transactional(value=“transactionManager”)
这样就可以实现事务的回滚操作了。我们在DongManServiceImpl类中添加异常操作,验证事务是否生效。
修改原有addDongMan方法为:
@Override
public DongMan addDongMan(DongMan dongMan) {
//正常操作
dongManRepository.save(dongMan);
//异常操作
dongManRepository.getTest();
return dongManRepository.save(dongMan);
}
修改DongManRepository中的getTest接口的CQL为错误的CQL:
@Query("MATCH (n:dongMan {name:冯宝宝}) RETURN n")
DongMan getTest();
进行操作验证,添加一个CSDN的节点,添加成功则事务无效,因为添加成功操作后面有一个异常操作,事务应该回滚才正确。
后台异常如图:
数据库中查看是否添加CSDN成功:
说明neo4j的事务生效了,mysql的事务验证就不演示了,这里再演示下neo4j操作成功,mysql操作失败的数据库混合操作,看事务是否生效:
修改DongManServiceImpl中的addDongMan,引入mysql的数据库操作。代码如下:
//模拟混合数据源事务管理
@Resource
DocSqlMapper sqlMapper;
@Override
public DongMan addDongMan(DongMan dongMan) {
//正常操作
dongManRepository.save(dongMan);
//异常操作
DocSql docSql2 = sqlMapper.selectById("3");
docSql2.setDocId(null);
docSql2.setCreateTime("111");
sqlMapper.updateById(docSql2);
return dongMan;
}
运行查看事务是否生效:
MySQL操作失败,neo4j操作成功,但是2个操作在一个方法中,事务回滚,判定为操作失败,数据库不进行操作成功的数据写入。混合式事务验证成功。
大家不喜勿喷!