酷帅狂拽吊炸天——一统江湖的持久层框架之SpringData

本文深入讲解了SpringData框架如何简化数据访问,涵盖SpringDataJpa、SpringDataRedis与MongoDB操作,从CRUD到复杂查询、多表操作和高级特性如分页、SPEL和管道技术。
摘要由CSDN通过智能技术生成

一、SpringData概述

1.1 现有问题

​ 随着互联网的发展,互联网产品的复杂度越来越高,在开发中使用到的数据存储产品不再仅限于关系型数据库,还会使用到Redis、MongoDB、Elasticsearch、Solr等不同应用场景的数据库系统。而这些数据库产品,在Java中也都提供了优秀的访问技术,如Mybatis、Jedis等等。这些技术虽然对相应的数据库提供很便捷的操作方式,但是由于不同的持久层技术的api是不一样的,开发人员就必须同时掌握多种数据访问技术,增加了学习和开发成本。

​ 那么,是否有这么一种技术,它可以使用同一套API去操作不同的数据库产品?SpringData就是在这种需求下产生的。

1.2 SpringData简介

​ SpringData是一个简化dao层开发的框架,它提供了一套统一的数据访问api,使得开发者在不需要额外学习成本的前提下,能够操作不同的数据库产品。

​ SpringData支持的数据库非常多,包括mysql、elasticsearch、solr、mongodb、redis、hbase等等。

SpringData是一整个家族,包含了诸多的技术点,现在市面上提到SpringData就认为是指SpringDataJpa,这是错误的。

二、SpringDataJpa

2.1 概念

2.1.1 jpa

​ Jpa全称是 Java Persistence Api,即java持久化api,是sun公司推出的一套基于ORM的规范,其并未提供具体的实现,因此并不是框架。常见的实现有 SpringDataJpa、Hibernate-jpa、Mybatis-jpa

Jpa只是一个规范,而非框架,但是在我们日常的开发和交流中,会直接将 jpaSpringDataJpa 划等号,这是因为SpringDataJpa是诸多jpa实现中最有名的一种,因此大多数开发者会认为jpa就是SpringDataJpa。这在我们日常的开发交流中并不会有问题,但是在更严谨的场合仍然有必要指出二者的差别。

2.1.2 SpringDataJpa

​ SpringDataJPA是SpringData家族的一个成员,是Spring Data对JPA封装之后的产物,目的在于简化基于JPA的数据访问技术。使用SpringData JPA技术之后,开发者只需要声明Dao层的接口,不需要再写实现类 。

2.2 快速上手

​ 下面我们以一套CRUD为案例来快速上手Jpa,这里我们直接使用 SpringBoot 项目进行开发。

2.2.1 环境搭建

pom.xml
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!--引入web模块是为了使用jackson,后面学习redis需要使用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
实体类创建

/**
 * @Author: 杨德石
 * @Date: 2021/1/31 16:12
 * @Version 1.0
 */
// 标识这是个实体类
@Entity
// 对应表
@Table(name = "article")
public class Article implements Serializable {

    // 声明为主键
    @Id
    /**
     * 主键生成策略,有四个取值
     * TABLE:使用一个特定的数据库表格来保存主键。
     * SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
     * IDENTITY:主键由数据库自动生成(主要是自动增长型)
     * AUTO:主键由程序控制。
     */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
     * 声明类的属性和表字段对应,属性名如果一致可省略
     */
    @Column(name = "title")
    private String title;

    private String content;
    
    private Date createTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

这里的@Column注解是属性比较多的注解,属性和作用如下

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    // 定义了被标注字段在数据库表中所对应字段的名称;
    String name() default "";

    // 表示该字段是否为唯一标识,默认为false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用@Table标记中的@UniqueConstraint。
    boolean unique() default false;

    // 表示该字段是否可以为null值,默认为true。
    boolean nullable() default true;

    // 表示在使用“INSERT”脚本插入数据时,是否需要插入该字段的值。
    boolean insertable() default true;

    // 表示在使用“UPDATE”脚本插入数据时,是否需要更新该字段的值。insertable和updatable属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。
    boolean updatable() default true;

    // (使用率极低)表示创建表时,手动指定该字段创建的SQL语句,一般用于通过Entity生成表定义时使用。(也就是说,如果DB中表已经建好,该属性没有必要使用。)
    String columnDefinition() default "";

    // 表示当映射多个表时,指定表的表中的字段。默认值为主表的表名。
    String table() default "";

    // 表示字段的长度,当字段的类型为varchar时,该属性才有效,默认为255个字符。
    int length() default 255;

    // precision属性和scale属性表示精度,当字段类型为double时,precision表示数值的总长度,scale表示小数点所占的位数。
    int precision() default 0;

    int scale() default 0;
}
Repository接口创建

​ 使用SpringDataJpa操作数据库,只需要按照框架的要求提供Dao接口,指定实体泛型和主键泛型即可,不需要实现类。

​ SpringDataJpa中对于接口的要求有两点

  • 继承自JpaRepository和JpaSpecificationExecutor接口。前者为我们定义了一些简单的数据库操作API,后者主要是复杂sql的API。
  • 提供实体类泛型和主键泛型
/**
 * @Author: 杨德石
 * @Date: 2021/1/31 16:19
 * @Version 1.0
 */
public interface ArticleRepository extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article> {
}

application.yml

​ 下面是在配置文件中进行配置,除了SpringDataJpa的特定配置外,我们还需要配置数据源。

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sd?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
    username: root
    password: yangdeshi
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    show-sql: true # 显示sql
    hibernate:
      #create----每次运行该程序,没有表格会新建表格,表内有数据会清空
      #create-drop----每次程序结束的时候会清空表
      #update----每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
      #validate----运行程序会校验数据与数据库的字段类型是否相同,不同会报错
      ddl-auto: create
    # 数据库方言
    database-platform: org.hibernate.dialect.MySQL5Dialect

测试方法

​ 我们通过一个测试方法来演示 添加、修改、删除、根据id查询、查询所有 这五个操作

package com.jg.sd;

import com.jg.sd.pojo.Article;
import com.jg.sd.repository.ArticleRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Date;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/2/4 23:27
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class JpaTest {

    @Autowired
    private ArticleRepository articleRepository;

    @Test
    public void testSave() {
        Article article = new Article();
        article.setTitle("测试文章标题");
        article.setContent("测试文章内容");
        article.setCreateTime(new Date());
        articleRepository.save(article);
    }

    @Test
    public void testUpdate() {
        Article article = new Article();
        article.setId(1);
        article.setTitle("测试文章标题test");
        article.setContent("测试文章内容test");
        // save方法如果数据存在就是修改,如果不存在就是添加
        // save方法作为修改时是全字段修改,如果字段为空,就会置为null
        articleRepository.save(article);
    }

    @Test
    public void testGet() {
        Article article = articleRepository.findById(2).get();
        System.out.println(article);
    }

    @Test
    public void testFindAll() {
        List<Article> list = articleRepository.findAll();
        list.forEach(System.out::println);
    }

    @Test
    public void testDelete() {
        articleRepository.deleteById(1);
    }

}

2.3 SpringDataJpa运行机制

​ 通过上面的案例我们发现,SpringDataJpa使用起来极为方便,只需要提供个接口,然后继承指定的接口即可,那么SpringDataJpa底层是怎么执行的?为什么我们只需要提供接口,调用对应的API就能实现数据库的操作?我们来分析一下SpringDataJpa的运行原理。

继承关系

​ 首先我们通过 ArticleRepository 来查看UML图

image-20210131171043582

​ 这里我们需要关注的有这么几个接口:Repository、CrudRepository、PagingAndSortingRepository、JpaRepository

  • Repository 标记接口,继承该接口后会被Spring识别,进而能够在接口中按照一定的规范来定义方法,定义方法的方式我们后面再介绍。
  • CrudRepository 顾名思义,该接口实现了基本的增删改查方法。
  • PagingAndSortingRepository 该接口实现了分页和排序的方法
  • JpaRepository 该接口重写了几个查询和删除方法。

运行原理

​ 那么,通过这一系列的继承下来,每个接口都会为我们的ArticleRepository提供一部分功能,我们的dao接口就已经拥有了很多的方法,那这些方法是如何运行的,为什么不需要实现类就可以直接调用,我们继续向下分析。

​ 我们在任意的测试方法里打上断点,debug启动。我们发现在运行时,Spring会使用 JdkDynamicAopProxy 为我们的接口生成一个代理对象。

image-20210131171934344

​ 既然是生成了代理对象,那么这个对象是根据哪个类代理出来的呢?我们进入到 JdkDynamicAopProxy 类中,查看invoke方法,断点打在 targetSource 下面。我们发现代理的是SimpleJpaRepository

image-20210131172224507

​ 那么我们继续进入到 SimpleJpaRepository。直接定位到save方法,我们发现,这里使用到了一个叫 em 的成员属性,该属性是 EntityManager 对象。 EntityManager 是jpa规范中提供的类,说明SpringDataJpa只是对jpa标准进行了进一步的封装,最终还是需要执行jpa中的方法。

image-20210131172435478

2.4 复杂查询

2.4.1 分页和排序

​ 在我们的 findAll 方法中,有两个重载方法,分别接收 Sort 对象和 Pageable 对象,用来排序和分页。

代码演示

    @Test
    public void testFindPage() {
        // 注意,这里第一个参数传递的是页数,而非mysql的偏移量
        Pageable pageable = PageRequest.of(0, 20);
        Page<Article> page = articleRepository.findAll(pageable);
        System.out.println("总数:" + page.getTotalElements());
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("数据");
        page.getContent().forEach(System.out::println);
    }

    @Test
    public void testFindSort() {
        Sort sort = Sort.by(Sort.Order.desc("id"));
        List<Article> list = articleRepository.findAll(sort);
        list.forEach(System.out::println);
    }

    @Test
    public void testFindPageSort() {
        Sort sort = Sort.by(Sort.Order.desc("id"));
        Pageable pageable = PageRequest.of(0, 20, sort);
        Page<Article> page = articleRepository.findAll(pageable);
        System.out.println("总数:" + page.getTotalElements());
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("数据");
        page.getContent().forEach(System.out::println);
    }

    @Test
    public void testInitData() {
        for (int i = 4; i < 51; i++) {
            Article article = new Article();
            article.setTitle("测试文章标题" + i);
            article.setContent("测试文章内容" + i);
            article.setCreateTime(new Date());
            articleRepository.save(article);
        }
    }

2.4.2 方法命名规则查询

​ SpringData提供了一套方法命名的规范,只需要按照这个规范去定义方法名称,就能创建查询,而不需要提供具体的方法实现。SpringDataJpa在执行的时候会解析方法名,并自动生成查询语句进行查询。

​ 方法的命名规则以 findBy 开头,涉及到条件查询时,将条件对应的 属性 用条件关键字连接,其中属性首字母需要大写。

代码演示
package com.jg.sd.repository;

import com.jg.sd.pojo.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.List;

/**
 * SpringDataJpa中对接口的要求有两点
 * 1. 继承自JpaRepository和JpaSpecificationExecutor
 * 2. 提供实体类的泛型和主键泛型
 *
 * @Author: 杨德石
 * @Date: 2021/2/4 23:14
 * @Version 1.0
 */
public interface ArticleRepository extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article> {

    /**
     * 根据title查询
     * 等价于 where title = ?
     *
     * @param title
     * @return
     */
    List<Article> findByTitle(String title);

    /**
     * 根据title模糊查询
     * 等价于 where title like ?
     * 注意:这里是没有前后 % 的,我们需要把 % 作为参数传递过去
     * 这样其实是比较危险的,生产环境不建议使用
     *
     * @param title
     * @return
     */
    List<Article> findByTitleLike(String title);

    /**
     * 根据标题查询
     * 等价于 where title like '%?%'
     * @param title
     * @return
     */
    List<Article> findByTitleContaining(String title);

    /**
     * 根据 title 和 content 查询
     * 等价于 where title = ? and content = ?
     * @param title
     * @param content
     * @return
     */
    List<Article> findByTitleAndContent(String title, String content);

    /**
     * 等价于 where title like '%?%' or content like '%?%'
     * @param title
     * @param content
     * @return
     */
    List<Article> findByTitleContainingOrContentContaining(String title, String content);

    /**
     * 等价于 where id between ?1 and ?2
     * @param startId
     * @param endId
     * @return
     */
    List<Article> findByIdBetween(Integer startId, Integer endId);

}

方法命名规则表

​ SpringDataJpa的方法命名规则虽然很多,但是规则很简单,知道一种就可以举一反三。具体的命名规则如下表。

关键词示例对应的sql片段
AndfindByLastnameAndFirstname… where x.lastname = ? and x.firstname = ?
OrfindByLastnameOrFirstname… where x.lastname = ? or x.firstname = ?
Is,EqualsfindByFirstname
findByFirstnameIs
findByFirstnameEquals
… where x.firstname = ?
BetweenfindByStartDateBetween… where x.startDate between ? and ?
LessThanfindByAgeLessThan… where x.age < ?
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?
GreaterThanfindByAgeGreaterThan… where x.age > ?
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?
AfterfindByStartDateAfter… where x.startDate > ?
BeforefindByStartDateBefore… where x.startDate < ?
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?
NotLikefindByFirstnameNotLike… where x.firstname not like ?
StartingWithfindByFirstnameStartingWith… where x.firstname like concat(?, '%')
EndingWithfindByFirstnameEndingWith… where x.firstname like concat('%', ?)
ContainingfindByFirstnameContaining… where x.firstname like concat('%', ?, '%')
OrderByfindByAgeOrderByLastnameDesc… where x.age = ? order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?
InfindByAgeIn… where x.age in ?
NotInfindByAgeNotIn… where x.age not in ?
TruefindByActiveTrue… where x.active = true
FalsefindByActiveFalse… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?)

方法命名规则中支持别名,这是为了满足不同开发者的习惯,如 StartingWith 对应的 StartsWith,二者方法名有区别,但是功能上是一样的。

2.4.3 JPQL查询

​ 虽然我们使用SpringDataJpa提供的查询方法已经可以满足大部分的应用场景,但是对于某些业务来说还需要更加灵活的查询,而类似于统计等特别复杂的操作,更是需要手写SQL来完成,这时就可以使用 @Query 注解,结合 JPQL 来完成查询。

​ JPQL(Java Persistence Query Language)是JPA中定义的查询语言,该语言类似于sql,但是又有所区别,改语言目的是让开发者忽略数据库中的表和字段,而去关注实体以及属性,这点类似于 Hibernate 中的 HQL。它的写法类似于SQL,不同的是要将表名和列名换成实体类的类名和属性名。

快速上手

​ 因为JPQL的应用场景一定是查询操作,所以JPQL省略了 select *,写法如下。

    @Query("from Article")
    List<Article> selectAll();
? 参数占位符

​ 当我们需要传递参数查询时,可以使用 ? 占位符来进行,使用方式是 ?参数位置参数位置从1开始

    @Query("from Article where id > ?1 and title like %?2%")
    List<Article> selectByCondition(Integer id, String title);
: 参数占位符

​ 上面的写法解决了参数传递问题,但是使用下标位置去定位参数,代码可读性无疑是比较差的,那么怎么解决这个问题呢?jpa提供了 :参数名称 的方式,再结合 @Param 注解,就可以为参数命名,并取出对应的参数

    @Query("from Article where id > :id and title like %:title% order by createTime desc")
    List<Article> selectByConditionSorted(@Param("id") Integer id, @Param("title") String title);
    
SPEL表达式

​ 上面我们解决了参数可读性的问题,但是当我们参数变多时,即使我们使用了 : 占位符,代码的可读性依然很差。熟悉开发的同学可能就会想到,我们可以传递一个对象进去,这样直接取这个对象的属性就可以了。SpringDataJpa也为我们提供了这种传参方式,我们可以传递一个对象作为参数,然后使用 SPEL 表达式取出对象的属性作为JPQL的参数。

​ 格式:?#{} 括号中使用 [下标] 来取出指定位置的参数,如 ?#{[0]} 则取出第一个参数。之后直接取出参数中的指定属性即可,如 ?#{[1].title} 就是取出第二个参数的title属性。注意,这里的下标是从0开始的

    @Query("from Article where id > ?#{[0].id}")
    List<Article> selectBySpel(Article article);

​ 既然我们上面的 ? 占位符可以用 : 占位符替代,那么spel表达式能否这么操作呢?答案是可以的。我们在idea中使用上面的写法,idea会报红,很明显开发工具并不推荐我们这么写,因此我们依然可以结合 @Param 注解来取参数。

​ 格式::#{#参数名.属性名}。可以看出这种用法比 ? 占位符要简单的多了,也更容易理解。这种写法在idea中有代码提示,因此在实际开发中推荐采用这种写法。

    @Query("from Article where id > :#{#article.id}")
    List<Article> selectBySpelParam(@Param("article") Article article);

分页查询

​ JPQL的分页查询非常简单,只需要在参数中加上 Pageable 参数,SpringDataJpa就会自动给我们分页。

    /**
     * SPEL查询
     * 带分页
     *
     * @param article
     * @param pageable
     * @return
     */
    @Query("select a from Article a where a.id <= :#{#article.id} or a.title like %:#{#article.title}%")
    Page<Article> selectBySpelPage(@Param("article") Article article, Pageable pageable);

    @Query("from Article  where id <= :#{#article.id} or title like %:#{#article.title}%")
    List<Article> selectBySpelPageaa(@Param("article") Article article, Pageable pageable);

    @Query("select count(id) from Article  where id <= :#{#article.id} or title like %:#{#article.title}%")
    Integer countBySpelPageaa(@Param("article") Article article);

2.4.4 本地SQL查询

​ jpql已经满足绝大多数场景的查询需求,如果需要编写业务特别复杂的SQL,还可以使用原生的SQL进行查询。

@Query 注解有个 nativeQuery 属性,当设置为true时,表示这是个原生SQL语句,而非jpql语句,在这里我们需要按照sql的写法编写查询语句,而参数占位符依然遵循spel表达式。

    /**
     * 使用原生SQL查询
     * @param article
     * @param pageable
     * @return
     */
    @Query(value = "select * from article where id <= :#{#article.id} or title like '%:#{#article.title}%'", nativeQuery = true)
    Page<Article> selectBySpelPageSql(@Param("article") Article article, Pageable pageable);

2.4.5 Specifications查询

​ 有时候我们传递的参数是不固定的,这样我们就没法提供一个确定的SQL来填充参数。回想起mybatis的做法,mybatis提供了 <if> 标签,可以根据条件的真假决定是否要拼接上某个where条件,SpringDataJpa中也有类似的操作。

​ SpringDataJpa中可以通过 JpaSpecificationExecutor 接口查询,相比JPQL和SQL,其更加的面向对象,只是写起来可能会比较麻烦。

    @Test
    public void selectBySpecification() {
        Article article = new Article();
        article.setId(0);
        article.setTitle("5");
        List<Article> list = findBySpecification(article);
        list.forEach(System.out::println);
    }

    public List<Article> findBySpecification(Article article) {
        List<Article> list = articleRepository.findAll(new Specification<Article>() {
            /**
             * @param root 用于处理实体和字段,实体与实体之间的关系
             * @param criteriaQuery 主要用于对查询结果的处理,比如分组、聚合、排序、去重等等
             * @param criteriaBuilder 用于构造查询条件,调用sql函数等等
             * @return
             */
            @Override
            public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                // 定义一个Predicate的List,用来保存我们的参数
                List<Predicate> predicates = new ArrayList<>();
                // 取出对应的属性,拼接对应的参数
                // 这里取出的仅仅是属性,不是属性的值
                Path<String> title = root.get("title");
                if (article.getTitle() != null) {
                    Predicate predicate = criteriaBuilder.like(title, "%" + article.getTitle() + "%");
                    predicates.add(predicate);
                }
                Path<Integer> id = root.get("id");
                if (article.getId() != null) {
                    Predicate predicate = criteriaBuilder.gt(id, article.getId());
                    predicates.add(predicate);
                }
                Predicate predicate = criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
                return predicate;
            }
        });
        return list;
    }

​ 如果需要分页的话,同样只需要在最后加上 Pageable 参数即可

2.5 多表操作

​ 在实际开发中,难免会出现多表操作的情况,SpringDataJpa可以让我们通过实体类去操作多表。多表操作大体上可以分为 一对一、一对多、多对多关系,下面我们通过文章和评论关联关系来讲解多表操作。

2.5.1 一对一关系

​ 一对一关系我们创建个文章内容类,有 id、内容、文章id 三个字段,并且我们将原有的文章;Article类中的content字段移除。

ArticleDetail
package com.jg.sd.pojo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;

/**
 * @Author: 杨德石
 * @Date: 2021/2/1 17:39
 * @Version 1.0
 */
@Entity
@Table(name = "article_detail")
public class ArticleDetail implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String content;

    // 文章到文章内容一对一关系
    @OneToOne
    // 使用@JoinColumn注解维护外键关系,指定当前表的articleId指向Article中的主键id
    @JoinColumn(name = "articleId", referencedColumnName = "id", unique = true)
    private Article article;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Article getArticle() {
        return article;
    }

    public void setArticle(Article article) {
        this.article = article;
    }
}

Article
package com.jg.sd.pojo;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/1/31 16:12
 * @Version 1.0
 */
// 标识这是个实体类
@Entity
// 对应表
@Table(name = "article")
public class Article implements Serializable {

    // 声明为主键
    @Id
    /**
     * 主键生成策略,有四个取值
     * TABLE:使用一个特定的数据库表格来保存主键。
     * SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
     * IDENTITY:主键由数据库自动生成(主要是自动增长型)
     * AUTO:主键由程序控制。
     */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
     * 声明类的属性和表字段对应,属性名如果一致可省略
     */
    @Column(name = "title")
    private String title;

    private String content;

    private Date createTime;

    // 在一的一方放弃维护外键
    @OneToMany(mappedBy = "article")
    private List<ArticleComment> commentList = new ArrayList<>();

    // 建立文章内容到文章的一对一关系
    // 设置级联操作,当操作article的时候,同时级联操作article_detail的信息
    /**
     * cascade:关联属性,这个属性定义了当前类对象操作了之后,级联对象的操作。本例中定义了:CascadeType.ALL,当前类增删改查改变之后,关联类跟着增删改查。
     * fetch:FetchType类型的属性。可选择项包括:FetchType.EAGER 和FetchType.LAZY。  FetchType.EAGER表示关系类在主类加载的时候同时加载,FetchType.LAZY表示关系类在被访问时才加载。默认值是FetchType.LAZY。
     * mappedBy:
     拥有关联关系的域,如果关系是单向的就不需要。
那么什么叫拥有关联关系呢,可以这么认为,假设是双向一对一的话,那么拥有关系的这一方有建立、解除和更新与另一方关系的能力,而另一方没有,只能被动管理;在双向一对多和双向多对多中是一个意思。
由于JoinTable和JoinColumn一般定义在拥有关系的这一端,而Hibernate又不让mappedBy跟JoinTable和JoinColumn定义在一起,所以mappedBy一定是定义在关系的被拥有方,the owned side,也就是跟定义JoinTable和JoinColumn互斥的一方,它的值指向拥有方中关于被拥有方的字段,可能是一个对象(OneToMany),也可能是一个对象集合(ManyToMany)。 
     
     * 该属性是与 JoinColumn互斥的,设置在没有 JoinColumn的一方
     * 
     * CascadeType.PERSIST:级联新增(又称级联保存)
     * CascadeType.MERGE:级联合并(级联更新)
     * CascadeType.REMOVE:级联删除
     * CascadeType.REFRESH:级联刷新(查询)
     * CascadeType.ALL:以上四种都是。
     */
    @OneToOne(mappedBy = "article", cascade = CascadeType.ALL)
    private ArticleDetail articleDetail;

    public List<ArticleComment> getCommentList() {
        return commentList;
    }

    public void setCommentList(List<ArticleComment> commentList) {
        this.commentList = commentList;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public ArticleDetail getArticleDetail() {
        return articleDetail;
    }

    public void setArticleDetail(ArticleDetail articleDetail) {
        this.articleDetail = articleDetail;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"id\":")
                .append(id);
        sb.append(",\"title\":\"")
                .append(title).append('\"');
        sb.append(",\"content\":\"")
                .append(content).append('\"');
        sb.append(",\"createTime\":\"")
                .append(createTime).append('\"');
        sb.append(",\"articleDetail\":")
                .append(articleDetail);
        sb.append('}');
        return sb.toString();
    }
}

测试
    @Test
    public void testSaveO2O() {
        Article article = new Article();
        article.setTitle("测试一对一");
        ArticleDetail detail = new ArticleDetail();
        detail.setContent("一对一内容");
        // 建立二者的关系,否则外键的一列会为null
        article.setArticleDetail(detail);
        detail.setArticle(article);
        articleRepository.save(article);
    }

    @Test
    public void testGetO2O() {
        System.out.println(articleRepository.findById(12).get());
    }

    @Test
    public void testDeleteO2O() {
        articleRepository.deleteById(12);
    }

2.5.2 一对多关系

​ 一对多关系我们创建一个文章评论类,有 编号、文章编号、评论内容 三个属性。

ArticleComment
package com.jg.sd.pojo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.io.Serializable;

/**
 * @Author: 杨德石
 * @Date: 2021/2/1 21:03
 * @Version 1.0
 */
@Entity
@Table(name = "article_comment")
public class ArticleComment implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String content;

    @ManyToOne
    @JoinColumn(name = "article_id", referencedColumnName = "id")
    private Article article;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Article getArticle() {
        return article;
    }

    public void setArticle(Article article) {
        this.article = article;
    }
}

Article
package com.jg.sd.pojo;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/1/31 16:12
 * @Version 1.0
 */
// 标识这是个实体类
@Entity
// 对应表
@Table(name = "article")
public class Article implements Serializable {

    // 声明为主键
    @Id
    /**
     * 主键生成策略,有四个取值
     * TABLE:使用一个特定的数据库表格来保存主键。
     * SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
     * IDENTITY:主键由数据库自动生成(主要是自动增长型)
     * AUTO:主键由程序控制。
     */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
     * 声明类的属性和表字段对应,属性名如果一致可省略
     */
    @Column(name = "title")
    private String title;

    private String content;

    private Date createTime;
    
    // 在一的一方放弃维护外键
    @OneToMany(mappedBy = "article")
    private List<ArticleComment> commentList = new ArrayList<>();

    // 建立文章内容到文章的一对一关系
    // 设置级联操作,当操作article的时候,同时级联操作article_detail的信息
    /**
     * cascade:关联属性,这个属性定义了当前类对象操作了之后,级联对象的操作。本例中定义了:CascadeType.ALL,当前类增删改查改变之后,关联类跟着增删改查。
     * fetch:FetchType类型的属性。可选择项包括:FetchType.EAGER 和FetchType.LAZY。  FetchType.EAGER表示关系类在主类加载的时候同时加载,FetchType.LAZY表示关系类在被访问时才加载。默认值是FetchType.LAZY。
     * mappedBy:单向关系不需要设置该属性,双向关系必须设置,避免双方都建立外键字段。
     *          该属性是与 JoinColumn互斥的,设置在没有 JoinColumn的一方
     */
    @OneToOne(mappedBy = "article", cascade = CascadeType.ALL)
    private ArticleDetail articleDetail;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public ArticleDetail getArticleDetail() {
        return articleDetail;
    }

    public void setArticleDetail(ArticleDetail articleDetail) {
        this.articleDetail = articleDetail;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"id\":")
                .append(id);
        sb.append(",\"title\":\"")
                .append(title).append('\"');
        sb.append(",\"content\":\"")
                .append(content).append('\"');
        sb.append(",\"createTime\":\"")
                .append(createTime).append('\"');
        sb.append(",\"articleDetail\":")
                .append(articleDetail);
        sb.append('}');
        return sb.toString();
    }
}

测试
    @Test
    public void testSaveO2M() {
        Article article = new Article();
        article.setTitle("一对多");
        ArticleComment comment1 = new ArticleComment();
        comment1.setContent("评论1");
        comment1.setArticle(article);
        ArticleComment comment2 = new ArticleComment();
        comment2.setContent("评论2");
        comment2.setArticle(article);
        article.getCommentList().add(comment1);
        article.getCommentList().add(comment2);
        articleRepository.save(article);
    }

2.5.3 多对多关系

​ 多对多我们来创建一个文章分类类,有 ID、名称 两个属性

Type
package com.jg.sd.pojo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/2/1 21:18
 * @Version 1.0
 */
@Entity
@Table(name = "type")
public class Type implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToMany
    @JoinTable(
            // 这里的name表示中间表名称
            name = "article_type",
            // 中间表的外键字段关联当前实体类所对应表的主键字段
            joinColumns = {
                    @JoinColumn(name = "type_id", referencedColumnName = "id")
            },
            // 中间表的外键关联字段对应对方类对应表的主键字段
            inverseJoinColumns = {
                    @JoinColumn(name = "article_id", referencedColumnName = "id")
            }
    )
    private List<Article> articleList = new ArrayList<>();

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Article> getArticleList() {
        return articleList;
    }

    public void setArticleList(List<Article> articleList) {
        this.articleList = articleList;
    }
}

Article
package com.jg.sd.pojo;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/1/31 16:12
 * @Version 1.0
 */
// 标识这是个实体类
@Entity
// 对应表
@Table(name = "article")
public class Article implements Serializable {

    // 声明为主键
    @Id
    /**
     * 主键生成策略,有四个取值
     * TABLE:使用一个特定的数据库表格来保存主键。
     * SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
     * IDENTITY:主键由数据库自动生成(主要是自动增长型)
     * AUTO:主键由程序控制。
     */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
     * 声明类的属性和表字段对应,属性名如果一致可省略
     */
    @Column(name = "title")
    private String title;

    private String content;

    private Date createTime;

    // 在一的一方放弃维护外键
    @OneToMany(mappedBy = "article")
    private List<ArticleComment> commentList = new ArrayList<>();

    // 建立文章内容到文章的一对一关系
    // 设置级联操作,当操作article的时候,同时级联操作article_detail的信息
    /**
     * cascade:关联属性,这个属性定义了当前类对象操作了之后,级联对象的操作。本例中定义了:CascadeType.ALL,当前类增删改查改变之后,关联类跟着增删改查。
     * fetch:FetchType类型的属性。可选择项包括:FetchType.EAGER 和FetchType.LAZY。  FetchType.EAGER表示关系类在主类加载的时候同时加载,FetchType.LAZY表示关系类在被访问时才加载。默认值是FetchType.LAZY。
     * mappedBy:单向关系不需要设置该属性,双向关系必须设置,避免双方都建立外键字段。
     * 该属性是与 JoinColumn互斥的,设置在没有 JoinColumn的一方
     *
     * CascadeType.PERSIST:级联新增(又称级联保存)
     * CascadeType.MERGE:级联合并(级联更新)
     * CascadeType.REMOVE:级联删除
     * CascadeType.REFRESH:级联刷新(查询)
     * CascadeType.ALL:以上四种都是。
     */
    @OneToOne(mappedBy = "article", cascade = CascadeType.ALL)
    private ArticleDetail articleDetail;

    @ManyToMany(mappedBy = "articleList")
    private List<Type> typeList = new ArrayList<>();

    public List<ArticleComment> getCommentList() {
        return commentList;
    }

    public List<Type> getTypeList() {
        return typeList;
    }

    public void setTypeList(List<Type> typeList) {
        this.typeList = typeList;
    }

    public void setCommentList(List<ArticleComment> commentList) {
        this.commentList = commentList;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public ArticleDetail getArticleDetail() {
        return articleDetail;
    }

    public void setArticleDetail(ArticleDetail articleDetail) {
        this.articleDetail = articleDetail;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"id\":")
                .append(id);
        sb.append(",\"title\":\"")
                .append(title).append('\"');
        sb.append(",\"content\":\"")
                .append(content).append('\"');
        sb.append(",\"createTime\":\"")
                .append(createTime).append('\"');
        sb.append(",\"articleDetail\":")
                .append(articleDetail);
        sb.append('}');
        return sb.toString();
    }
}

TypeRepository
public interface TypeRepository extends JpaRepository<Type, Integer>, JpaSpecificationExecutor<Type> {
}

测试方法
    @Test
    public void testM2M() {
        Article a1 = new Article();
        a1.setTitle("多对多1");
        Article a2 = new Article();
        a2.setTitle("多对多2");

        Type t1 = new Type();
        t1.setName("多对多1");
        Type t2 = new Type();
        t2.setName("多对多2");

        // 建立关联关系
        a1.getTypeList().add(t1);
        a1.getTypeList().add(t2);
        a2.getTypeList().add(t1);
        a2.getTypeList().add(t2);

        t1.getArticleList().add(a1);
        t1.getArticleList().add(a2);
        t2.getArticleList().add(a1);
        t2.getArticleList().add(a2);
        articleRepository.save(a1);
        articleRepository.save(a2);
        typeRepository.save(t1);
        typeRepository.save(t2);

    }

三、SpringDataRedis

3.1 SpringDataRedis简介

​ Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。

​ SpringDataRedis主要提供了如下功能

  • 连接池自动管理,提供了一个高度封装的RedisTemplate类,基于这个类就可以对redis进行各种操作
  • 针对jedis、lettuce客户端中大量api进行了归类封装,封装为了 operation 接口,接口有如下实现。
    • ValueOperations:简单字符串类型数据操作
    • SetOperations:set类型数据操作
    • ZSetOperations:zset类型数据操作
    • HashOperations:map类型数据操作
    • ListOperations:list类型数据操作

3.2 安装redis

​ 我们这里就简单的使用docker进行安装。

docker run -di --name redis -p 6379:6379 redis

3.3 快速上手

3.3.1 pom.xml


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3.3.2 application.yml

spring:
  redis:
    host: 39.100.74.64
    port: 6379

3.3.3 入门代码

@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisTest {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void test() {
        redisTemplate.opsForValue().set("name", "jige");
    }

}

RedisAutoConfiguration 中,已经默认给我们配置好了一个泛型是 <Object, Object> 的 redisTemplate,当你想要注入其他泛型时,就会报错,解决方案是使用 Resource 注入。

3.4 SpringDataRedis的序列化器

​ 在上面我们存入了数据之后,通过redis命令行或者其他的图形化客户端可以发现,存入redis的是带有一串乱码,类似于二进制的数据,这是什么原因呢?

​ SpringDataRedis在操作数据的时候,会经过底层的一个序列化器进行序列化,它会将要保存的数据按照一定的规则进行序列化后再进行存储。

​ SpringDataRedis提供了下面几种序列化器

  • StringRedisSerializer:字符串序列化
  • GenericToStringSerializer:将对象泛化为字符串并序列化
  • 2:序列化为json字符串
  • GenericJackson2JsonRedisSerializer:功能同上,但更易反序列化
  • OxmSerializer:序列化为xml
  • JdkSerializaztionRedisSerializer:序列化为二进制数据。

​ RedisTemplate默认使用最后一个序列化器。

3.4.1 设置序列化器

​ 前面我们通过源码发现,SpringDataRedis默认会检测容器里是否有 redisTemplate 的对象,如果没有,会默认给我们注入一个RedisTemplate对象,那么,这就意味着我们可以自己对RedisTemplate进行定制化配置,比如修改它的序列化器。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 设置
        template.setValueSerializer(new StringRedisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        return template;
    }

}

3.5 SpringDataRedis操作

3.5.1 String类型

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 21:03
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class StringTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testSet() throws Exception {
        ValueOperations operations = redisTemplate.opsForValue();
        // 直接set值
        operations.set("name", "jige");
        // 偏移量设置,不是设置过期时间
        operations.set("name", "leige", 2);
        // 设置过期时间10分钟
        operations.set("name3", "jigege", 10, TimeUnit.MINUTES);

        // 批量保存
        Map map = new HashMap();
        map.put("names1", "jige1");
        map.put("names2", "jige2");
        map.put("names3", "jige3");

        operations.multiSet(map);

        // 追加,存在就追加,不存在就保存
        operations.append("name1", "leige");

        // 存在就返回false,不存在就返回true
        Boolean aBoolean = operations.setIfAbsent("lock", "value");
        System.out.println(aBoolean);
    }

    @Test
    public void testGet() throws Exception {
        ValueOperations operations = redisTemplate.opsForValue();
        Object name = operations.get("name");
        System.out.println(name);

        // 批量获取
        List keys = new ArrayList();
        keys.add("name");
        keys.add("name1");
        keys.add("name2");
        List list = operations.multiGet(keys);
        System.out.println(list);
    }

    @Test
    public void testDelete() throws Exception {
        redisTemplate.delete("name");
    }

    @Test
    public void testIncr() throws Exception {
        ValueOperations operations = redisTemplate.opsForValue();
        // 自增
        operations.increment("age");
        // 自增后的返回值
        Long age = operations.increment("age", 3);
        System.out.println(age);

        operations.decrement("num");

    }

}

3.5.2 Hash类型

​ 操作Hash类型之前,我们需要设置一下序列化器,否则存入redis的数据会是乱码

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 设置
        template.setValueSerializer(new StringRedisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

接着, 我们开始编写代码

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 21:15
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class HashTest {


    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testPut()  {
        Map map = new HashMap();
        map.put("name", "鸡哥");
        map.put("age", 18);
        Map map2 = new HashMap();
        map2.put("name", "雷哥");
        map2.put("age", 50);

        HashOperations operations = redisTemplate.opsForHash();
        operations.put("user", "1", map);
        operations.put("user", "2", map2);
    }

    @Test
    public void testGet() {
        // 判断hashkey是否存在
        HashOperations operations = redisTemplate.opsForHash();
        Boolean flag = operations.hasKey("user", "2");
        System.out.println(flag);

        Object user = operations.get("user", "1");
        System.out.println(user);

        Set keys = operations.keys("user");
        System.out.println(keys);

        /**
         * 获取所有value
         */
        List list = operations.values("user");
        System.out.println(list);

        // 键值对获取
        Map map = operations.entries("user");
        System.out.println(map);
        
    }


}

3.5.3 List类型

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 21:44
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class ListTest {
    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testAdd() {
        ListOperations operations = redisTemplate.opsForList();
        // 左push和右push
        operations.leftPush("names", "张三");
        operations.leftPushAll("names", "李四", "王五", "赵六");

        operations.rightPush("names", "田七");
        operations.rightPushAll("names", "王八", "老九");

    }

    @Test
    public void testGet() {
        ListOperations operations = redisTemplate.opsForList();
        // 0代表从左边开始第一个元素
        Object names = operations.index("names", 0);
        System.out.println(names);

        // -1代表右边开始第一个元素
        Object names1 = operations.index("names", -1);
        System.out.println(names1);

        // range表示一个范围,开始索引到结束索引,首尾都包含从
        List list = operations.range("names", 0, 2);
        System.out.println(list);
    }
    
    @Test
    public void testRemove() {
        ListOperations operations = redisTemplate.opsForList();
        Object names = operations.rightPop("names");
        System.out.println(names);
    }

}

3.5.4 Set类型

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.Set;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 21:56
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class SetTest {
    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testAdd() {
        SetOperations operations = redisTemplate.opsForSet();
        operations.add("namesSet", "zhangsan", "lisi", "wangwu", "zhangsan");
    }

    @Test
    public void testFind() {
        SetOperations operations = redisTemplate.opsForSet();
        // 查询集合中所有元素
        Set namesSet = operations.members("namesSet");
        System.out.println(namesSet);

        // 随机获取
        Object members = operations.randomMember("namesSet");
        System.out.println(members);

        // 随机获取多个
        Object member = operations.randomMembers("namesSet", 2);
        System.out.println(member);

    }

    @Test
    public void testRemove() {
        redisTemplate.opsForSet().pop("namesSet");
    }

    @Test
    public void testMulti() {
        SetOperations operations = redisTemplate.opsForSet();
        operations.add("name1", "zhangsan", "lisi", "wangwu", "zhaoliu");
        operations.add("name2", "zhangsan", "lisi", "tianqi", "wangba");
        // 交集
        Set intersect = operations.intersect("name1", "name2");
        System.out.println(intersect);

        // 并集
        Set union = operations.union("name1", "name2");
        System.out.println(union);

        // 差集
        Set difference = operations.difference("name1", "name2");
        System.out.println(difference);
    }

}

3.5.5 ZSet类型

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.Set;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 22:08
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class ZsetTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testAdd() {
        ZSetOperations operations = redisTemplate.opsForZSet();
        operations.add("user", "zhangsan", 100);
        operations.add("user", "lisi", 101);
        operations.add("user", "wangwu", 90);
        operations.add("user", "zhaoliu", 93);
        operations.add("user", "tianqi", 92);
    }

    @Test
    public void testGet() {
        ZSetOperations operations = redisTemplate.opsForZSet();
        // 查询分数
        Double score = operations.score("user", "zhangsan");
        System.out.println(score);

    }

    @Test
    public void testList() {
        ZSetOperations operations = redisTemplate.opsForZSet();
        // 根据排名区间查询
        Set user = operations.range("user", 0, 1);
        System.out.println(user);

        // 通过排名区间获取元素集合和分数
        Set<ZSetOperations.TypedTuple<String>> users2 = operations.rangeWithScores("user", 0, 2);
        for (ZSetOperations.TypedTuple<String> tuple : users2) {
            String value = tuple.getValue();
            Double score = tuple.getScore();
            System.out.println("value: " + value + ", scopre:" + score);
        }

        // 通过分数区间获取集合元素
        Set users3 = operations.rangeByScore("users", 95, 100);

        // 通过分数区间获取元素和分数
        Set users = operations.rangeByScoreWithScores("users", 95, 100);
        // 以上都是从小到大排序,从大到小排序,只需要在方法前加上reverse
    }

}

3.5.6 管道

​ 当我们需要频繁往redis中写入数据时,每次操作redis都需要开启连接,对性能的损耗比较严重,那么有没有办法可以只开一次连接,等我们操作完之后再关闭连接呢?这里我们可以使用SpringDataRedis的管道技术

​ 普通的请求模型是同步的,每次请求对应一次IO操作等待;redis为了提高性能,提供了管道 (Pipeline)技术后, 可以将命令合并为一次IO,除了时延可以降低之外,还能大幅度提升系统吞吐量。

普通请求模型

img

管道请求模型

img

​ spring 封装的redisTemplate,在常规调用时候,一般直接调用opsfor…来操作redis数据库,每执行一条命令是要重新拿一个连接,因此很耗资源, executePipelined方法 专门用来支持管道开发, 可以将批量执行的命令封装到 RedisCallback

​ 我们以往redis中存入1000条数据为例

package com.jg.sd.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

/**
 * @Author: 杨德石
 * @Date: 2021/2/2 22:25
 * @Version 1.0
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class PipelineTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void test() {
        HashOperations operations = redisTemplate.opsForHash();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            operations.put("normal", "key" + i, "value" + i);
        }
        long time2 = System.currentTimeMillis();
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (int i = 0; i < 1000; i++) {
                    redisConnection.hSet("pipeline".getBytes(), ("kay" + i).getBytes(), ("velue" + i).getBytes());
                }
                return null;
            }
        });
        long time3 = System.currentTimeMillis();
        System.out.println(time2 - time1);
        System.out.println(time3 - time2);
    }

}

四、SpringDataMongoDB

4.1 简介

mongodb-driver 是mongodb 官方推出的Java连接MongoDB的驱动包,类似于JDBC驱动。该包操作mongodb非常的不友好,这里只提一下有这个技术,感兴趣的可以自己看菜鸟教程学习一下。

SpringDataMongoDB是SpringData家族成员之一,酷帅狂拽吊炸天的MongoDB持久层框架,底层封装了mongodb-driver。

4.2 安装mongodb

docker run -di --name=mongo -p 27017:27017 mongo

4.3 快速上手

4.3.1 pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

4.3.2 application.yml

spring:
  #数据源配置
  data:
    mongodb:
      #主机地址
      host: 127.0.0.1
      #数据库
      database: bbsdb
      #默认端口是27017
      port: 27017

4.3.3 集合结构

字段描述
_idID
name姓名
age年龄
sex性别
address地址
createdTime创建时间
state状态,1启用0禁用
followNum关注者数量

4.3.4 实体类编写

package com.jg.mongo.pojo;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.io.Serializable;
import java.util.Date;

/**
 * 使用 @Document("user") 指定这个类对应 user 集合
 * 使用 @CompoundIndex(def = "{'id':1, 'age': -1}") 表示复合索引
 *
 * @Author: 杨德石
 * @Date: 2020/8/13 22:01
 * @Version 1.0
 */
@Document("user")
@CompoundIndex(def = "{'id':1, 'age': -1}")
public class User implements Serializable {

    /**
     * 主键,该属性会自动对应_id字段。
     * 如果该属性名称就叫id,那么注解可以省略
     */
    @Id
    private String id;

    /**
     * 指定该属性对应集合中的name列
     * 如果属性名已经和name对应了
     * 就可以不写
     */
    @Field("name")
    private String name;

    private Integer age;

    private String address;

    /**
     * 使用 @Indexed 指定单列索引
     */
    @Indexed
    private Integer sex;

    private Date createdTime;

    private Integer state;

    private Integer followNum;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getSex() {
        return sex;
    }

    public void setSex(Integer sex) {
        this.sex = sex;
    }

    public Date getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(Date createdTime) {
        this.createdTime = createdTime;
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public Integer getFollowNum() {
        return followNum;
    }

    public void setFollowNum(Integer followNum) {
        this.followNum = followNum;
    }
}

  1. 实体类需要使用 @Document 注解标识为MongoDB文档,并指定集合名。
  2. @Id 注解指定文档的主键,不建议省略
  3. @CompoundIndex 注解指定复合索引,可以在Java类中添加索引,也可以在MongoDB中添加
  4. @Indexed 注解指定单字段索引。

4.3.5 Dao编写

package com.jg.mongo.dao;

import com.jg.mongo.pojo.User;
import org.springframework.data.mongodb.repository.MongoRepository;

/**
 * 继承MongoRepository,指定实体和主键的类型作为泛型
 *
 * @Author: 杨德石
 * @Date: 2020/8/13 22:07
 * @Version 1.0
 */
public interface UserRepository extends MongoRepository<User, String> {
}

Dao层编写非常简单,只需要编写一个接口,继承 MongoRepository ,指定实体和主键类型即可

4.3.6 Junit测试

package com.jg.mongo;

import com.jg.mongo.pojo.User;
import com.jg.mongo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;
import java.util.List;

@SpringBootTest
public class MongoApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    public void testSave() {
        User user = new User();
        user.setAge(10);
        user.setSex(1);
        user.setName("高尔稽");
        user.setState(1);
        user.setFollowNum(0);
        user.setAddress("我也不知道你住哪");
        user.setCreatedTime(new Date());
        userService.save(user);
    }

    @Test
    public void testFindAll() {
        List<User> userList = userService.findAll();
        System.out.println(userList);
    }

    @Test
    public void testFindById() {
        User user = userService.findById("5f34036ce9a0ec5fdc5354a4");
        System.out.println(user);
    }

    @Test
    public void testUpdate() {
        User user = new User();
        user.setId("5f340372e9a0ec5fdc5354a6");
        user.setAge(10);
        user.setSex(1);
        user.setName("菜文稽");
        user.setState(1);
        user.setFollowNum(0);
        user.setCreatedTime(new Date());
        userService.update(user);
    }

    @Test
    public void testDelete() {
        userService.deleteById("5f34036ce9a0ec5fdc5354a5");
    }

}

5.3.7 方法命名规则

根据启用状态和性别查询

Repository中编写如下代码

    /**
     * 根据state和sex查询
     *
     * @param state
     * @param sex
     * @return
     */
    List<User> findByStateAndSex(Integer state, Integer sex);

单元测试

    @Test
    public void testFindByStateAndSex() {
        List<User> list = userService.findByStateAndSex(1, 1);
        for (User user : list) {
            System.out.println(user);
        }
    }

5.3.8 MongoTemplate

现在有个需求,我们需要给某个用户的关注量+1,下面的代码是实现方案

    public void incrFollowCount(String id) {
        User user = userRepository.findById(id).get();
        user.setFollowNum(user.getFollowNum() + 1);
        userRepository.save(user);
    }

该方案实现起来虽然简单,但是性能不高。

我们只需要给关注量+1,并不需要姓名、地址等这些数据,因此也就没必要查询出这些字段,甚至于根本就不需要查询操作,直接更新就可以了。

我们可以使用MongoTemplate解决这个需求。

    public void incrFollowCount(String id) {
        // 构造查询对象
        Query query = Query.query(Criteria.where("_id").is(id));
        // 构造更新对象
        Update update = new Update();
        // follow字段+1
        update.inc("followNum");
        // 执行update
        mongoTemplate.updateFirst(query, update, User.class);
    }

5.3.9 分页查询

分页和排序的使用方式与Jpa类似

Repository中编写代码

    /**
     * 根据年龄分页查询
     *
     * @param age
     * @param pageable
     * @return
     */
    Page<User> findByAge(Integer age, Pageable pageable);

Service中添加代码

    /**
     * 根据名称分页查询
     *
     * @param age
     * @param page
     * @param size
     * @return
     */
    public Page<User> findByAgePage(Integer age, int page, int size) {
        // 构造分页对象
        PageRequest pageRequest = PageRequest.of(page - 1, size);
        return userRepository.findByAge(age, pageRequest);
    }

测试类

    @Test
    public void testFindByAgePage() {
        Page<User> users = userService.findByAgePage(18, 1, 2);
        System.out.println("总条数:" + users.getTotalElements());
        System.out.println("总页数:" + users.getTotalPages());
        System.out.println("本页数据:");
        List<User> userList = users.getContent();
        for (User user : userList) {
            System.out.println(user);
        }
    }

实际开发中我们可能会需要能够多条件分页查询,上面的场景可能不满足需求。使用MongoTemplate也可以解决分页问题

    /**
     * 使用MongoTemplate分页查询
     *
     * @param page
     * @param size
     * @param user
     * @return
     */
    public List<User> findPageByTemplate(int page, int size, User user) {
        // 构造一个查询对象
        Query query = new Query();
        // 设置参数
        if (!StringUtils.isEmpty(user.getName())) {
            query.addCriteria(Criteria.where("name").regex(user.getName() + ".*"));
        }
        if (user.getAge() != null) {
            query.addCriteria(Criteria.where("age").lt(user.getAge()));
        }
        if (user.getSex() != null) {
            query.addCriteria(Criteria.where("sex").is(user.getSex()));
        }
        // 跳过多少条
        query.skip((page - 1) * size);
        // 取出多少条
        query.limit(size);
        // 构造排序对象
        Sort.Order order = new Sort.Order(Sort.Direction.DESC, "age");
        // 设置排序对象
        query.with(Sort.by(order));
        return mongoTemplate.find(query, User.class);
    }

5.3.10 @Query注解

我们使用SpringDataJpa操作MySql的时候,尽管JPA已经非常强大,仍然避免不了手写sql的场景。

SpringDataMongoDB也存在类似的情况,因此我们可能会需要手写MongoDB的查询语句,使之操作更加灵活。

手写查询语句可以使用 @Query 注解,该注解的使用方式和SpringDataJpa一样


需求1:根据 state 和 age 查询

    /**
     * 根据state和age查询
     *
     * @param state
     * @param age
     * @return
     */
    @Query("{ state: ?0, age: ?1  }")
    List<User> selectEnableUserByAge(Integer state, Integer age);


需求2:有时候我们的参数可能过多,条件也可能不同,我们想传入一个实体类进行查询,直接取出实体类中的属性进行条件构造。

现在我们需要查询 性别为女,或者年龄在18岁以下的所有启用中的用户

    /**
     * 根据实体类查询
     * 根据性别或者年龄,和状态查询
     *
     * @param user
     * @return
     */
    @Query("{  state: ?#{[0].state}, $or: [ {sex: ?#{[0].sex}}, {age: { $lt: ?#{[0].age} }}  ] }")
    List<User> selectByEntity(User user);

同时,如果我们只想获取指定的字段,我们还可以使用第二个参数 fields 来进行投影查询

    /**
     * 根据实体类查询
     * 根据性别或者年龄,和状态查询
     *
     * @param user
     * @return
     */
    @Query(value = "{  state: ?#{[0].state}, $or: [ {sex: ?#{[0].sex}}, {age: { $lt: ?#{[0].age} }}  ] }",
            fields = "{ name:1, age:1 }")
    List<User> selectByEntity(User user);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值