Bootstrap

【SpringBoot+Neo4j+MySQL+Mybatis-Plus集成和操作实现】

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个操作在一个方法中,事务回滚,判定为操作失败,数据库不进行操作成功的数据写入。混合式事务验证成功。

大家不喜勿喷!

;