SpringDataNeo4j实现分页查询和使用Session的方法进行复杂查询

前言

项目框架

我的项目采用的是SpringBoot集成SpringDataNeo4j和MyBatisPlus的框架,接口测试使用Swagger,MyBatisPlus当时是搭建项目想尝试配两种数据库做的测试,目前还并没有用到.但是看了一些参考资料,大家都比较建议将不需要在Neo4j中查询时使用的的属性放到MySQL等其他数据库中,这个工作可能日后再优化吧.

版本说明:

  • SpringBoot 2.1.6
  • SpringDataNeo4j 5.1.9
  • 本机安装的Neo4j 3.5.6

项目配置(依赖)

依赖添加和项目配置很多教程都有,我就不赘述了.值得一提的是,我当时配置项目用到了这个依赖,jdbc的驱动,如果不加这个,当时项目启动时候会报错,因为在配置类中有一个bean通过这种连接方式来启动并获取SessionFactory的.

		<dependency>
            <groupId>org.neo4j</groupId>
            <artifactId>neo4j-jdbc-driver</artifactId>
            <version>3.4.0</version>
        </dependency>

另外,neo4j默认的依赖是使用http连接方式的,要是用bolt的连接方式的话,需要添加bolt驱动,也就是下面这个依赖

 		<dependency>
            <groupId>org.neo4j</groupId>
            <artifactId>neo4j-ogm-bolt-driver</artifactId>
            <version>3.1.12</version>
        </dependency>

配置类

这里再贴一下我自己的Neo4jConfig,下文中有地方会用到.
可以看到我创建了2个bean,一个是连接Neo4j数据库的,返回一个SessionFactory,一个是Neo4j事务的

package com.jytd.sdntest.common.config;

import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.beans.factory.annotation.Value;
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.data.neo4j.transaction.SessionFactoryUtils;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableNeo4jRepositories(basePackages = "com.jytd.sdntest.sdn.repository")
@EnableTransactionManagement
public class Neo4jConfig{
    @Value("${spring.data.neo4j.uri}")
    private String databaseUrl;

    @Value("${spring.data.neo4j.username}")
    private String userName;

    @Value("${spring.data.neo4j.password}")
    private String password;

    @Bean
    public SessionFactory sessionFactory() {
        org.neo4j.ogm.config.Configuration configuration = new org.neo4j.ogm.config.Configuration.Builder()
                .uri(databaseUrl)
                .credentials(userName, password)
                .build();
        SessionFactory sessionFactory=new SessionFactory(configuration, "com.jytd.sdntest.sdn");
        return sessionFactory;
}

    @Bean
    public Neo4jTransactionManager transactionManager() {
        return new Neo4jTransactionManager(sessionFactory());
    }


}

分页查询

官方文档及实现方式

由于一开始尝试着写项目,对于分页都是直接skip然后limit的,但是返回结果只有数据的集合,不包含分页的属性,所以正好那天因为一些问题翻看官方说明文档,看到了分页查询的使用方法.官方文档地址在文章最后的参考资料里贴出.这里稍微写一下
下面代码复制自官方文档

	// returns a Page of Actors that have a ACTS_IN relationship to the movie node
	// with the title equal to movieTitle parameter with an accurate total count
    @Query(value = "MATCH (movie:Movie {title={0}})<-[:ACTS_IN]-(actor) RETURN actor", 
    countQuery = "MATCH (movie:Movie {title={0}})<-[:ACTS_IN]-(actor) RETURN count(*)")
    Page<Actor> getActorsThatActInMovieFromTitle(String movieTitle, Pageable page);

我按照这种写法自己也尝试写了一个,value属性是要查询的cypher,无须写SKIPLIMIT,countQuery属性是查询总数的查询语句,语句是一样的,返回结果是count()而已

	//根据学生ID查询学生-实验,分页
    /*@Query(value = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)=:#{#student.id} RETURN e AS experimentNode",
            countQuery = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)=:#{#student.id} RETURN count(e)")*/
    @Query(value = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)={id} RETURN r",
            countQuery = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)={id} RETURN count(r)")
    Page<StudentExperimentRel> queryExperimentByStudentId(@Param("id") Long id, Pageable pageable);

参数说明

从上面的例子可以看到,分页查询的参数是查询条件和一个Pageable,即分页参数,查询参数没什么好说的,需要几个传几个就是了.
Pageable的话就是分页参数,需要传的属性是page,size,sort.之前没用过SpringData,所以也没用过Pageable这个接口.看了一下它的实现类常用的是PageRequest
所以,用如下方法创建一个PageRequest对象.

Pageable pageable=PageRequest.of(页码,每页条数,排序方式(可不要));

特别提醒:这个分页的页码是 从0开始的!从0开始的!从0开始的! 之前用MyBatisPlus都是从1开始的,计算起始也是(page-1)*limit这么算的,所以我之前page传的1说什么查不出数据!困扰好久!
service层的写法:

	@Override
    public Page<StudentExperimentRel> queryExperimentByStudentId(StudentQo studentQo) {
        Pageable pageable=PageRequest.of(studentQo.getPage(),studentQo.getSize());
        Page<StudentExperimentRel> relationPage=experimentRepository.queryExperimentByStudentId(studentQo.getId(),pageable);
        return relationPage;
    }

返回值说明

按如上方式查询之后,返回的是一个Page接口,这个接口的实现类是PageImpl,下面自定义复杂查询会用到.我们来看一下这个接口返回的对象属性,分析一下吧!因为是从controller的返回值复制过来的,所以以下内容是Json数据

{
  "status": "OK",
  "code": 0,
  "message": "成功响应",
  "data": {
    "content": [//content这个key,对应的就是查询结果,是个数组
      {
        "id": 337,//这里每一个对象都是我的一个RelationshipEntity,并且包含开始节点和结束节点
        "startId": null,
        "endId": null,
        "score": null,
        "code": null,
        "result": null,
        "graph": null,
        "submitTime": null,
        "scoreTime": null,
        "isFinished": null,
        "studentNode": {//开始节点
          "id": 159,
          "name": "张三",
          "gender": null,
          "profession": null,
          "username": null,
          "password": null,
          "createTime": null,
          "state": null,
          "courseRel": []
        },
        "experimentNode": {//结束节点
          "id": 276,
          "name": "实验04",
          "purpose": null,
          "content": null,
          "procedure": null,
          "courseId": null
        }
      },
      {
        "id": 335,
        "startId": null,
        "endId": null,
        "score": null,
        "code": "修改测试",
        "result": null,
        "graph": null,
        "submitTime": null,
        "scoreTime": null,
        "isFinished": null,
        "studentNode": {
          "id": 159,
          "name": "张三",
          "gender": null,
          "profession": null,
          "username": null,
          "password": null,
          "createTime": null,
          "state": null,
          "courseRel": []
        },
        "experimentNode": {
          "id": 309,
          "name": "实验添加测试",
          "purpose": null,
          "content": null,
          "procedure": null,
          "courseId": null
        }
      }
],
    "pageable": { //这个pageable就是我查询时候的那个参数
      "sort": {//排序条件,如果参数传了分页相关的,应该会不一样
        "sorted": false,
        "unsorted": true,
        "empty": true
      },
      "offset": 0,
      "pageNumber": 0,//这个就是页码,从0开始
      "pageSize": 5,//这是每页记录条数
      "unpaged": false,
      "paged": true
    },
    //这里往下就是Page这个接口所含的分页属性,前端取值取这个应该更方便
    "totalElements": 2,//总记录条数
    "totalPages": 1,//总页数
    "last": true,
    "number": 0,//这里跟上面pageable里面是一样的,页码
    "size": 5,//每页记录条数
    "sort": {
      "sorted": false,
      "unsorted": true,
      "empty": true
    },
    "numberOfElements": 2,
    "first": true,
    "empty": false
  },
  "date": 1566197748209
}

存在的问题

存在的问题其实就是参数的问题,可以翻看我前面的博客,在@Query里面可以使用:#{#对象名.属性名}的方式来传一个对象类型的参数,用这种写法获取到对象中的属性作为参数,但是这种写法在@Query(value="xxxxx")里面可以用,但是在countQuery中,这种写法是不行的,之前因为页码问题尝试debug了一次源码,在第一句中,这种写法就被编译为了{str_exp1}大概这样的,具体啥样记不清了.但第二句的时候不编译!!!仍然是#{#对象名.属性名}!特别坑!所以当使用这种方式分页查询时,只能一个一个的传参数进去,用最简单的{参数名}来匹配参数

拼接CQL实现复杂查询

在Repository中使用@Query来进行Cyther查询虽然方便,但是不能像MyBatis一样作条件判断,SpringDataJPA中是可以的,它在@Query中可以使用if(),同时还有一个nativeQuery = true的属性,SpringDataNeo4j没有…所以当复杂的需要根据条件拼接Cypher语句的情况下,我们只能自己拼接再使用其他方法来将语句进行执行.

Session方法实现

SpringDataNeo4j的session中有save,delete,query等很多方法,具体可以自行查看Session接口中定义的方法!
所以首要要解决的问题,只要解决如何获取到这个Session就可以了.
之前参考了一个朋友的博客( 链接在这里)他用的是这种方式:

Session session = SessionFactoryUtils.getSession(sessionFactory);

这种方式没问题,官方API文档也写了这些方法.可是我用这种方式,获取到的session一直是null,看了源码也没看出是为啥,我觉得可能跟spring自带的事务管理器有关,不知如何解决就算了.
使用了SessionFactory来获取session,由于在前面的Neo4jConfig里添加了一个SessionFactory的bean,所以在service等其他地方直接@Autowired即可,然后调用openSession()方法即可获取Session.

	@Autowired
    private SessionFactory sessionFactory;
    
    //在方法里直接获取即可
    Session session=sessionFactory.openSession();

下面贴一下我Service的代码,根据controller的参数进行拼接Cypher并执行.(不相关的方法删掉了)

package com.jytd.sdntest.ssm.node.service.impl;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.jytd.sdntest.common.utils.Util;
import com.jytd.sdntest.sdn.node.domain.ExperimentNode;
import com.jytd.sdntest.sdn.node.domain.StudentNode;
import com.jytd.sdntest.sdn.node.model.ExperimentQo;
import com.jytd.sdntest.sdn.node.model.StudentQo;
import com.jytd.sdntest.sdn.node.repository.ExperimentRepository;
import com.jytd.sdntest.sdn.relation.domain.StudentExperimentRel;
import com.jytd.sdntest.ssm.node.service.ExperimentService;
import org.neo4j.ogm.driver.Driver;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.transaction.SessionFactoryUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Set;

@Service
public class ExperimentServiceImpl implements ExperimentService {

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public Page<ExperimentNode> queryExperimentByParam(ExperimentQo experimentQo) {
        StringBuffer cql=new StringBuffer();
        //拼接基本语句
        cql.append("MATCH (n:Experiment) WHERE 1=1");
        //按名称模糊查询
        if(Util.isNotEmpty(experimentQo.getName())){
            cql.append(" AND n.name=~'.*"+experimentQo.getName()+".*'");
        }
        //按学科查询
        if(Util.isNotEmpty(experimentQo.getSubject())){
            cql.append(" AND n.subject='"+experimentQo.getSubject()+"'");
        }
        //按分类查询
        if(Util.isNotEmpty(experimentQo.getSort())){
            cql.append(" AND n.sort='"+experimentQo.getSort()+"'");
        }
        //按难易程度查询
        if(Util.isNotEmpty(experimentQo.getDifficulty())){
            cql.append(" AND n.difficulty="+experimentQo.getDifficulty());
        }
        //按是否发布查询
        if(Util.isNotEmpty(experimentQo.getIsPublished())){
            cql.append(" AND n.isPublished="+experimentQo.getIsPublished());
        }
        //拼接返回值和Limit部分
        int skip=experimentQo.getPage()*experimentQo.getSize();
        String queryEnd=" RETURN n SKIP "+skip+" LIMIT "+experimentQo.getSize();
        String countEnd=" RETURN count(n)";

        //获取session
        Session session=sessionFactory.openSession();

        //分页数据查询结果
        Iterable<ExperimentNode> experimentNodes=session.query(ExperimentNode.class,cql.toString()+queryEnd,Maps.newHashMap());
        //查询总数
        Integer total=session.queryForObject(Integer.class,cql.toString()+countEnd, Maps.newHashMap());

        //封装为分页类
        List<ExperimentNode> content= Lists.newArrayList(experimentNodes);
        Pageable pageable=PageRequest.of(experimentQo.getPage(),experimentQo.getSize());
        Page<ExperimentNode> page=new PageImpl<ExperimentNode>(content,pageable,total);
        return page;
    }
}

这里返回分页类是用的Page的实现类PageImpl的构造方法,将查询结果,分页,总数作为参数即可.

APOC实现

APOC安装
  1. 根据Neo4j版本下载对应的APOC的jar包,
    地址在这里: https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases
    我的版本是3.5.6所以我下的是3.5.X

  2. 将jar包放到Neo4j目录下的plugins目录下
    在这里插入图片描述

  3. 修改conf/neo4j.conf的配置(可选)

dbms.security.procedures.unrestricted=apoc.*
#增加页缓存到至少4G,推荐20G:
dbms.memory.pagecache.size=4g
#JVM堆保存留内存从1G起,最大4G:
dbms.memory.heap.initial_size=1g
dbms.memory.heap.max_size=4G
  1. 重新启动neo4j服务,在7474的web页面执行RETURN apoc.version();如果返回版本号说明安装成功
调用存储过程

配置成功之后,我们就可以在项目中的Repository调用存储过程了.写法很简单
首先按上面的方法拼接一个CQL,作为参数传到Repository中
然后定义一个这样的接口方法,将cql作为从参数传入,调用apoc.cypher.run()

	@Query("call apoc.cypher.run({cql}, null) yield value return value.n")
    Set<TeamNode> teamTest(@Param("cql") String cql);

就可以查到返回结果了.
这里需要注意的是,调用之后后面的YIELD value RETURN value这里,如果是这样写,返回的是这样的结果,不能映射到我定义的集合泛型上去.
在这里插入图片描述
可以看到返回的是一个value,value里面对应的是{“n”:节点}的对象,可是我们要的只是这个n的集合,所以返回这里,要写成YIELD value RETURN value.n
在这里插入图片描述
这样返回的就是我们想要的结果,也能映射到NodeEntity上了.
之后再像Session的方法一样,查询总数,封装分页类即可.

参考资料

SpringDataNeo4j官方文档及API:https://spring.io/projects/spring-data-neo4j#learn
APOC说明文档:https://neo4j-contrib.github.io/neo4j-apoc-procedures/#_virtual_nodes_rels

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 1. 使用 `java.sql.Statement` 类的 `setMaxRows` 和 `setFetchSize` 方法来设置每页的记录数,然后使用 `java.sql.ResultSet` 类的 `absolute` 方法来跳转到指定页。 2. 使用 `java.sql.PreparedStatement` 类的 `setMaxRows` 和 `setFetchSize` 方法来设置每页的记录数,然后使用 `java.sql.ResultSet` 类的 `absolute` 方法来跳转到指定页。 3. 使用 `SELECT` 语句的 `LIMIT` 和 `OFFSET` 子句来限制查询返回的行数。 4. 使用 `java.sql.Statement` 或 `java.sql.PreparedStatement` 对象的 `executeQuery` 方法执行带有 `LIMIT` 和 `OFFSET` 子句的 `SELECT` 语句,然后使用 `java.sql.ResultSet` 类的 `next` 方法来遍历查询结果。 5. 使用数据库的特定功能(如 MySQL 的 `LIMIT` 子句)来实现分页。这种方法实现方式取决于数据库的类型。 ### 回答2: Java实现分页查询的几种常用方法有: 1.使用SQL语句的LIMIT关键字:在查询数据库时,可以使用LIMIT关键字来指定查询结果的起始位置和数量。通过计算页号PageNum和每页数量PageSize,可以得到SQL语句中的LIMIT子句,从而实现分页查询。 2.使用MyBatis的分页插件:MyBatis是一种优秀的持久化框架,在其配置文件中可以配置分页插件,并使用插件提供的分页功能完成分页查询。通过在Mapper接口中定义查询方法,并在配置文件中配置插件,可以方便地实现分页逻辑。 3.使用Spring Data JPA的分页查询Spring Data JPA是一种简化数据库操作的框架,可以通过定义Repository接口来实现数据的增删改查操作。在查询方法中,可以使用Pageable参数来指定查询结果的起始位置和数量,从而实现分页查询。 4.使用PageHelper插件:PageHelper是一种基于MyBatis的分页插件,提供了丰富的分页查询功能。在查询方法中,可以通过调用PageHelper.startPage方法来指定页号和每页数量,PageHelper会自动在SQL语句中添加LIMIT子句,从而实现分页查询。 5.使用自定义工具类:可以自定义一个工具类来实现分页查询逻辑。通过传入查询结果集合、页号和每页数量,可以在工具类中进行计算,得到分页查询的结果集合。 以上是Java实现分页查询的几种常用方法,根据项目的需求和使用的框架,可以选择适合的方法实现分页功能。 ### 回答3: Java实现分页查询有多种方法,以下是几种常见的实现方式: 1. 使用数据库的分页查询:在SQL语句中使用LIMIT关键字,通过设置偏移量和每页显示的数据条数,实现分页效果。例如,使用MySQL数据库时,可以通过"SELECT * FROM table_name LIMIT offset, limit"语句来实现分页查询。 2. 使用集合分页:将查询结果全部加载到内存中的集合中,然后通过对集合进行切片操作,截取指定页的数据,实现分页效果。常见的集合类如List、ArrayList等都提供了subList方法,可以方便地实现分页。 3. 使用ORM框架:ORM(Object-Relational Mapping)框架如Hibernate、MyBatis等,提供了丰富的分页查询功能。通常只需在查询方法中传入页码和每页显示的数据数量,框架会自动生成对应的分页查询SQL语句,并返回分页查询结果。 4. 使用第三方库:有一些开源的分页查询库可以直接使用,如PageHelper、Spring Data JPA等。这些库提供了简单易用的API,可以快速实现分页查询功能。 需要注意的是,无论使用哪种方法实现分页查询,都要考虑性能和数据量。当处理大数据量时,应尽量减少数据库查询次数,避免一次性加载全部数据;同时,合理使用缓存、索引等技术,提高查询效率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值