一、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只是一个规范,而非框架,但是在我们日常的开发和交流中,会直接将
jpa
和SpringDataJpa
划等号,这是因为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图
这里我们需要关注的有这么几个接口:Repository、CrudRepository、PagingAndSortingRepository、JpaRepository
。
- Repository 标记接口,继承该接口后会被Spring识别,进而能够在接口中按照一定的规范来定义方法,定义方法的方式我们后面再介绍。
- CrudRepository 顾名思义,该接口实现了基本的增删改查方法。
- PagingAndSortingRepository 该接口实现了分页和排序的方法
- JpaRepository 该接口重写了几个查询和删除方法。
运行原理
那么,通过这一系列的继承下来,每个接口都会为我们的ArticleRepository提供一部分功能,我们的dao接口就已经拥有了很多的方法,那这些方法是如何运行的,为什么不需要实现类就可以直接调用,我们继续向下分析。
我们在任意的测试方法里打上断点,debug启动。我们发现在运行时,Spring会使用 JdkDynamicAopProxy
为我们的接口生成一个代理对象。
既然是生成了代理对象,那么这个对象是根据哪个类代理出来的呢?我们进入到 JdkDynamicAopProxy
类中,查看invoke方法,断点打在 targetSource
下面。我们发现代理的是SimpleJpaRepository
。
那么我们继续进入到 SimpleJpaRepository
。直接定位到save方法,我们发现,这里使用到了一个叫 em
的成员属性,该属性是 EntityManager
对象。 EntityManager
是jpa规范中提供的类,说明SpringDataJpa只是对jpa标准进行了进一步的封装,最终还是需要执行jpa中的方法。
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片段 |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ? and x.firstname = ? |
Or | findByLastnameOrFirstname | … where x.lastname = ? or x.firstname = ? |
Is,Equals | findByFirstname findByFirstnameIs findByFirstnameEquals | … where x.firstname = ? |
Between | findByStartDateBetween | … where x.startDate between ? and ? |
LessThan | findByAgeLessThan | … where x.age < ? |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ? |
GreaterThan | findByAgeGreaterThan | … where x.age > ? |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ? |
After | findByStartDateAfter | … where x.startDate > ? |
Before | findByStartDateBefore | … where x.startDate < ? |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ? |
NotLike | findByFirstnameNotLike | … where x.firstname not like ? |
StartingWith | findByFirstnameStartingWith | … where x.firstname like concat(?, '%') |
EndingWith | findByFirstnameEndingWith | … where x.firstname like concat('%', ?) |
Containing | findByFirstnameContaining | … where x.firstname like concat('%', ?, '%') |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ? order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ? |
In | findByAgeIn | … where x.age in ? |
NotIn | findByAgeNotIn | … where x.age not in ? |
True | findByActiveTrue | … where x.active = true |
False | findByActiveFalse | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … 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,除了时延可以降低之外,还能大幅度提升系统吞吐量。
普通请求模型
管道请求模型
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 集合结构
字段 | 描述 |
---|---|
_id | ID |
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;
}
}
- 实体类需要使用
@Document
注解标识为MongoDB文档,并指定集合名。@Id
注解指定文档的主键,不建议省略@CompoundIndex
注解指定复合索引,可以在Java类中添加索引,也可以在MongoDB中添加@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);