当前位置:网站首页>【开发技术】SpingBoot数据库与持久化技术,JPA,MongoDB,Redis
【开发技术】SpingBoot数据库与持久化技术,JPA,MongoDB,Redis
2022-07-22 21:12:00 【码农C风】
sSpringBoot
内容管理
- 使用JdbcTemplate访问RDB
- JPA 【java Persistence API】
- Spring Data MongoDB
- 安装MongoDB
- 引入stater依赖
- 继承AbstractMongoClientConfiguration配置
- 使用MongoTemplate访问MongoDB
- 创建MongoDB的实体类@Document
- insert(T objectToSave, String collectionName) 用于在初始化时插入到数据库的集合中
- save(T objectToSave) 保存实体,插入或者更新
- updateFirst(Query query,UpdateDefintion update,Class<?> EntityClass) 更新查询到的第一条记录,返回值为UpdateResult更新后的结果
- updateMulti((Query query,UpdateDefintion update,Class<?> EntityClass)批量更新
- findAndModify((Query query,UpdateDefintion update,Class<?> EntityClass)更新第一条记录,返回值为更新前记录
- upsert((Query query,UpdateDefintion update,Class<?> EntityClass) 类似与save,但是这是根据query的结果
- 使用MongoRepository访问MongoDB【和Redis有所区别】
- Spring Data Redis
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】
使用JdbcTemplate访问RDB
JDBC是最初访问数据库的手段,是java提供的编写应用程序作为客户端访问数据库的API,JdbcTemplate是对于JDBC的封装(JDBC编程6步 – 有些固定的代码)简化了JDBC的使用,
当请求数据库的需求不复杂的时候,就可以简单使用JDBCTemplate进行数据库的访问,使用JDBCTemplate,就简单的引入相关依赖,在SpringBoot中,需要引入的起步依赖就是Spring-boot-starter-jdbc; 数据库的驱动使用mysql-connecter即可
<!-- 使用jdbcTemplate进行数据库的访问-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Spring Boot自动初始化数据库 加载schema.sql和data.sql
Spring Boot中可以进行相关配置,使得SpringBoot自动从classpath下面加载schema.sql和data.sql,前者决定表结构,后者进行数据的初始化。如果使用不同的数据库,可以在yml中进行配置,配置spring.datasource.platform区分环境,同时建立不同的文件,比如: schema-${platform}.sql和data-…sql
初始化行为通过spring.datasource.initializetion-mode进行控制:
- DataSourceInitializationMode.ALWAYS : 总是执行初始化操作
- DataSourceInitializationMode.EMBEDDED: 仅数据源为嵌入式数据库的时候执行
- DataSourceInitializationMode.NEVER: 从不执行初始化操作
默认情况下,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脚本,需要注意不要出现语法错误
JdbcTemplate.queryForObject
使用该方法可以执行一条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);
}
}
使用RowMapper映射实体
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使用?作为占位符
使用BeanPropertyRowMapper映射
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"));
};
jdbcTemplate.queryForList
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}
NamedParameterJdbcTemplate可以使用有含义的占位符取代 ?
使用简单的JdbcTemplate,其占位符就是 ? ,当占位符过多时,语句含义难以理解,这个时候可以选择使用NamedParameterJdbcTemplate进行代替,可以参照ESQL的 :xxxx
- 占位符的映射操作就需要使用MapSqlParameterSource操作,参数名需要和SQL语句的占位符保持一致,否则会报错No value supplied for the SQL parameter ‘name’: No value registered
//@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)
jdbcTemplate.update()
上面的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 Persistence API】
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的存储库。Spring Data JPA主要就是基于JPA进行数据访问层的增强,JPA是规范,Spring Data JPA是一个全自动的数据访问层的框架,使用之后就需要编写Repository接口即可,框架会自动提供对应的实现

该框架是遵守JPA规范的,在JPA规范下提供Repository的实现,提供配置项用以切换具体实现规范的ORM框架
基于JpaRepository和CrudRespository接口查询
基于接口的查询方式,更具接口名的定义自动生成相关的SQL语句的代理实例,不需要手写SQL,并且还提供了基础的CRUD的实现,继承了之后就会默认为@Repository,会自动创建实例,不需要再添加该注解
- Repostory是Spring Data JPA的核心接口,需要领域实体类domain和实体类entity的ID类型作为类型参数进行管理,该类的作用作为标记接口,捕获要使用的类型和扩展接口子接口
//看看源码
@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(?)
基于JpaSpecificationExecutor接口查询
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
基于JPQL和SQL, 依赖注解@Query
上面的方式都没有直接使用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 一对一
使用该注解声明表关系时,首先需要关注的是外键的所有者,在外键所有者的实体类中,@OnetoOne注解需要配合[email protected]==一起使用,@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;
}
@OneToMany 和@ManyToOne
多对一的关系,上面已经进行了演示,对于Class和Student来说,为一对多关联关系; 在Class表中加@OneToMany,在Student表中加@ManyToOne;单向关系,不能使用mappedBy,同时一般使用的是@ManyToOne,同时也要加上@JoinColum表外键的字段
@ManyToMany
多对多的关联关系一般需要中间表的协助,通过@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;
级联操作cascade
级联操作基于多表连接,在上面的@ManyToOne等注解中,包含一个cascade属性设置表之间的级联操作,描述多个表更新之后所发生的级联反应
级联操作有不同的等级:
- PERSIST: 级联保存 ,当前实体保存,相关联的实体也保存
- REMOVE: 级联删除,当前实体删除, 也删除
- MERGE: 级联合并,当前实体数据更新, 也更新
- REFRESH: 级联刷新,A,B同时操作一个订单实体及其相关数据,A先于B修改保存,那么B操作的时候,就需要先刷新订单实体,再进行保存
- DETACH: 级联脱离 实体与其他实体的联系分离
- ALL: 包含上面的所有级联
加载类型fetchType
加载类型FetchType,和cascade一样是关系注解的配置项,加载类型分为EAGER,和LAZY,LAZY为默认值,EAGER就是关联实体立刻假爱,而LAZY是需要时才会加载,执行SQL语句
也就是当为LAZY只是执行当前表的SQL语句,当需要其他的表的时候才会进行连接操作,但是EAGER就是只要使用到当前表,就会加上复杂的表连接语句
所以fetchType(取类型)一般设置为LAZY
Spring Data MongoDB
除了关系型数据库之外,还有非关系型数据库占了很重要的一部分,MongoDB作为文档型数据库,具有高性能,易部署的特性;
在SpringBoot中,we使用Spring Data MongoDB集成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端口
引入stater依赖
想要使用Spring Data MongoDB,只需要引入相关的stater即可,其中就有相关的template对象
<!-- 使用MongoDB数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- 本来这样就可以了,但是其中的driver-async 4.6.1一直无法解析,所以只好排除之后降低版本 <!-- 问题解除了,这里解析不了就不断刷新,重启应用,在确保不是冲突的情况下,反正博主这里再次打开就好了 -->
<!-- 使用MongoDB数据库-->
<!-- 使用MongoDB数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>org.mongodb</groupId>-->
<!-- <artifactId>mongodb-driver-sync</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.mongodb</groupId>-->
<!-- <artifactId>mongodb-driver-sync</artifactId>-->
<!-- <version>4.4.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.mongodb</groupId>-->
<!-- <artifactId>mongodb-driver-reactivestreams</artifactId>-->
<!-- <version>4.6.1</version>-->
<!-- </dependency>-->
也就是data-mongoDB,之前的jdbc中就是jdbcTemplate,data-jpa就是Spring Data JPA
Spring Data MongoDB提供两种方式访问数据:
- 基于MongoTemplate : 遵循Spring Boot的标注模板形式,就像RedisTemplate和JdbcTemplate类似,都是在官方的客户端基础上封装的持久化引擎
- 基于MongoRepository: 按照Spring Data家族通用的设计模式设计的API
先把MongoDB数据库跑起来: C:\Users\xxx-PC>mongod -dbpath D:\MongoDB\Server\4.2\data\db
Spring Data MongoDB可以通过创建配置类集成AbstractMongoClientConfiguration进行配置,或者神功MongoClient或者MongoTemplate的JavaBean实现
继承AbstractMongoClientConfiguration配置
主要是配置数据库名,相关的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");
}
}
使用MongoTemplate访问MongoDB
Spring Data MongoDB的基本文档查询就是依赖的MongoTemplate常见的方法就是增删改查
insert,save,updateFirst,updateMulti,findAndModify,upsert,remove
创建MongoDB的实体类@Document
在类上面加上@Document表明由MongoDB维护,就类似与之前的Spring Data JPA中的实体类注解[email protected],还有@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不支持范型,在使用过程中需要注意不要定义范型
insert(T objectToSave, String collectionName) 用于在初始化时插入到数据库的集合中
这个方法用于初始化时将数据写入数据库,可以指定Collection,不指定就是默认类名
save(T objectToSave) 保存实体,插入或者更新
通过id判断实体是否存在与数据库中,存在就为更新操作,不存在就是插入操作
updateFirst(Query query,UpdateDefintion update,Class<?> EntityClass) 更新查询到的第一条记录,返回值为UpdateResult更新后的结果
这里的查询使用的是Query对象,new Query()之后,addCriteria查询条件,Criteria.where等着和之前的JpaSpecializationExecutor是类似的,就是组合谓词列表
而Update对象则用来进行更新,update的set就和之前的mysql类似,会将查询到的对象的某个属性重新set,
EntityClass就是识别MongoDB管理的entity类型
updateFirst方法只是更新查询结果的第一条记录,查询结果可能有很多
updateMulti((Query query,UpdateDefintion update,Class<?> EntityClass)批量更新
和上面的查询不同,会更新多条记录,所有都会更新,和Mysql类似
findAndModify((Query query,UpdateDefintion update,Class<?> EntityClass)更新第一条记录,返回值为更新前记录
和UpdateFirst只是返回值不同
upsert((Query query,UpdateDefintion update,Class<?> EntityClass) 类似与save,但是这是根据query的结果
查找更新创建实体,无则创建,有则更新,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);
//成功移除了对象,其他的方法都很简单,就不一一演示
}
可以看到执行结果符合预期

使用MongoRepository访问MongoDB【和Redis有所区别】
Redis是直接当作和Mysql等一样,直接继承JpaRepository即可,但是MongoDB有海量数据,所以使用更加独特的MongoRepository,MongoRepository和JPA中的Repository很相似,继承了PagingAndSortingRepository<T,ID>接口和QueryByExampleExecutor< T 》除了基础的增删改查之外,还有排序分页,以及Example匹配对象的方式
使用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 {
saveAll(Iterable s> entities) 保存集合中所有的对象到MongoDB中,默认是类名同名的Collection中
database就是之前config中设置的base,但是集合自己选定,saveAll方法就是mogorepository的,保存给出的所有的实体对象
exists(Example s> example) 判断是否存在与Example匹配的元素
这里的关键就是Example匹配对象,类似于Objects,使用方式: Example.of(构造的对象), 与构造的对象进行匹配,eg: Example.of(new Student().setName(“zs”))
findAll(Sort sort) 查询所有记录,按照规则排序,可以直接Sort.Direction.xx获取
查询所有的记录即可
findAll(Pageable pageable) 查询所有记录分页【海量】
这里的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中的结果,正常,【其他的几个测试结果放在注解程序内文档中】

Spring Data Redis
在NoSQL中,MongoDB是适合处理海量的易于扩展的数据,而Redis是更加注重性能,作为内存型键值数据库,大多数开发中,Redis当作缓存使用,用于缓存其他数据库中的热点数据,提高查询性能
SpringBoot中使用Redis是依靠的Spring Data Redis,和MongoDB类似,也提供了Template和repository两种访问的方式
引入依赖
还是引入相关的起步依赖就可以使用相关的template对象
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
连接Redis由两种客户端解决方案: Jedis和莴苣lettuce; Jedis和dos命令类似,Spring Data Redis默认集成Lettuce,如果要使用Jedis作为客户端,需要额外引入Jedis依赖
<!-- 使用jedis作为客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
Spring Data Redis提供了RedisTemplate和StringRedisTemplate;二者的区别就是
- StringRedisTemplate: 把k,v当作String处理,使用的是String的序列化,可读性好
- ReidsTemplate: 把k,v经过序列化存到Redis,可读性不好
默认使用的是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的客户端了
@EnableCaching 配置类上 启用缓存功能 @Cacheable: 赋予缓存功能,标记方法或者类上,代表该类中所有方法支持缓存
缓存之后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对象
配置使用Jedis为客户端【Pool】
//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结合的方式
使用RedisRepository访问Redis
redisRepoitory使用的是Spring Data中通用的Repository的风格
和前面的Jpa和MongoDB类似,Jedis风格的实体类采用@RedisHash,代表实体存储在RedisHash中,使用Repository访问,一定需要这个注解,timeToLive标注存活时间,单位为s,@Indexed和之前的一样代表添加索引
Redis Hash实体类
主要注解和之前的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对象了
使用RedisTepmlate访问Redis
相较于固定的使用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,再加入其他的配置就可
封装自定义redisTemplate 【配置Jedis见上】
实际使用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的内容,就不用其他的安全框架了
边栏推荐
- 小程序毕设作品之微信校园二手书交易小程序毕业设计成品(8)毕业设计论文模板
- js确定滚动元素和解决tab切换滚动位置独立
- Draw a wave ball with the curve of flutter and Bessel
- Digital collections start the 100 billion level market
- Chapter 2 how to use sourcetree to update code locally
- 船舶测试/ IMO A.799 (19)船用结构材料不燃性试验
- Go语言系列-协程-GMP简介-附字节跳动内推
- BGP笔记(二)
- 如何优雅的改变this指向
- Wechat campus second-hand book trading applet graduation design finished product (4) opening report
猜你喜欢

小程序毕设作品之微信酒店预订小程序毕业设计(7)中期检查报告

类和对象(1)

002_Kubernetes安装配置

Chapter 2 how to use sourcetree to update code locally

Trees and binary trees

Understanding service governance in distributed development

Go语言系列-协程-GMP简介-附字节跳动内推
![[technology popularization] alliance chain layer2- on a new possibility](/img/e1/be9779eee3d3d4dcf56e103ba1d3d6.jpg)
[technology popularization] alliance chain layer2- on a new possibility

The new idea 2022.2 was officially released, and the new features are really fragrant

ETL tool (data synchronization)
随机推荐
Mysql的索引为什么用B+树而不是跳表?
Daily question brushing record (XXXI)
Small program completion work wechat campus second-hand book trading small program graduation design finished product (2) small program function
2022年暑假ACM热身练习4(总结)
工作流引擎在vivo营销自动化中的应用实践
避错,常见Appium相关问题及解决方案
删除数组中的重复项(保留最后一次出现的重复元素并保证数组的原有顺序)
【无标题】
JS determines the scrolling element and solves the tab to switch the scrolling position independently
测试用例设计方法合集
NB-IOT的四大特性
如何优雅的改变this指向
Why does MySQL index use b+ tree instead of jump table?
VR panoramic zoo, a zoo business card with different achievements
Wechat hotel reservation applet graduation project (6) opening defense ppt
7、学习MySQL 选择数据库
局域网SDN硬核技术内幕 21 亢龙有悔——规格与限制(中)
Utools recommendation
网络安全之ARP欺骗防护
【09】程序装载:“640K内存”真的不够用么?