sSpringBoot
SpringBoot开发技术 — 数据库与持久化技术
技术无止境,唯有继续沉淀…之前介绍了SpringBoot的过滤,拦截器,相关的事件,相关的日志,文件等,Restful风格的Http动词,相关的注解,包括相关的@GetMapping等,@RequestParam,@PathVarible,@RequestHeader,@RestController,@RequestBody【前台需要使用相关的JavaScript方法处理表单元素】,@ResponseBody,@Vaildation【@NotNull… 引入Validation依赖】,创建自定义的注解和Validator,全局异常处理@ControllerAdivice,@ExceptionHandler,使用Swagger【使用@Api,@ApiModel…引入springfox依赖】
持久化是项目的基本需求,没有持久化,数据在RAM中断电就会丢失,Spring Boot中应用程序开发所使用的持久化,依赖各种类型的数据库、对应的客户端和相关的持久层框架【Redis,MongoDB】
JDBC是最初访问数据库的手段,是java提供的编写应用程序作为客户端访问数据库的API,JdbcTemplate是对于JDBC的封装(JDBC编程6步 – 有些固定的代码)简化了JDBC的使用,
当请求数据库的需求不复杂的时候,就可以简单使用JDBCTemplate进行数据库的访问,使用JDBCTemplate,就简单的引入相关依赖,在SpringBoot中,需要引入的起步依赖就是Spring-boot-starter-jdbc; 数据库的驱动使用mysql-connecter即可
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
Spring Boot中可以进行相关配置,使得SpringBoot自动从classpath下面加载schema.sql和data.sql,前者决定表结构,后者进行数据的初始化。如果使用不同的数据库,可以在yml中进行配置,配置spring.datasource.platform区分环境,同时建立不同的文件,比如: schema-${platform}.sql和data-…sql
初始化行为通过spring.datasource.initializetion-mode进行控制:
默认情况下,Spring boot使用JDBCTemplate的快速失败功能,也就是当两个脚本出现错误的时候就会导致程序异常;可以通过spring.datasource.contine-on-error来进行调整
上面三项的基础配置从datasource,变为了sql.init【数据源就只是单纯的数据源】
server:
port: 8081
servlet:
context-path: /persistence
spring:
datasource:
url: jdbc:mysql://localhost:3306/presis?servertimezone=GMT%2B8
username: cfeng
password: a1234567890b
#当执行schema和data.sql的用户不同时,可以配置相关的username和password
driver-class-name: com.mysql.cj.jdbc.Driver
sql:
init:
data-locations: classpath:sql/data.sql
schema-locations: classpath:sql/schema.sql
# continue-on-error: true
mode: always #这就会自动进行初始化,扫描相关的sql文件
现在已经不是在dataSource中定义初始化的相关配置,而是在sql.init下面,需要注意一定要定义data和schema的位置,这样才能够进行自动的扫描,所以其实名称是可以自定义的,但是还是使用默认的名称便于辨认
创建的两个sql脚本demo
##########schema.sql##############
----------------------------------
--- table structure for vehicle
-----------------------------------
DROP TABLE IF EXISTS vehicle;
CREATE TABLE vehicle(
id INT(11) NOT NULL AUTO_INCREMENT,
ve_name VARCHAR(255) DEFAULT NULL,
price DECIMAL(10,2) DEFAULT NULL,
PRIMARY KEY(id) <--- 需要注意不要出现语法问题
);
#########data.sql##################
------------------------------
--- Records of vehicle
-----------------------------
INSERT INTO vehicle VALUES (1,"Beanley",2750000.00);
INSERT INTO vehicle VALUEs (2,"Land Rover",1468000.00);
INSERT INTO vehicle VALUES (3,"Lincoin",1580000.00);
有了数据库,那么如何利用JdbcTemplate进行数据的访问呢?
正确配置之后,就需要正确书写Sql脚本,需要注意不要出现语法错误
使用该方法可以执行一条SQL语句得到一个结果对象,结果对象的类型需要在参数中进行声明,需要注意的是这只能执行一条Sql语句,Sql语句就是一个String类型,使用queryForObject需要传入的就是待执行的Sql语句和执行完毕之后的结果类型
这里可以直接使用test模块的assert来进行结果测试
import javax.annotation.Resource;
/**
* @author Cfeng
* @date 2022/7/1
*/
@Transactional(rollbackFor = Exception.class) //开启事务
@SpringBootTest//一体化测试,可以加载容器
public class JdbcTemplateTests {
@Resource
private JdbcTemplate jdbcTemplate; //spring boot 自动配置的对象,相关的starter中会创建这个对象
@Test
public void queryForObjectTest() {
//查询vehicle表
String sql = "SELECT COUNT(*) FROM vehicle";
//获取查询的结果,queryForObject的就是放入相关的sql语句,同时指定返回结果的类类型
Integer numOfVehicle = jdbcTemplate.queryForObject(sql,Integer.class);
assert numOfVehicle != null;
System.out.format("There is %d vehicles in the table",numOfVehicle);
}
}
JdbcTemplate轻量级,在简单的demo中可以使用,在SpringBoot中就是借助封装的jdbcTemplate对象【starter-jdbc中自动装配】
上面的queryForObjectt的返回值是包装类型,如果要使用自定义类型,直接修改会遇到IncorrectResult…异常,比如返回值如果是Vehicle
@Data
@Accessors(chain = true)
@NoArgsConstructor
public class Vehicle {
private Integer id;
private String veName;
private BigDecimal price;
}
//这里的查询结果如果是这个实体类对象,那么就需要RowMapper映射类的help
@Test
public void queryForObjectTest() {
//查询vehicle表
String sql = "SELECT * FROM vehicle WHERE id = 1";
//获取查询的结果,queryForObject的就是放入相关的sql语句,同时指定返回结果的类类型
Vehicle testVehicle = jdbcTemplate.queryForObject(sql,Vehicle.class);
assert testVehicle != null;
System.out.println(testVehicle);
这里的查询结果为自定义类型,但是queryForObject不支持自动化的映射操作,所以需要使用RowMapper
org.springframework.dao.EmptyResultDataAccessException: Incorrect result size
所以这里需要使用Rowmapper进行包裹Vehicle,RowMapper使用JDBC的查询结果集ResultSet,将查询的结果进行set赋值封装为自定义对象
@Test
public void queryForObject_WithRowMapper() {
//RowMapper将自定义类型包裹之后才能被识别
RowMapper<Vehicle> rowMapper = (ResultSet resultSet, int rowNum) -> {
return new Vehicle().setId(resultSet.getInt("id")).setVeName(resultSet.getString("veName")).setPrice(resultSet.getBigDecimal("price"));
};
String sql = "SELECT * FROM vehicle WHERE id = ?";
int id = 1;
Vehicle vehicle = jdbcTemplate.queryForObject(sql,rowMapper,new Object[]{1});
assert vehicle != null;
System.out.println(vehicle);
}
这里还是使用的PreparedStatement使用?作为占位符
RowMapper的缺点就是每次查询新的结果都需要重新进行映射操作,因为其与ResultSet有关,当查询的字段名与映射类的属性名一致时,可以使用BeanPropertyRowMapper进行自动映射
其实也就是简化了RowMapper封装结果的过程【使用lambda表达式将ResultSet的结果(使用rowNumber表结果的行数)封装为自定义的结果类型】,BeanPropertyRowMapper在属性一致的时候就可以自动完成这一过程
//需要注意映射类需要有默认或者无参构造器,使用BeanPropertyRowMapper.newInstance(Vehicle.class)即可代替
@Test
public void queryForObject_WithRowMapper() {
//RowMapper将自定义类型包裹之后才能被识别
RowMapper<Vehicle> rowMapper = (ResultSet resultSet, int rowNum) -> {
return new Vehicle().setId(resultSet.getInt("id")).setVeName(resultSet.getString("veName")).setPrice(resultSet.getBigDecimal("price"));
};
String sql = "SELECT * FROM vehicle WHERE id = ?";
int id = 1;
Vehicle vehicle = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(Vehicle.class),new Object[]{1});
assert vehicle != null;
System.out.println(vehicle);
}
BeanPropertyRowMapper.newInstance(Vehicle.class)就会自动完成包装的过程:
(ResultSet resultSet, int rowNum) -> {
return new Vehicle().setId(resultSet.getInt("id")).setVeName(resultSet.getString("veName")).setPrice(resultSet.getBigDecimal("price"));
};
queryForObject的查询结果都是单个对象,当查询结果为列表的时候,就应该使用queryForList方法
//相比queryForObject没有什么太大的区别
@Test
public void queryForListTest() {
String sql = "SELECT * FROM vehicle";
List<Map<String,Object>> result = jdbcTemplate.queryForList(sql);
assert !result.isEmpty();
result.forEach(System.out::println);
}
这里只是简单将查询结果封装为一个List,之后使用forEach打印,结果:
{id=2, ve_name=Land Rover, price=1468000.00}
{id=3, ve_name=Lincoin, price=1580000.00}
使用简单的JdbcTemplate,其占位符就是 ? ,当占位符过多时,语句含义难以理解,这个时候可以选择使用NamedParameterJdbcTemplate进行代替,可以参照ESQL的 :xxxx
//@SpringBootTest加载容器,可以直接注入NamedParameterJdbcTemplate
@Test
public void queryForObject_withNamedJdbcTemplate() {
String sql = "SELECT * FROM vehicle WHERE ve_name LIKE :ve_name AND price > :price LIMIT 1";
//映射参数
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource().addValue("ve_name","L%")
.addValue("price","6000");
//将参数映射源放入queryForObject中;RowMapper行映射器将资源映射为...
Vehicle vehicle = namedParameterJdbcTemplate.queryForObject(sql,mapSqlParameterSource,BeanPropertyRowMapper.newInstance(Vehicle.class));
assert vehicle != null;
System.out.println(vehicle.toString());
}
执行结果【当出现…Incorrect异常时,是因为查询的结果为空或者不匹配等问题】
Vehicle(id=2, ve_name=Land Rover, price=1468000.00)
上面的query都是查询方法,当需要更新数据库时,需要使用update方法,包括对记录的增加、删除、修改
@Test
public void update_saveVehicle() {
String sql = "INSERT INTO vehicle(ve_name,price) VALUES(:ve_name,:price)";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource().addValue("ve_name","Tesla")
.addValue("price","850000");
//update的result就是修改的行数
int result = namedParameterJdbcTemplate.update(sql,mapSqlParameterSource);
assert result > 0;
System.out.println(result);
}
@Test
public void update_updateVehicle() {
//修改操作
String sql = "UPDATE vehicle SET price = price * 0.5 WHERE ve_name LIKE :ve_name";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource().addValue("ve_name","Land%");
int re = namedParameterJdbcTemplate.update(sql,mapSqlParameterSource);
assert re > 0;
}
@Test
public void update_deleteVehicle() {
String sql = "DELETE FROM vehicle WHERE price > :price";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource().addValue("price","1500000");
int ret = namedParameterJdbcTemplate.update(sql,mapSqlParameterSource);
assert ret > 0;
}
增删改都是使用的update方法,这里都是用NamedParameterJdbcTemplate使用有含义的占位符,执行的结果都是影响的行数,需要使用MapSqlParameterSource进行参数的映射,之后将映射源加入到update方法中执行sql
JPA是一种Java持久化规范,可以简化对于ORM技术的整合,结束比如Hibernate、JDO等各自为营的局面,JPA在使用上说就是一种全自动的持久层框架,相关的还有Mybatis-plu,全自动框架不需要再写Sql语句,对于JPA来说,按照JPA规范的相关方法就可以自动调用相关的SQL语句
JPA的操作步骤:
- 加载配置文件,根据配置串改你实体管理工厂对象
- 使用实体管理工厂创建实体的管理器
- 创建事务对象开启事务
- CRUD操作
- 提交事务
- 释放资源
和Mybatis的基本步骤是类似的
public void jpaTest() {
//加载配置文件,创建实体管理器工厂对象
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaConfig");
//使用实体管理器工厂创建实体管理器
EntityManager manager = factory.createEntityManager();
//获取事务对象,开启事务
EntityTransaction tx = manager.getTransaction();
tx.begin();
//完成CRUD操作
Vehicle vehicle = new Vehicle();
vehicle.setVe_name("LinKen");
//保存使用实体管理器的persist方法
manager.persist(vehicle);
//提交事务
tx.commit();
//释放资源
manager.close();
factory.close();
}
JPA最主要的几个对象就是EntityManager和其Factory,Mybatis就是SqlSession和其factory
jpa: Presistence ---create(config)-> EntityManagerFactory ---> EntityManager
mybatis:SqlSessionFactoryBuilder --build(Resouces.getXXXXAsInputStream(config))--> SqlSessionFactory ---> SqlSession
Spring Data JPA是Spring Data中一个,可以轻松实现JPA的存储库。Spring Data JPA主要就是基于JPA进行数据访问层的增强,JPA是规范,Spring Data JPA是一个全自动的数据访问层的框架,使用之后就需要编写Repository接口即可,框架会自动提供对应的实现
该框架是遵守JPA规范的,在JPA规范下提供Repository的实现,提供配置项用以切换具体实现规范的ORM框架
基于接口的查询方式,更具接口名的定义自动生成相关的SQL语句的代理实例,不需要手写SQL,并且还提供了基础的CRUD的实现,继承了之后就会默认为@Repository,会自动创建实例,不需要再添加该注解
//看看源码
@Indexed
public interface Repository<T, ID> {
}
//再看看JpaRespostory
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
/** @deprecated */
@Deprecated
default void deleteInBatch(Iterable<T> entities) {
this.deleteAllInBatch(entities);
}
void deleteAllInBatch(Iterable<T> entities);
void deleteAllByIdInBatch(Iterable<ID> ids);
void deleteAllInBatch();
/** @deprecated */
@Deprecated
T getOne(ID id);
/** @deprecated */
@Deprecated
T getById(ID id);
T getReferenceById(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
这里的@NoRepositoryBean的作用就是不为此类创建Repository Bean实例
使用Spring Data JPA全自动框架,需要首先创建相关的表对应的实体类,这里的表是自动创建到数据库中,所以需要手动定义相关的主键和相关的约束
@Entity
@Table(name = "ve_user")
@Data
@Accessors(chain = true)
public class veUser {
private static final long serialVersionUID = 1L;
//主键
@Id
@Column(name = "id", nullable = false) //NOTNULL
private Integer id;
//姓名
@Column(name = "user_name",nullable = false)
private String userName;
//身高
@Column(name = "height")
private BigDecimal height;
//体重
@Column(name = "weight")
private BigDecimal weight;
//BMI
@Column(name = "BMI")
private BigDecimal BMI;
}
使用@Entity就可以标注该类为实体类,框架就会自动更新相关的表【需要进行配置】,@Id标注主键,@Colum标注表中对应的字段名,还可以进行约束,@Table指定对应的表名
配置jpa
spring:
datasource:
url: jdbc:mysql://localhost:3306/presis?servertimezone=GMT%2B8
username: cfeng
password: a1234567890b
#当执行schema和data.sql的用户不同时,可以配置相关的username和password
driver-class-name: com.mysql.cj.jdbc.Driver
dbcp2: #连接池的相关配置
initial-size: 10
min-idle: 10
max-idle: 30
max-wait-millis: 3000
time-between-eviction-runs-millis: 200000 #检查关闭相关连接的时间
remove-abandoned-timeout: 200000
jpa: #Spring data jpa的配置,dialect是properties下面的
show-sql: true
open-in-view: true
database: mysql
hibernate:
ddl-auto: update
naming:
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy #SpringPhysical就是遇到下划线转大写
dbcp2为连接池的相关配置,包括最大,最小的链接数量等
操作的数据库的Respository接口
public interface VeUserRepository extends CrudRepository<VeUser,Integer> {
//按照规范书写相关方法名
List<VeUser> findByUserName(String userName);
//查询比height高的用户
VeUser findByHeightGreaterThan(BigDecimal height);
}
JPA框架会自动创建其相关的实例,所以可以直接使用
@SpringBootTest
public class JpaTests {
@Resource
private VeUserRepository veUserRepository;
@Test
public void testJpaRepository() {
List<VeUser> list = veUserRepository.findByUserName("张三");
assert list != null;
System.out.println(list);
}
完成查询只需要一个findBy{:colum}格式的方法,定义之后,框架自动创建相关的实现类实例对象放入容器中等待使用,这里的findBy可以替换为getBy等,因为Spring Data JPA会对方法进行解析,解析的过程为:去掉findBy等关键字,根据剩下的字段名和关键字生成对应的查询的代码【SQL语句】
JPA的相关关键字:
- And: findByLastNameAndFirstName ----> 对应的就是 where x.lastname = ? and x.firstname = ?
- Or : xxxxxxxOrFirstName ---- > where x.lastname = ? or x.firstname = ?
- IsEquals: 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和Before
- After: findByStartDateAfter —> where x.startDate > ?
- Before < ?
对于非空null,就是IsNull,IsNotNull
- IsNull : findByAgeIsNull --> where x.age is null
- IsNotNull : is not null
模糊查询对应的是StartingWith,EndingWith,Containing 分别为%X X% %X%
- StartingWith: findByFirstNameStartingWith —> where x.firstName like X%
- EndingWith : %X
- Containing: %X%
还有诸如排序的OrderBy,不相等的Not, in 和 not In 对应的是In 和NotIn,还有IgnoreCase忽略大小写比较
- OrderBy: findByAgeOrderByLastNameDesc ----> where x.age = ? order by x.lastname desc
- Not: findByLastNameNot ----> where x.lastname <> ?
- In: findByLastNameIn ----> where x.lastname in ?
- TRUE/FALSE: findByAgeTrue —> where x.age = true/false
- Ignorecase: findByFirstNameIgnoreCase: —> where UPPER(x.firstName) = UPPER(?)
JpaRepository和CrudRepository固然方便,但是对于逻辑复杂的需求,不方便实现,对于难以实现的部分,Spring Data JPA提供了JpaSpecificationExecutor
public interface JpaSpecificationExecutor<T> {
//根据spec查询一个Optional的实体类【包装】
Optional<T> findOne(@Nullable Specification<T> spec);
//根据spec查询一个实体列表
List<T> findAll(@Nullable Specification<T> spec);
//更具spec查询一个实体分页
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
//查询之后根据sort进行排序
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
//查询满足spec条件的实体长度
long count(@Nullable Specification<T> spec);
//查询实体是否存在
boolean exists(Specification<T> spec);
}
Specification提供的toPredicate方法,便于开发人员构造复杂的查询条件
public interface Specification<T> extends Serializable {
long serialVersionUID = 1L;
static <T> Specification<T> not(@Nullable Specification<T> spec) {
return spec == null ? (root, query, builder) -> {
return null;
} : (root, query, builder) -> {
return builder.not(spec.toPredicate(root, query, builder));
};
}
static <T> Specification<T> where(@Nullable Specification<T> spec) {
return spec == null ? (root, query, builder) -> {
return null;
} : spec;
}
default Specification<T> and(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
}
default Specification<T> or(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
}
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
所以在实现项目中的Repository时,往往同时实现JpaRepository和JpaSpecificationExecutor,同时具备各种能力
public interface VeUserRepository extends CrudRepository<VeUser,Integer>, JpaSpecificationExecutor<VeUser> { //接口是可以多继承的
这里可以测试一下: 给出一个时间区间和关键字,查出创建时间在此区间的用户,并且用户包含该关键字
private List<VeUser> getVeUser(@Nullable LocalDateTime start, @Nullable LocalDateTime end, @Nullable String keyWord) {
//查询是按照关键字或者时间组合查询,可能没有关键字
//String.format中%为特殊字符,需要加一个%进行转义 %s%
String nameLike = keyWord == null ? null : String.format("%%s%%",keyWord);
//其中可以为Specification,Lambda实现函数接口
return veUserRepository.findAll(((root, query, criteriaBuilder) -> {
//根据传入参数的不同构造谓词Predicate列表 criteria 条件
List<Predicate> predicates = new ArrayList<>();
//root就是查询的表记录,query就是查询对象,criteriaBuilder就是条件构造器,可以构造between等条件,加入到谓词列表中【比较的条件】
if(start != null && end != null) {
predicates.add(criteriaBuilder.between(root.get("createTime"),start,end));
}
if(nameLike != null) {
predicates.add(criteriaBuilder.like(root.get("ve_name"),nameLike));
}
//查询条件列表转为数组,放入query.where中进行条件查询
query.where(predicates.toArray(new Predicate[0]));
return query.getRestriction(); //得到约束后的查询结果
}));
}
@Test
public void complicateJpaTest_withExecutor() {
//首先创建一个user
VeUser susan = new VeUser();
susan.setUserName("susan");
susan.setId(2);
susan.setWeight(new BigDecimal(50.00));
susan.setHeight(new BigDecimal(168.00));
susan.setBMI(new BigDecimal(18.02));
susan.setCreateTime(LocalDateTime.now());
System.out.println(veUserRepository.save(susan));
//根据时间和关键字查询
List<VeUser> queryWithTime = getVeUser(LocalDateTime.of(2022, 7, 10, 0, 0, 0), LocalDateTime.of(2022, 7, 17, 0, 0, 0), null);
assert queryWithTime != null;
}
这样就可以进行查询了,根据不同的字段和变量创建不同的谓词列表Predictes,主要就是toPredicate方法的构造相关的查询条件并进行查询获得getRestirction
上面的方式都没有直接使用SQL语句,和Mybatis相同,JPA也是可以基于SQL的,因为不同的数据库的SQL语法略有差异,提供了另外的Java Persistence Query Language,JPQL进行sql的编写
使用Sql的关键就是@Query注解,将SQL语句给出,代表该方法对应的是该SQL语句【JPA本身就是解析方法为SQL语句】
//SQL
//下面这个方法名没有遵循关键字的规范
@Query(nativeQuery = true,value = "select ve_name from ve_user where (create_time between ? and ?) and ve_name like ?")
List<VeUser> queryByTimeAndName(LocalDateTime start,LocalDateTime end,String keyWord);
//JPQL
@Query("from User user1 where (user1.createTime between ? and ?) and (user1.ve_name like ?)")
JPQL与SQL通过nativeQuery参数进行区分,当nativeQuery为True时,查询语句当作SQL处理,JPQL语法与SQL非常相似,但是JPQL对表和字段的描述使用的实体类及其属性表达,属性名是区分大小写的,所以属性名一定要对应一致
Spring Data JPA在处理复杂业务系统时,能够像对待对象一样管理两个表之间的关系,因为表就是@Entity自动更新的,根据业务逻辑,可以创建单向或者双向关系
表与表字段之间的关系可以是一对一,或者多对一,或者多对多,使用@ManyToOne等注解即可表示
使用该注解声明表关系时,首先需要关注的是外键的所有者,在外键所有者的实体类中,@OnetoOne注解需要配合==@JoinColum==一起使用,@JoinColum用来声明外键的字段名
@Entity
@Data
@Accessors(chain = true)
@Table(name = "t_class")
public class Class {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "id",nullable = false)
private Integer id;
@Column(name = "class_name")
private String className;
@OneToMany(fetch = FetchType.LAZY,mappedBy = "clazz") //mappedBy可以让Student实体访问Class实体,因为为双向关系
private List<Student> students;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room id")
private ClassRoom classRoom;
}
多对一的关系,上面已经进行了演示,对于Class和Student来说,为一对多关联关系; 在Class表中加@OneToMany,在Student表中加@ManyToOne;单向关系,不能使用mappedBy,同时一般使用的是@ManyToOne,同时也要加上@JoinColum表外键的字段
多对多的关联关系一般需要中间表的协助,通过@JoinTable来指定中间表
其中name指定表名,joinColums指定正向连接字段名,inverseJoinColum指定反向连接的字段名
//多对多为双向关系,另外一个表可以不用@JoinTable批注,而是用mappedBy属性指定
@ManyToMany
@JoinTable(name = "techer_class", joinColums = {@JoinColum(name = "class_id")}, inverseJoinColums = {@JoinColum(name = "teacher_id")})
private Set<Class> classes;
级联操作基于多表连接,在上面的@ManyToOne等注解中,包含一个cascade属性设置表之间的级联操作,描述多个表更新之后所发生的级联反应
级联操作有不同的等级:
加载类型FetchType,和cascade一样是关系注解的配置项,加载类型分为EAGER,和LAZY,LAZY为默认值,EAGER就是关联实体立刻假爱,而LAZY是需要时才会加载,执行SQL语句
也就是当为LAZY只是执行当前表的SQL语句,当需要其他的表的时候才会进行连接操作,但是EAGER就是只要使用到当前表,就会加上复杂的表连接语句
所以fetchType(取类型)一般设置为LAZY
除了关系型数据库之外,还有非关系型数据库占了很重要的一部分,MongoDB作为文档型数据库,具有高性能,易部署的特性;
在SpringBoot中,we使用Spring Data MongoDB集成MongoDB数据库
这里就不专门介绍,直接到官网下载安装包解压安装即可,注意配置一下环境变量,这样就可以方便使用bin命令了
D:\>cd D:\MongoDB\Server\4.2\bin
D:\MongoDB\Server\4.2\bin>mongod -dbpath D:\MongoDB\Server\4.2\data\db
2022-07-17T18:07:04.297+0800 I CONTROL [main] Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'
2022-07-17T18:07:04.672+0800 W ASIO [main] No TransportLayer configured during NetworkInterface startup
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] MongoDB starting : pid=13004 port=27017 dbpath=D:\MongoDB\Server\4.2\data\db 64-bit host=DESKTOP-4A4BD0R
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] targetMinOS: Windows 7/Windows Server 2008 R2
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] db version v4.2.6
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] git version: 20364840b8f1af16917e4c23c1b5f5efd8b352f8
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] allocator: tcmalloc
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] modules: none
2022-07-17T18:07:04.675+0800 I CONTROL [initandlisten] build environment:
这样就开启了服务,一定要在data下面创建db文件夹
之后就可以新建一个窗口进行相关的mongo操作,直接使用mongo命令即可
C:\Users\OMEY-PC>mongo
MongoDB shell version v4.2.6
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("ca813d51-d8a3-43df-9420-60abc3e0efa8") }
MongoDB server version: 4.2.6
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
http://docs.mongodb.org/
Questions? Try the support group
http://groups.google.com/group/mongodb-user
Server has startup warnings:
2022-07-17T18:07:04.747+0800 I CONTROL [initandli
同时还可以安装可视化客户端mongodb compass,安装之后建立连接即可【server要一直打开】,url就是 mongodb://127.0.0.1:27017,也就是本机的27107端口
想要使用Spring Data MongoDB,只需要引入相关的stater即可,其中就有相关的template对象
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-mongodbartifactId>
dependency>
<!-- 本来这样就可以了,但是其中的driver-async 4.6.1一直无法解析,所以只好排除之后降低版本
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-mongodbartifactId>
dependency>
也就是data-mongoDB,之前的jdbc中就是jdbcTemplate,data-jpa就是Spring Data JPA
Spring Data MongoDB提供两种方式访问数据:
先把MongoDB数据库跑起来: C:\Users\xxx-PC>mongod -dbpath D:\MongoDB\Server\4.2\data\db
Spring Data MongoDB可以通过创建配置类集成AbstractMongoClientConfiguration进行配置,或者神功MongoClient或者MongoTemplate的JavaBean实现
主要是配置数据库名,相关的url和其它的一些配置
data:
mongodb:
host: localhost
port: 27017
database: cfengBase
可以直接在yaml中进行配置,或者采用配置类的方式,这里可以通过@Value引入配置动态修改
public class MongoConfig extends AbstractMongoClientConfiguration {
@Override
protected String getDatabaseName() {
return "cfengBase";
}
@Override
public MongoClient mongoClient() {
ConnectionString connectionString = new ConnectionString("mongodb://localhost:27107/cfengBase");
MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString).build();
return MongoClients.create(mongoClientSettings);
}
@Override
protected String getMappingBasePackage() {
return Collections.singleton("indvi.cfeng.persistencedemo");
}
除了继承AbstractMongoClientConfiguration之外,还可以通过JavaBean的方式
@Configuration
public class MongoConfig {
@Bean
public MongoClient mongo() {
ConnectionString connectionString = new ConnectionString("mongodb://localhost:27107/cfengBase");
MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString).build();
return MongoClients.create(mongoClientSettings);
}
@Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongo(),"cfengBase");
}
}
Spring Data MongoDB的基本文档查询就是依赖的MongoTemplate常见的方法就是增删改查
insert,save,updateFirst,updateMulti,findAndModify,upsert,remove
在类上面加上@Document表明由MongoDB维护,就类似与之前的Spring Data JPA中的实体类注解==@Entity,还有@Indexed和@CompoundIndex为索引和复合索引, 使用@Filed指定在MongoDB数据库中对应的字段名,和之前的JPA的@Colum类似==
@Data
@Document("MongoWare")
public class MongoWare {
@Id
private Integer id;
@Field(value = "mogo_name")
private String mogoName;
@Field(value = "mogo_class")
private String mogoClass;
}
@Transient和之前的JPA中一样,就是该字段作为普通的属性,不录入到数据库中
Mongdb不支持范型,在使用过程中需要注意不要定义范型
这个方法用于初始化时将数据写入数据库,可以指定Collection,不指定就是默认类名
通过id判断实体是否存在与数据库中,存在就为更新操作,不存在就是插入操作
这里的查询使用的是Query对象,new Query()之后,addCriteria查询条件,Criteria.where等着和之前的JpaSpecializationExecutor是类似的,就是组合谓词列表
而Update对象则用来进行更新,update的set就和之前的mysql类似,会将查询到的对象的某个属性重新set,
EntityClass就是识别MongoDB管理的entity类型
updateFirst方法只是更新查询结果的第一条记录,查询结果可能有很多
和上面的查询不同,会更新多条记录,所有都会更新,和Mysql类似
和UpdateFirst只是返回值不同
查找更新创建实体,无则创建,有则更新,save是根据id判断,这里是根据查询条件判断
remove((Query query,Class> EntityClass) 将符合查询条件的对象移除数据库
这里也是根据查询条件移除
@Resource
private MongoTemplate mongoTemplate;
/**
* 使用MongoTemplate访问更加灵活,可选择Collection等,Repository方式更加简单,不管是Mysql还是redis也是一样,Repository直接操作即可,封装比较彻底
*/
@Test
public void testMongoTemplate_insert() {
MongoWare mongoWare = new MongoWare();
mongoWare.setMogoName("矿泉水");
mongoWare.setId(4);
mongoWare.setMogoClass("HC3001");
//插入数据库
mongoTemplate.insert(mongoWare,"testdata");
}
@Test
public void testMongoTemplate_save() {
MongoWare mongoWare = new MongoWare();
mongoWare.setId(4);
mongoWare.setMogoName("矿泉水");
mongoWare.setMogoClass("HC2001");
//save id有则更新
MongoWare mongoWare1 = new MongoWare();
mongoWare1.setId(3);
mongoWare1.setMogoName("矿泉水");
mongoWare1.setMogoClass("HC2009");
mongoTemplate.save(mongoWare1);
mongoTemplate.save(mongoWare);
//这里的执行结果在MongoWare这个集合中重新创建该对象,上面的id同的对象在testdata集合中,不同
}
@Test
public void testMongoTemplate_updateFirst() {
//有两个矿泉水,查询矿泉水
Query query = new Query();
query.addCriteria(Criteria.where("mogoName").is("矿泉水"));
Update update = new Update();
update.set("mogoName","xiaoBao");
mongoTemplate.updateFirst(query,update,MongoWare.class);
//这里的执行结果就是修改了一个对象
}
@Test
public void testMongoTemplate_updateMulti() {
Query query = new Query();
query.addCriteria(Criteria.where("mogoName").is("xiaoBao"));
Update update = new Update();
update.set("mogoName","xiaoHuan");
mongoTemplate.updateMulti(query,update,MongoWare.class);
//两个对象均被修改为xiaoHuan
}
@Test
public void testMongoTemplate_remove() {
Query query = new Query();
query.addCriteria(Criteria.where("mogoName").is("矿泉水"));
mongoTemplate.remove(query,MongoWare.class);
//成功移除了对象,其他的方法都很简单,就不一一演示
}
可以看到执行结果符合预期
Redis是直接当作和Mysql等一样,直接继承JpaRepository即可,但是MongoDB有海量数据,所以使用更加独特的MongoRepository,MongoRepository和JPA中的Repository很相似,继承了PagingAndSortingRepository
使用Repository的方式访问,首先就是创建MongoDB管理的实体类
@Data
@Document("MongoWare")
public class MongoWare {
@Id
private Integer id;
@Field(value = "mogo_name")
private String mogoName;
@Field(value = "mogo_class")
private String mogoClass;
}
之后就是创建Repository继承MongoRepository
public interface WareMongoRepository extends MongoRepository<MongoWare,Integer> {
//这里的Mongo使用Repository和Reids有所不同,Redis是直接使用的JPA其他数据库一样的CRUDRepository,但是Mongo使用的是MongoRepository
}
其中已经提供了很多基础的方法,这里就先不另外增加了,也是要遵守规范定义方法名称,**为了能够让启动类识别该Repository,需要加上注解@EnableMongoRepositories
@SpringBootApplication @EnableMongoRepositories(basePackages = "indvi.cfeng.persistencedemo.repository") public class PresisApplication {
database就是之前config中设置的base,但是集合自己选定,saveAll方法就是mogorepository的,保存给出的所有的实体对象
这里的关键就是Example匹配对象,类似于Objects,使用方式: Example.of(构造的对象), 与构造的对象进行匹配,eg: Example.of(new Student().setName(“zs”))
查询所有的记录即可
这里的pageable就是PageRequest.of(0,10),类似与之前的PageHelper
接下来我们就可以使用这个repository进行相关的访问,repository默认创建和类同名的集合
@Resource
private MongoRepository<MongoWare,Integer> mongoRepository;
@Test
public void testMongoRepository_saveAll() {
List<MongoWare> wares = new ArrayList<>();
MongoWare ware1 = new MongoWare();
ware1.setId(1);
ware1.setMogoName("xiaoHuan");
ware1.setMogoClass("HC2001");
MongoWare ware2 = new MongoWare();
ware2.setId(2);
ware2.setMogoName("xiaoBao");
ware2.setMogoClass("HC2002");
wares.add(ware1);
wares.add(ware2);
mongoRepository.saveAll(wares);
}
@Test
public void testMongoRepository_exists() {
MongoWare Cfeng = new MongoWare();
Cfeng.setMogoName("Cfeng");
boolean isCfengExist = mongoRepository.exists(Example.of(Cfeng));
System.out.println(isCfengExist ? "存在" : "不存在");
}
@Test
public void testMongoRepository_findAllWithSort() {
//这里按照mogoName升序排列,xiaoBao在前
System.out.println(mongoRepository.findAll(Sort.by(Sort.Direction.ASC,"mogoName")));
}
//[MongoWare(id=2, mogoName=xiaoBao, mogoClass=HC2002), MongoWare(id=1, mogoName=xiaoHuan, mogoClass=HC2001)]
@Test
public void testMongoRepository_findAllwWithPage() {
//这里使用pageRequest.of只显示第一页1条记录
System.out.println(mongoRepository.findAll(PageRequest.of(0,1)));
//Page 1 of 2 containing indvi.cfeng.persistencedemo.entity.MongoWare instances
}
我们可以查看compass中的结果,正常,【其他的几个测试结果放在注解程序内文档中】
在NoSQL中,MongoDB是适合处理海量的易于扩展的数据,而Redis是更加注重性能,作为内存型键值数据库,大多数开发中,Redis当作缓存使用,用于缓存其他数据库中的热点数据,提高查询性能
SpringBoot中使用Redis是依靠的Spring Data Redis,和MongoDB类似,也提供了Template和repository两种访问的方式
还是引入相关的起步依赖就可以使用相关的template对象
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
连接Redis由两种客户端解决方案: Jedis和莴苣lettuce; Jedis和dos命令类似,Spring Data Redis默认集成Lettuce,如果要使用Jedis作为客户端,需要额外引入Jedis依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.3.0version>
dependency>
Spring Data Redis提供了RedisTemplate和StringRedisTemplate;二者的区别就是
默认使用的是JDK序列化,序列化就是将对象转为可传输的字节
反序列化就是将字节序列还原为对象,序列化必须要实现Serlizable接口,定义相关的序列号
序列化的目的是为了对象跨平台和网络传输,网络传输使用的IO为字节传输,要传输对象就必须序列化
在settings中的editor下面的inspections中选择serilizable without UUID... public class Vehicle implements Serializable { private static final long serialVersionUID = 9019832572160148201L;
可以设置key或者value的u序列化方式: redisTemplate.setKeySerializer(new StringRedisSerializer()) SetValue…; redisTemplate.opsForValue().set(k,v);
redis:
port: 6379
host: localhost
password:
jedis:
pool:
max-active: 10
max-idle: 8
min-idle: 1
max-wait: 1
connect-timeout: 6000
配置之后就可以使用RedisTempLate访问
@SpringBootTest
public class redisTests {
@Resource
private RedisTemplate redisTemplate;
@Test
public void testRedis() {
//字符串类型操作valueOperations
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("class","HC2001");
//取出结果
System.out.println(valueOperations.get("class"));
}
}
这里就可以成功连接Windows上面的redis【linux版本需要开虚拟机,关闭防火墙】
上面只是简单的配置,并且使用的是Lettuce作为客户端,这里的配置是在yaml中进行配置,也可以采用javaConfig的方式进行配置,可以配置采用Jedis客户端
@Configuration
public class RedisConfig {
//使用Lettuce作为客户端需要声明LettuceFactory Bean
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
//redis独立配置host和端口standalone
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost",6379);
//有密码也需要通过这个对象设置
// redisStandaloneConfiguration.setPassword("xxxx");
//设置首先采用的数据库为1号数据库
redisStandaloneConfiguration.setDatabase(1);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
//使用jedis作为客户端声明该Bean
// @Bean
//public JedisConnectionFactory jedisConnectionFactory() {
// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost",6379);
// redisStandaloneConfiguration.setPassword("3434");
//redisStandaloneConfiguration.setDatabase(1);
//return new JedisConnectionFactory(redisStandaloneConfiguration);
//}
//设置RedisTemplate相关属性,注入Bean
@Bean
public RedisTemplate<?,?> redisTemplate() {
RedisTemplate<String,String> template = new RedisTemplate<>();
//序列化器
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
//设置template的连接的相关属性等
//使用jedis作为客户端
template.setConnectionFactory(jedisConnectionFactory());
//设置key,hashKey的序列化方式为String,可读性好【这样就类似StringRedisTemplate】
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//设置hashValue,Value的序列化方式为jdk方式,因为容量更大,jdk序列化传输更好
template.setValueSerializer(jdkSerializationRedisSerializer);
template.setHashValueSerializer(jdkSerializationRedisSerializer);
//设置事务
template.setEnableTransactionSupport(true);
//默认的序列化器,如果不设置将...
template.afterPropertiesSet();
return template;
}
}
当然这里we如果要使用Jedis,那么就需要添加Jedis的依赖
java.lang.ClassNotFoundException: redis.clients.jedis.JedisClientConfig
报错就是因为没有启用Jedis客户端,启用Jedis,因为使用了Commons-pool,所以需要配置JedisClientConfiguration.JedisPoolingClientConfigurationBuilder创建一个JedisPoolConfig对象,这个对象的属性可以再yaml中配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用jedis作为客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<!-- <version>3.3.0</version>-->
</dependency>
//这里必须要exclude,不然下面的配置不能生效,默认还是会采用lettuce
这里就派出了lettuce-core,加入了Jedis的依赖,这样使用的就是Jedis的客户端了
缓存之后Spring就会在该方法执行依次之后将返回值缓存到内存中,下次利用相同的参数来执行该方法的时候就不会直接执行,而是直接从缓存中获取结果,键执行hi默认策略和自定义策略,@Cacheable的3个属性:value,key,condition,Cache就类似一个大Map,有很多Cache,方法放在哪个缓存中,需要指定名称 ------> value指定,cacheNames; key是方法返回值对应的key,默认采用方法参数创建
而Cache需要CacheManager的支持,ConcurrentMapCacheManager内部使用的ConcurrentMap实现
接下来演示按照Jedis配置,再yaml中配置,再创建配置类,再配置类中将yaml中的配置项注入,便于当作属性直接修改
@Primary的作用就是标记Bean,当byType注入的时候有多个bean符合时,注入@Primary标记的Bean
对于yaml中单独的一项,使用@Value(“${}”)注入,对于一个prefix下面的,就直接使用ConfigurationProperties注入给一个对象
将yaml中配置的jedis的所有选项注入给属性,并且将jedisPool的配置注入给一个JedisPoolConfig对象
//yaml配置
redis:
port: 6379
host: localhost
database: 1
timeout: 1000
password:
jedis:
pool:
max-active: 10
max-idle: 8
min-idle: 1
max-wait: 1
//配置类
@Configuration
@EnableCaching //在配置类中加入该注解,代表启用缓存功能【缓存就是局部性原理】
public class RedisConfig {
//将yaml中的属性注入
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.database}")
private Integer database;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Primary //标记优先级最高,当byType注入的时候优先
@Bean(name = "jedisPoolConfig")
@ConfigurationProperties(prefix = "spring.redis.jedis.pool") //将其中pool下面的属性当作一个对象注入给pool
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxWait(Duration.ofSeconds(10));
return jedisPoolConfig;
}
//使用jedis客户端
@Bean
//上面的bean拿下来使用
public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host,port);
redisStandaloneConfiguration.setDatabase(database);
// redisStandaloneConfiguration.setPassword(password);
//jedis客户端配置,创建poolConfig,这里要加上cast
JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jedisPoolClientBuilder = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
//使用poolConfig装载pool对象
jedisPoolClientBuilder.poolConfig(jedisPoolConfig);
JedisClientConfiguration jedisClientConfiguration = jedisPoolClientBuilder.build();
//不仅仅配置port等,还要将配置的jedisPool加入
return new JedisConnectionFactory(redisStandaloneConfiguration,jedisClientConfiguration);
}
//需要使用的参数会自动注入容器中的对象,其他的和之前的相同
@Primary
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//设置value的序列化器为Json,JDK可读性差
Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer(Object.class);
template.setValueSerializer(redisSerializer);
template.setHashValueSerializer(redisSerializer);
//设置默认
template.afterPropertiesSet();
return template;
}
配置的时Jedis的factory,而@ConfigurationProperties可以直接用在对象上面,不只是放在类上面,可以直接放在@Bean创建的对象上面,再将yaml中配置的注入即可,所以一般都是采用yaml + JavaConfig结合的方式
redisRepoitory使用的是Spring Data中通用的Repository的风格
和前面的Jpa和MongoDB类似,Jedis风格的实体类采用@RedisHash,代表实体存储在RedisHash中,使用Repository访问,一定需要这个注解,timeToLive标注存活时间,单位为s,@Indexed和之前的一样代表添加索引
主要注解和之前的Jpa的@Entity和MongoDB中的@Document类似
/**
* @author Cfeng
* @date 2022/7/18
* Redis是集群部署,实体hash对象需要进行网络传输,需要序列化,序列化的方式一般为jdk或者json
*/
@Data
@Accessors(chain = true)
@RedisHash(value = "Student",timeToLive = 10) //设置存活时间10s
public class RedisStudent {
//性别枚举
public enum Gender {
MALE,FEMALE
}
private String id;
@Indexed //redis中的
private String name;
//性别
private Gender gender;
private int grade;
}
接下来创建一个Repository,使用@Repository就会创建该访问对象到容器
/**
* @author Cfeng
* @date 2022/7/18
* Repository的方式相当于还是使用JPA,只是存储就会自动识别为redis存储
*/
@Repository
public interface StudentRedisRepository extends CrudRepository<RedisStudent,String> {
//自定义查询方式,使用@Indexed属性进行查询
RedisStudent findByName(String name);
}
JPA框架可以动态适应不同的数据库,所以这里就可以自动匹配Redis数据库
@Resource
private StudentRedisRepository studentRedisRepository;
@Test
public void testRedis() {
//字符串类型操作valueOperations
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("class","HC2001");
//取出结果
System.out.println(valueOperations.get("class"));
}
@Test
public void testRedisSave_withJpaAndRepository() {
RedisStudent student = new RedisStudent().setId("20220101001").setName("Cfeng").setGender(RedisStudent.Gender.MALE).setGrade(1);
//根据ID新增记录
System.out.println(studentRedisRepository.save(student));
}
@Test
public void testRedisSelect_withJpaAndRepository() {
assert studentRedisRepository.findById("20220101001").isPresent();
//根据自定义方法查询,符合JPA规范
assert studentRedisRepository.findByName("Cfeng") != null;
System.out.println(studentRedisRepository.findByName("Cfeng"));
}
@Test
public void testRedisDelete_withJpaAndRepository() {
studentRedisRepository.deleteById("20220101001");
assert studentRedisRepository.findById("20220101001").isPresent();
}
@Test
public void testRedisFindAll_withJpaAndRepository() {
studentRedisRepository.save(new RedisStudent().setId("20220101002").setGender(RedisStudent.Gender.MALE).setName("huan").setGrade(2));
studentRedisRepository.save(new RedisStudent().setId("20220101003").setGender(RedisStudent.Gender.FEMALE).setName("bao").setGrade(3));
List<RedisStudent> redisStudentList = Lists.newArrayList(studentRedisRepository.findAll());
assert redisStudentList.size() > 0;
System.out.println(redisStudentList);
}
这里加入的这个hash对象就会加入到Redis数据库中,所以JPA也是可以连接Redis数据库的,就是Repository的方式,因为Spring Data是不关心底层数据库的,包括MongoDB都是可以使用Repository的方式,只要在实体类加上不同的标记,对象就会被不同的数据库维护
127.0.0.1:6379[1]> keys *
- “Student:20220101001:idx”
- “Student”
- “Student:name:Cfeng”
可以查询windows-redis数据库中已经有hash对象了
相较于固定的使用JPA的方式访问Redis,使用RedisTemplate的方式会更加灵活,RedisTemplate是基于原生的Redis命令一系列操作方法,基于5种基础的数据结构:String,List,Hash,Set,ZSet,当然因为Lettuce封装之后的操作命令和原生的有了一些区别,可以重新封装Redis的操作方法使之和原生的操作命令相同
@SpringBootTest
public class redisTests {
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Test
public void testString_withTemplate() {
//字符串类型操作valueOperations,设置键值对
// ValueOperations valueOperations = redisTemplate.opsForValue();
redisTemplate.opsForValue().set("stuName","Cfeng");
System.out.println(redisTemplate.opsForValue().get("stuName"));
//设置带有有效时间的set,设置单位TimeUnit为秒
redisTemplate.opsForValue().set("stuClass","Hc1920",10, TimeUnit.SECONDS);
//10s后该值为null,keys * 找不到该key
redisTemplate.opsForValue().get("stuClass");
}
@Test
public void testList_withTemplate() {
//列表类型的,使用leftPush等方法,列表也是一个key
redisTemplate.opsForList().leftPush("Kids","yeOne");
redisTemplate.opsForList().leftPush("Kids","yeTwo");
redisTemplate.opsForList().leftPush("Kids","yeThree");
//查询列表长度
assert redisTemplate.opsForList().size("Kids") == 3;
System.out.println(redisTemplate.opsForList().range("Kids",1,3));
//弹出左右的元素
assert Objects.equals(redisTemplate.opsForList().leftPop("Kids"),"yeThree");
assert Objects.equals(redisTemplate.opsForList().rightPop("Kids"),"yeOne");
}
@Test
public void testHash_withTemplate() {
//更新hash第一个参数为hash的键,第二个该hash内的键值对
redisTemplate.opsForHash().put("employee","empName","Chuan");
redisTemplate.opsForHash().put("employee","emAge",34);
redisTemplate.opsForHash().put("employee","emHeight",134.00);
// System.out.println(redisTemplate.opsForHash().get("employee","empName"));
//get获取,keys获取所有
Set<Object> employee = redisTemplate.opsForHash().keys("employee");
//这里要强制转型【先上转型再下转型的】, 不然类型不对应,抛异常
employee.forEach(key -> System.out.println(key + ":" + redisTemplate.opsForHash().get("employee",(String)key)));
}
@Test
public void testZset_withTemplate() {
//新增zset的内容
redisTemplate.opsForZSet().add("Teacher","miss Zhang",1);
redisTemplate.opsForZSet().add("Teacher","Mr Li",3);
redisTemplate.opsForZSet().add("Teacher","Ms Liu",8);
//获取zset种sir权重
System.out.println(redisTemplate.opsForZSet().score("Teacher","Mr Li"));
//根据权重排序zset
redisTemplate.opsForZSet().range("Teacher",0,-1).forEach(System.out::println);
}
redisTemplate的方法都是封装后的,后原生的Redis的命令名称不同,封装一下,如果要使用Jedis,那么使用的就是JedisPool,再加入其他的配置就可
实际使用redis缓存的场景,需要让java数据类型和redis六种数据结构的命令对应,这里就将redisTemplate封装为全局的CacheTemplate,按照面向接口的思想,首先定义一个IGlodbalCache接口为缓存封装模板的接口
在项目的cache包下面定义接口及其实现类
package indvi.cfeng.persistencedemo.cache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Cfeng
* @date 2022/7/18
* 进一步封装redisTemplate,封装的接口规范
*/
public interface IGlobalCache {
/**
*首先就是expire key seconds 设置生存时间,返回值为设置是否成功
*/
boolean expire(String key, long time);
/**
* ttl key 返回key剩余的生存时间
*/
long ttl(String key);
/**
* exists key 查看key是否存在,java中还是直接单独判断合适
*/
boolean exist(String key);
/**
* del key 删除key
* 参数可选多个
*/
void del(String... key);
//select database只能通过配置文件修改
//===========================String类型的相关操作=================
/**
* set key value [timeToLive]
* get key
* incr/decr key 将key数字值加1
*/
boolean set(String key,Object value);
boolean set(String key, Object value, long timeToLive);
Object get(String key);
//java中incr加上自定义数值delta
long incr(String key, long delta);
long decr(String key, long delta);
//======================Hash=============
/**
* hset key field value
* hget key field
* hmset hmget hgetall
* hdel
* hexists
*/
Object hget(String key, String field);
Map<Object, Object> hmget(String key);
boolean hset(String key, String field, Object value);
boolean hset(String key ,String field, Object value, long timeToLive);
boolean hmset(String key, Map<String,Object> map);
boolean hmset(String key, Map<String,Object> map, long timeToLive);
void hdel(String key, Object... field);
boolean hexist(String key, String field);
//hash递增和递减
double hincr(String key , String field, double delta);
double hdecr(String key, String field, double delta);
//=================Set无序集合==================
/**
* sadd key member
* sismember key member
* scard key
* srem key member ..
* spop key count
* smembers key 获取集合中所有
*/
Set<Object> smembers(String key);
boolean sismember(String key,Object member);
long sadd(String key, Object... members);
long sadd(String key, long timeToLive, Object... members);
long scard(String key);
long srem(String key, Object... members);
//=============list列表=======================
/**
* lpush/pop key value
* rpush/pop key value
* lindex key index
* lrem key
* llen key
* lset key index value 设置index位置的value
* lrange key start stop
*/
List<Object> lrange(String key,long start, long end);
long llen(String key); //获取长度
Object lindex(String key, long index);
boolean lpush(String key, Object value);
boolean lpush(String key, Object value, long timeToLive);
boolean lpushAll(String key, List<Object> value);
boolean lpushAll(String key, List<Object> value, long timeToLive);
boolean rpush(String key, Object value);
boolean rpush(String key, Object value, long timeToLive);
boolean rpushAll(String key, List<Object> value);
boolean rpushAll(String key, List<Object> value, long timeToLive);
boolean lset(String key, long index, Object value);
long lrem(String key, long count, Object value);
//移除start和end之间的元素
void lrangeRem(String key, long start, long end);
//返回当前的redis对象
RedisTemplate getRedisTemplate();
}
上面简单定义了String,hash,set,list数据结构的几种简单的操作方法的封装,接下来就是调用RedisTemplate实现这个接口,创建实现类RedisCacheTemplate
package indvi.cfeng.persistencedemo.cache.impl;
import indvi.cfeng.persistencedemo.cache.IGlobalCache;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author Cfeng
* @date 2022/7/19
* 封装redisTemplate,遵循之前定义的接口规范
*/
@Getter
@AllArgsConstructor
@Component //创建一个单例Bean放入容器
public class RedisCacheTemplate implements IGlobalCache {
private RedisTemplate<String,Object> redisTemplate;
@Override
public boolean expire(String key, long time) {
//这里就设置过期时间,但是可能发生异常,发生异常就捕获返回false
try {
if(time > 0) {
redisTemplate.expire(key,time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public long ttl(String key) {
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
@Override
public boolean exist(String key) {
//可变参数为一个args[]数组
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public void del(String... key) {
//可变参数就要判断为多少个,刚好有删除集合的方法delete collection
if(key != null && key.length > 0) {
if(key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
}
}
}
@Override
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key,value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean set(String key, Object value, long timeToLive) {
try {
if(timeToLive > 0) {
redisTemplate.opsForValue().set(key, value, timeToLive,TimeUnit.SECONDS);
} else {
this.set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
@Override
public long incr(String key, long delta) {
if(delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key,delta);
}
@Override
public long decr(String key, long delta) {
if(delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().decrement(key, delta);
}
@Override
public Object hget(String key, String field) {
return redisTemplate.opsForHash().get(key,field);
}
@Override
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public boolean hset(String key, String field, Object value) {
try {
redisTemplate.opsForHash().put(key,field,value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean hset(String key, String field, Object value, long timeToLive) {
try {
redisTemplate.opsForHash().put(key,field,value);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key,map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean hmset(String key, Map<String, Object> map, long timeToLive) {
try {
redisTemplate.opsForHash().putAll(key,map);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public void hdel(String key, Object... field) {
//刚好可选参数
redisTemplate.opsForHash().delete(key,field);
}
@Override
public boolean hexist(String key, String field) {
return redisTemplate.opsForHash().hasKey(key,field);
}
@Override
public double hincr(String key, String field, double delta) {
return redisTemplate.opsForHash().increment(key,field,delta);
}
@Override
public double hdecr(String key, String field, double delta) {
//这里直接-delta就是递减
return redisTemplate.opsForHash().increment(key,field,-delta);
}
@Override
public Set<Object> smembers(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public boolean sismember(String key, Object member) {
try {
return redisTemplate.opsForSet().isMember(key, member);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public long sadd(String key, Object... members) {
try {
return redisTemplate.opsForSet().add(key,members);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public long sadd(String key, long timeToLive, Object... members) {
try {
long count = redisTemplate.opsForSet().add(key,members);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public long scard(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public long srem(String key, Object... members) {
try {
return redisTemplate.opsForSet().remove(key,members);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public List<Object> lrange(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public long llen(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public Object lindex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public boolean lpush(String key, Object value) {
try {
redisTemplate.opsForList().leftPushIfPresent(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean lpush(String key, Object value, long timeToLive) {
try {
redisTemplate.opsForList().leftPushIfPresent(key, value);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean lpushAll(String key, List<Object> value) {
try {
redisTemplate.opsForList().leftPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean lpushAll(String key, List<Object> value, long timeToLive) {
try {
redisTemplate.opsForList().leftPushAll(key, value);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean rpush(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean rpush(String key, Object value, long timeToLive) {
try {
redisTemplate.opsForList().rightPush(key, value);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean rpushAll(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean rpushAll(String key, List<Object> value, long timeToLive) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if(timeToLive > 0) {
this.expire(key,timeToLive);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean lset(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public long lrem(String key, long count, Object value) {
try {
long rem = redisTemplate.opsForList().remove(key, count, value);
return rem;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public void lrangeRem(String key, long start, long end) {
try {
redisTemplate.opsForList().trim(key, start, end);
} catch (Exception e) {
e.printStackTrace();
}
}
}
接下来测试使用封装的Template来操作Redis
@Resource
private IGlobalCache redisCache;
@Test
public void testJedis_withCache() {
redisCache.set("xiaohuan","isPig");
redisCache.lpushAll("xiaohuanlist", Arrays.asList("hello","redis"));
List<Object> list = redisCache.lrange("xiaohuanlist",0,-1);
System.out.println(redisCache.get("xiaohuan"));
}
查询数据库操作正确,上面创建的自定义封装类是需要创建对象的,不然无法直接使用,其实还是使用RedisTemplate,只是方法名自定义,和redis原生命令一致
接下来会分享Spring Security的内容,就不用其他的安全框架了