Spring Data JPA 入门
文章目录
- 前言、Spring Data JPA 是什么?
- 1、背景
- 2、优势
- 3、Spring Data JPA 和 MyBatis-Plus 对比
- 4、Spring Data JPA 与 JPA 的关系是什么?
- 一、准备
- 1、依赖引入
- Spring Boot 框架依赖引入:
- 非 Spring Boot 框架依赖引入:
- 2、定义实体
- 3、创建表结构和实体
- 表结构
- 学生(Student)
- 学生证(StudentCard)
- 班级(Classroom)
- 二、实体中使用到的注解介绍
- @Entity
- @Table
- @Id
- @IdClass:
- @EmbeddedId:
- @GeneratedValue
- @OneToOne、@OneToMany、@ManyToOne 和 @JoinColumn
- @Column
- 三、查询
- 1、JpaRepository 和 CrudRepository 介绍
- 2、方法名查询
- 3、@Query 注解查询
- 4、Specifications(动态查询)
- 5、分页和排序
- 方法名称 分页排序
- @Query 分页排序
- Specifications(动态查询) 分页排序
- 四、性能优化和常见问题
- 1、延迟加载 与 LazyInitializationException
- 什么是延迟加载(Lazy Loading)
- 什么是 LazyInitializationException
- 解决 LazyInitializationException 的常见策略
- 2、N+1 查询问题
- 问题
- 解决办法
- 3、批量操作
- 4、缓存机制
- **一级缓存(First-Level Cache)**
- 二级缓存(Second-Level Cache)
- 五、Github 项目地址
前言、Spring Data JPA 是什么?
1、背景
近年来国内大多数都是使用 MyBatis-Plus 框架进行查询数据库数据,MyBatis-Plus 简单易上手的操作让很多中小公司的开发人员爱不释手。再加上 MyBatis-Plus 官方人员也非常给力,陆续开发出很多好用的插件,尤其是代码生成器,让小型项目的搭建更加简单快速,直接生成 MVC 的基础代码和包结构,这简直是小型项目或者初期项目快速实现的大杀器。
而就是因为前期为了快速实现,后面业务慢慢的发展,那么问题就会逐一暴露出来。在一个项目的初期开发者完全可以通过 MyBatis-Plus 的组件快速生成一整个系统,然后简单修改后就可以直接上线。但是当业务慢慢越来越复杂,Java 代码中类与类的关系本身就是不明确的,如果再加上初期没有表结构设计和一些项目设计文档,那对于新加入的新人来说,面对超级多的 Entity 类、Mapper 接口和 XML 中那大量的 SQL 代码,那绝对是灾难级别的。
2、优势
针对上述问题,对于 Spring Data JPA 来说,当业务变得复杂时,Spring Data JPA 是能够通过面向对象的方式,更好地管理和表达实体之间的关系,减少出错的可能性。由于代码风格统一、结构清晰,新加入的成员能够更快地理解每个模块实体与实体之间的关系,并且能够快速上手项目代码,不会说还得面对几百上千行的 SQL 时需要一直去问各个表之间的关系,减少了沟通成本和学习曲线。
3、Spring Data JPA 和 MyBatis-Plus 对比
下面是 Spring Data JPA 和 Mybatis-Plus 两个框架之间的对比:
对比维度 | Spring Data JPA | MyBatis-Plus |
---|---|---|
编程范式 | ORM 框架,面向对象 | 半 ORM,SQL 为中心 |
开发效率 | 高,自动化 CRUD,方法名解析查询 | 高,通用 Mapper,代码生成器 |
灵活性 | 较高,受限于 JPA 规范 | 高,可直接编写 SQL,支持数据库特性 |
学习成本 | 需要理解 JPA 规范,可能较高 | 较低,熟悉 SQL 即可 |
性能 | 需注意延迟加载和批量操作,性能优化需深入理解 | 性能可控,可通过优化 SQL 提升性能 |
社区支持 | Spring 生态,社区活跃,文档丰富(中文文档比较少) | 社区活跃,文档和示例丰富 |
适用场景 | 面向对象程度高、实体关系复杂、追求代码规范的项目 | 复杂查询、多数据库特性、需要精细化 SQL 优化的项目 |
4、Spring Data JPA 与 JPA 的关系是什么?
简单点说,JPA 是 Java EE(Jakarta EE)标准规范,而 Spring Data JPA 则是更高级的抽象接口,它底层调用的还是 JPA 规范的接口,而真正实现 JPA 规范的框架是叫做 Hibernate。
本文章主要是聊 Spring Data JPA 提供的高级抽象接口的使用,可能会有读者知道 EntityManager 这个接口,这个接口其实是 JPA 规范中的接口。
一、准备
接下来,博主会使用一些场景,来进行演示 Spring Data JPA 的使用方式。在演示之前,我们在项目中需要引入 Spring Data JPA 的依赖,如果读者使用的是 Spring Boot 框架,那就太好了,因为你只需要引入依赖,不需要定义版本。而对于项目中使用非 Spring Boot 框架的读者来说,可能就需要好好注意版本的问题。
1、依赖引入
Spring Boot 框架依赖引入:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependencies>
非 Spring Boot 框架依赖引入:
<dependencies><!-- Spring 核心依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.29</version></dependency><!-- Spring Data JPA --><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-jpa</artifactId><version>2.7.12</version></dependency><!-- JPA 实现,如 Hibernate --><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-core</artifactId><version>5.6.15.Final</version></dependency><!-- 数据库驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version></dependency><!-- Spring ORM --><dependency><groupId>org.springframework</groupId><artifactId>spring-orm</artifactId><version>5.3.29</version></dependency>
</dependencies>
2、定义实体
在定义实体之前,先描述一个简单的业务场景,这样下面的使用时,会比较容易理解,而 JPA 中的一些注解,我会在使用到的地方进行介绍,最后会有一个总结,这样如果读者一点一点往下读下去,就不会一下子被很多的知识砸晕了。
拿一个简单的学生、学生证和班级业务举例,包括学生(Student)、学生证(StudentCard)和班级(Classroom)。一个学生只会拥有一个学生证,一个学生只会有一个所属班级,而一个班级下有 N 个学生,用这个关系去定义实体模型。
3、创建表结构和实体
表结构
-- 创建 Classroom 表
CREATE TABLE Classroom (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(255) NOT NULL,grade VARCHAR(50) NOT NULL
);-- 创建 StudentCard 表
CREATE TABLE StudentCard (id BIGINT PRIMARY KEY AUTO_INCREMENT,card_number VARCHAR(50) NOT NULL UNIQUE,issue_date DATE NOT NULL
);-- 创建 Student 表
CREATE TABLE Student (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(255) NOT NULL,email VARCHAR(255) UNIQUE,age INT,classroom_id BIGINT,student_card_id BIGINT UNIQUE
);
学生(Student)
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;import javax.persistence.*;
import java.io.Serializable;@Setter
@Getter
@ToString
@Entity
@Table(name = "student")
public class Student implements Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;// 姓名private String name;// 邮箱private String email;// 年龄private Integer age;// 所属班级@ManyToOne@JoinColumn(name = "classroom_id")private Classroom classroom;// 学生证@OneToOne(cascade = CascadeType.ALL)@JoinColumn(name = "student_card_id", referencedColumnName = "id")private StudentCard studentCard;}
学生证(StudentCard)
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;@Setter
@Getter
@ToString
@Entity
@Table(name = "student_card")
public class StudentCard implements Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;// 学生证编号@Column(name = "card_number")private String cardNumber;// 发放日期@Column(name = "issue_date")private LocalDateTime issueDate;@OneToOne(mappedBy = "studentCard")private Student student;}
班级(Classroom)
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;@Setter
@Getter
@ToString
@Entity
@Table(name = "classroom")
public class Classroom implements Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;// 班级名称private String name;// 年级private String grade;// 关联学生@OneToMany(mappedBy = "classroom", cascade = CascadeType.ALL)private List<Student> students = new ArrayList<>();}
二、实体中使用到的注解介绍
@Entity
@Eentity 注解放在类上,它表示该类代表一个实体。
@Table
@Table 注解用于指定实体类映射到数据库的哪一个表,以及表的相关属性。如果没有使用该注解,JPA 将默认使用实体类的类名作为表名。一般情况下会自己定义实体对应数据库中的表名。
@Table(name = "student"
)
public class Student implements Serializable {}
@Id
@Id 注解表示该属性是一个主键。如果说我的表是一个符合主键该怎么办呢?不用担心,JPA 中提供了两种定义复合主键方式,分别是 @IdClass 和 @EmbeddedId。一般使用 @IdClass 的方式比较多。
@IdClass:
需要注意的是,在 StudentId 实体中,一定要重写 equals 和 hashCode ,并且要实现 Serializable 接口。我这里是使用了 lombok 的 @EqualsAndHashCode 注解。
@Setter
@Getter
@ToString
@Entity
@IdClass(StudentId.class)
public class Student implements Serializable {@Idprivate Long studentAId;@Idprivate Long studentBId;}@Setter
@Getter
@ToString
@EqualsAndHashCode
public class StudentId implements Serializable {private Long studentAId;private Long studentBId;
}
@EmbeddedId:
需要注意的是,在 StudentId 实体中,一定要重写 equals 和 hashCode ,并且要实现 Serializable 接口。我这里是使用了 lombok 的 @EqualsAndHashCode 注解。
@Setter
@Getter
@ToString
@Entity
@IdClass(StudentId.class)
public class Student implements Serializable {@EmbeddedIdprivate StudentId id;}@Setter
@Getter
@ToString
@EqualsAndHashCode
@Embeddable
public class StudentId implements Serializable {private Long studentAId;private Long studentBId;
}
@GeneratedValue
@GeneratedValue 注解用于定义 主键字段 的生成策略。当我们希望数据库自动为实体生成唯一标识符(主键)时,可以在主键字段上使用该注解。它通常与 @Id 注解一起使用,标识该字段为主键,并指定主键的生成策略。需要注意的是 @GeneratedValue 只适用于简单主键,不支持复合主键。
@GeneratedValue 提供了多种主键生成策略,通过 strategy 属性进行指定。主键生成策略由 GenerationType 枚举定义,包括以下几种:
-
AUTO:JPA 自动选择合适的主键生成策略,具体取决于底层数据库和 JPA 实现提供商。
-
IDENTITY(最常用):采用数据库的 自增字段 来生成主键值。每次插入新记录时,数据库自动生成一个唯一的主键值。
-
SEQUENCE:使用数据库的序列(Sequence)来生成主键值,需要指定序列生成器。
-
TABLE:通过使用一张专用的数据库表来生成主键值,需要指定表生成器。
在实际的项目中,我们的 ID 都不会使用这几种,而是自己系统内部定义一个 ID 生成器,比如雪花、UUID等方式,那么这种情况下,我们会在创建实体时手动设置该实体的 ID 值。
@OneToOne、@OneToMany、@ManyToOne 和 @JoinColumn
@OneToOne(一对一)、@OneToMany(一对多)、@ManyToOne(多对一)是实体关系之间映射的注解,以上述的业务例子来说,一个学生有一个学生证,那就是 OneToOne 的关系。
@OneToOne 注解包含以下常用属性:
-
targetEntity:指定关联的目标实体类。可以不写,则默认使用属性的类型。
-
cascade:设置当前实体发生操作时,有哪些操作会影响到关联实体。
-
fetch:指定对关联实体的加载策略,默认为 FetchType.EAGER(直接加载),这个属性值在查询时有很多要说的点,我会在查询那里着重讲述。
-
optional:是否允许关联为空,默认为 true。
-
mappedBy:在双向关联中,由被关联方使用,指定关系的维护端。
-
orphanRemoval:删除实体时,是否将关联实体一块删除,默认是 false。
public class Student implements Serializable {// 学生证@OneToOne(cascade = CascadeType.ALL)@JoinColumn(name = "student_card_id", referencedColumnName = "id")private StudentCard studentCard;}public class StudentCard implements Serializable {@OneToOne(mappedBy = "studentCard")private Student student;}
再次拿过来 Student 和 StudentCard 实体,其中 @OneToOne 就是关联的实体,但是在两个实体中所设置的参数却是不一样的。在 Student 中设置了 cascade = CascadeType.ALL **,这里叫做联级操作,是表示当前实体发生操作时,会有哪种相同的操作影响到关联的实体。比如说,当删除 Student 时,那么关联的 StudentCard 实体也要一块删除。
在 CascadeType 中定义了很多属性值:
- ALL:所有操作都会影响到关联实体。
- PERSIST:持久化(存储)时同时保存关联实体。
- MERGE:合并时同时合并关联实体。合并这里的意思其实就是将修改后的数据同步更新到数据库中。比如从数据库中查询了 Id 为 1 的学生,修改了姓名,这时你可以使用 merge 操作,将修改后的数据同步到数据库中。
- REMOVE:删除时同时删除关联实体。
- REFRESH:刷新时同时刷新关联实体。刷新就是从数据库中,重新读取数据。
- DETACH:分离时同时分离关联实体。分离通常用于清理或优化内存。
注意:在实际业务中要尽量避免使用联级操作,因为会涉及到性能的问题。
在 StudentCard 中,设置了 @OneToOne(mappedBy = “studentCard”),表示 StudentCard 实体是被关联的实体。需要注意的是,不能 Student 和 StudentCard 都设置为相同的(比如都设置成被关联实体),必须要区分主动关联和被动关联。
接下来再看 Student 中的 @JoinColumn(name = “student_card_id”, referencedColumnName = “id”),@JoinColumn 注解用于指定 关联实体之间使用的外键列。当在实体类中定义关联关系时,可以使用 @JoinColumn 来精确控制外键列的名称、引用的列,以及其他属性。
@JoinColumn 中主要的属性是 name 和 referencedColumnName 这两个属性了,name 属性表示当前实体中的外键是哪一个字段,referencedColumnName 则是表示关联的表中哪一个字段跟当前实体中的外键进行关联的。就如案例中 Student 实体中的 student_card_id 字段对应 StudentCard 实体中的 id 字段关系一样。而 @JoinColumn 其他的属性就不太常用了,就不在这里一一介绍了。
在说到这,其实你就已经会了 @OneToMany 和 @ManyToOne 注解了。这两个跟 @OneToOne 是一样的用法,只是表达的关联关系不一样罢了,而还有一些案例中没有提到的关联关系注解,其实意思都是这样。
@Column
@Column 注解是用于 映射实体类的属性或字段到数据库表中的列,并可以通过其属性来定义列的详细信息,如列名、类型、长度、是否可为空等。一般很少会用到这个注解,很很多情况下就是不设置直接走默认。
在这里需要补充一点,就是实体类中是小驼峰的写法,而数据库中是各个单词之间下划线表示,可以通过配置去解决:
jpa:hibernate:naming:physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
三、查询
在 Spring Data JPA 中,可以使用很多方式进行查询数据,比如方法名查询、@Query 注解方式、Specifications 动态复杂查询、命名查询(NamedQuery)、Example(示例匹配查询)和 Query by Example(QBE)。本篇文章不会全讲述完,只会讲几个常用且实用的查询方式,保证让你在真实项目实战中能够应付,并且还绰绰有余。
在开始之前,我会先介绍 JpaRepositorye 和 CrudRepository 查询接口,这两个查询接口是 Spring Data JPA 官方给出的接口,我们只要实现这个接口,就可以进行一些很常用的查询。包括我下面要讲到的查询方式,都是基于这两个接口去扩展。
1、JpaRepository 和 CrudRepository 介绍
CrudRepository 和 JpaRepository 的关系是 JpaRepository 继承了 CrudRepository 接口。CrudRepository 是只提供了一些最基础的 CRUD 操作,而 JpaRepository 则是在 CrudRepository 的基础上,增加了对 JPA 规范的支持,提供了更多的操作方法和功能。像分页查询、刷新持久化上下文、批量操作、等操作都是在 JpaRepository 中进行扩展的,所以在实际的开发过程中,都是会直接基础 JpaRepository 进行操作。
在这里不会对这两个接口中的方法做过多的介绍,在文章的最后,我会把这两个接口中的方法做一个列举,下面的案例虽然基于这两个接口,但是并不妨碍理解。
2、方法名查询
方法名查询,顾名思义就是根据方法的名称进行查询。JPA 中有一套命名规则,基于方法名称的前缀去做不同的处理,比如查询单个的方法名称前缀规则就是 find,查询多个是 findAll,删除就是 remove 等等,这里我不会讲太多的规则,我会在文章最后做一个非常详细的表格,这里只讲如何使用。
那么我们就先写几个业务案例,这样你就会明白的。
import org.springframework.data.jpa.repository.JpaRepository;import java.util.Collection;
import java.util.List;public interface MethodNameQueryStudentRepository extends JpaRepository<Student, Long> {// 根据学生邮箱查询学生信息Student findByEmail(String email);// 根据学生姓名模糊查询学生信息List<Student> findAllByNameLike(String name);// 根据学生年龄大于某个值查询学生信息List<Student> findAllByAgeGreaterThan(Integer ageIsGreaterThan);// 根据学生年龄大于某个值且姓名模糊查询学生信息List<Student> findAllByAgeGreaterThanAndNameLike(Integer ageIsGreaterThan, String name);// 根据Id批量删除多个学生void removeByIdIsIn(Collection<Long> ids);}
在上述的案例中,我使用到了查询精准查询、模糊查询、大于某个值、大于某个值并且模糊查询,以及删除多个Id的方法。Spring Data JPA 通过多个关键词在前缀规则后进行追加的方式进行构建操作方法,只可惜的是只能适用于比较简单的查询,对于业务中一些复杂的操作查询,就不太够用了。下面列举几个常用的关键字符:
- 逻辑运算符:And、Or
- 比较运算符:Is、Equals、Between、LessThan、GreaterThan、Like、In、NotIn 等
- 特殊条件:IsNull、IsNotNull、IsEmpty、IsNotEmpty 等
在一般情况下,使用方法名查询能够做到大多数的业务都能够覆盖得到,会有一少部分覆盖不到,而至于分页和排序查询,我会在下面继续讲述,而连表条件查询,使用单纯的方法名查询方式是行不通的,需要配合其他注解,下面的 @Query 注解会有连表的条件查询。
3、@Query 注解查询
在讲述完基本的方法名查询后,就来介绍一个 @Query 注解查询方式。@Query 注解是其中一种强大且灵活的查询方式。它允许在接口方法上直接编写 JPQL(Java Persistence Query Language)或原生 SQL 查询,适用于单表和多表查询。
下面还是用几个案例,来介绍 @Query 注解查询的使用方法:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;import java.util.Collection;
import java.util.List;public interface QueryAnnotationQueryStudentRepository extends JpaRepository<Student, Long> {// 根据学生邮箱查询学生信息@Query("SELECT stu FROM Student stu WHERE stu.email = ?1")Student findByEmail(String email);// 根据班级名称查询班级下的所有学生@Query("SELECT stu FROM Student stu JOIN stu.classroom classroom WHERE classroom.name = :classroomName")List<Student> findUsersByClassroomName(@Param("classroomName") String classroomName);// 删除多个学生@Modifying@Query("DELETE FROM Student stu WHERE stu.id in (?1)")int removeAllByIdIn(Collection<Long> ids);}
可以看到上面的例子,能够使用原生的 SQL 进行单表查询和连表查询,而其中的 ?1 和 :classroomName 则是位置参数和命名参数,这两种做法主要是防止 SQL 注入,并且可以通过 JPQL 的方式去进行连表查询。而针对修改和删除时,则需要在方法上面加上 @Modifying 注解,这是必须的,不能不加,因为框架内部需要知道该方法是修改或删除的操作,不加会抛出 InvalidDataAccessApiUsageException 异常。
4、Specifications(动态查询)
Specifications 是 Spring Data JPA 提供的一种动态查询机制,基于 JPA 的 Criteria API。通过 Specifications,可以在代码中以 类型安全 且 面向对象 的方式构建查询条件。相比于固定的 JPQL 或 SQL 查询,Specifications 更适合在条件多变或需要根据用户输入临时拼装查询条件的场景。
Specifications 这种方式对比前两种的写法更加的复杂,但优点就是扩展性特别强!!!我有一篇很详细的文章专门去讲述通过 Specifications 这种方式查询,链接,在这里我就简单举个例子:
import com.demo.entity.Student; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List;public interface SpecificationsQueryStudentRepository extends JpaRepository<Student, Long> {// 定义查询接口List<Student> findAll(Specification<Student> specification);}
import com.demo.repository.SpecificationsQueryStudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;// 这只是 Demo 的启动类,不用管
@SpringBootTest(classes = DemoApplication.class)
public class SpecificationsQueryStudentRepositoryTest {@Autowiredprivate SpecificationsQueryStudentRepository repository;@Testpublic void testSaveAndFindStudent() {Specification<Student> specification = this.buildConditions();List<Student> list = repository.findAll(specification);}private Specification<Student> buildConditions() {Specification<Student> specifications = new Specification<Student>() {// 通过 CriteriaBuilder 来构建 Predicate 对象表示查询条件@Overridepublic Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {// 查询条件集合List<Predicate> predicates = new ArrayList<>();// 查询所有姓名等于张三的通宵predicates.add(criteriaBuilder.equal(root.get("name"), "张三"));// 连表查询,班级左关联学生,查询班级名称包含“二”【注意:classroom 是与 Student 实体中的属性名称一致】Join<Object, Object> join = root.join("classroom", JoinType.LEFT);predicates.add(criteriaBuilder.like(join.get("name"), "%二%"));// return query.where(criteriaBuilder.and(predicates.toArray(new Predicate[0]))).getGroupRestriction();}};return specifications;}}
还有另外一种封装的比较好的写法,这种封装的更清晰一些:
public void testSaveAndFindStudent() {Specification<Student> spec = Specification.where(hasName("张三")).and(hasClassroomName("二"));List<Student> all = repository.findAll(spec);
}public static Specification<Student> hasName(String name) {return (Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {if (name == null || name.trim().isEmpty()) {// 没有条件return cb.conjunction();}return cb.equal(root.get("name"), name);};
}public static Specification<Student> hasClassroomName(String classroomName) {return (Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {if (classroomName == null || classroomName.trim().isEmpty()) {return cb.conjunction();}Join<Object, Object> join = root.join("classroom", JoinType.LEFT);return cb.like(join.get("name"), "%" + classroomName + "%");};
}
上述的示例中,都是通过 CriteriaBuilder 去构建 Predicate 对象表示查询条件,将多个 Specification 条件组合(and、or)实现复杂查询。在实战的开发中,两种写法都是可以的,只不过是封装的颗粒的大小的问题。相对方法名查询和 @Query 注解的方式查询,这种写的扩展性更强,并且对于像常用的分页列表多个条件查询,用户没有输入则不查询的这种场景下,非常适合,所以,一般在实战项目的开发中,这几种都是混着使用的,根据不同的业务需求和场景,用不同的方式。
简单、固定的查询:用方法名查询最为快速。
中度复杂查询或需要多表关联:使用 @Query 注解,手写 JPQL 或原生 SQL,更直观。
条件动态变化、复杂灵活的查询:使用 Specifications,动态组合条件。
特性 | 方法名称查询 | @Query 查询 | Specifications (动态查询) |
---|---|---|---|
学习成本 | 低 | 中 | 高 |
实现复杂查询 | 较弱(简单查询为主) | 强(可编写复杂查询) | 最强(动态组合条件) |
可维护性 | 中,方法名易过长 | 中,查询语句需手写 | 中等偏高,需要编写 Predicate |
类型安全 | 是(方法名) | 不严格(需检查 JPQL/SQL) | 是(使用 Criteria API) |
动态性 | 较弱(方法名固定) | 较弱(查询固定) | 强(可根据条件组合Specification) |
适用场景 | 简单固定查询 | 中复杂度查询 | 条件多变、复杂动态查询 |
5、分页和排序
在上面常用的几种方式查询中,都没有去讲分页和排序,因为对于分页和排序来说,各自查询方式的用法更加重要,分页和排序则只是加一个参数罢了,下面就开始吧。
方法名称 分页排序
import com.demo.entity.Student;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;public interface MethodNameQueryStudentRepository extends JpaRepository<Student, Long> {// 分页排序查询:根据学生年龄大于某个值且姓名模糊查询学生信息Page<Student> findAllByAgeGreaterThanAndNameLike(Integer ageIsGreaterThan, String name, Pageable pageable);// 排序查询:根据学生年龄大于某个值且姓名模糊查询学生信息List<Student> findAllByAgeGreaterThanAndNameLike(Integer ageIsGreaterThan, String name, Sort sort);}import com.demo.DemoApplication;
import com.demo.entity.Student;
import com.demo.repository.MethodNameQueryStudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;import java.util.List;@SpringBootTest(classes = DemoApplication.class)
public class MethodNameQueryStudentRepositoryTest {@Autowiredprivate MethodNameQueryStudentRepository methodNameQueryStudentRepository;@Testpublic void testPageStudent() {// 第 1 页(索引从 0 开始),每页 10 条数据,按学生姓名升序排序,年龄降序排序PageRequest pageable = PageRequest.of(0, 10, Sort.by("name").ascending().and(Sort.by("age").descending()));// PageRequest pageable = PageRequest.of(0, 10, Sort.by("name").ascending().and(Sort.by("age").descending()));// 年龄大于 18,姓名包含 "张"Page<Student> page = methodNameQueryStudentRepository.findAllByAgeGreaterThanAndNameLike(18, "张", pageable);// 总页数int totalPages = page.getTotalPages();// 本页多少条long totalElements = page.getTotalElements();// 本页数据List<Student> content = page.getContent();}}
@Query 分页排序
@Query 这种方式,与方法名称的写法一样,就是在方法上加参数即可,下面举两个例子,不多探讨了:
public interface QueryAnnotationQueryStudentRepository extends JpaRepository<Student, Long> {// 分页排序查询:根据班级名称查询班级下的所有学生@Query("SELECT stu FROM Student stu JOIN stu.classroom classroom WHERE classroom.name = :classroomName")Page<Student> findUsersByClassroomName(@Param("classroomName") String classroomName, Pageable pageable);// 排序查询:根据班级名称查询班级下的所有学生List<Student> findUsersByClassroomName(@Param("classroomName") String classroomName, Sort sort);}
Specifications(动态查询) 分页排序
Specifications 与上述两种也都一样,加参数即可:
public interface SpecificationsQueryStudentRepository extends JpaRepository<Student, Long> {Page<Student> findAll(Specification<Student> specification, Pageable pageable);}
public class SpecificationsQueryStudentRepositoryTest {public void testFindAllPage() {// 分页和排序PageRequest pageRequest = PageRequest.of(0, 10, Sort.by("name").ascending());// 查询条件Specification<Student> specification = this.buildConditions();// 查询Page<Student> page = repository.findAll(specification, pageRequest);}}
四、性能优化和常见问题
1、延迟加载 与 LazyInitializationException
延迟加载 和 LazyInitializationException 异常,是两个常常伴随出现的概念与问题,出现一方,就会牵扯出另一方。在聊之前,要先一个一个知道是什么,再说解决办法。
什么是延迟加载(Lazy Loading)
延迟加载(Lazy Loading)是为了提高性能和减少不必要的数据加载而引入的一种策略。当使用 JPA 对象关系映射框架时,实体之间经常存在关联关系,通过设置 @OneToMany、@ManyToOne、@OneToOne、@ManyToMany ,来达到实体与实体之间的关系,而在这些注解中,会有一个 fetch 属性方法,用来设置加载方式,默认的情况是直接加载,也就是查询这一个实体,会把实体中所关联的实体数据都会查询出来。但是在实际查询中,我们并不总是需要立即加载关联的所有数据,所以就需要去设置延迟加载。延迟加载策略允许我们在初次查询实体时仅加载该实体的基本字,而将关联实体的数据加载推迟到真正访问该关联属性时再进行。
什么是 LazyInitializationException
LazyInitializationException 是当我们在 session 已关闭(或超出实体管理上下文范围)后尝试访问尚未初始化的延迟加载数据时抛出的异常。简单来说,就是当我们通过延迟加载这种方式去查询后,因为关联实体的关系初始化时未查询,但是在使用时需要使用到这个关联实体的数据,如果当前的 session 已经关闭了,那么就会出现这个问题。可能这种讲述还是有些抽象,我举个实际中的例子吧:
public void queryAll() {Optional<Student> studentOptional = repository.findById(1L);Student student = studentOptional.get();Long id = student.getId();String name = student.getName();Classroom classroom = student.getClassroom();
}@Transactional
public void queryByTransaction() {Optional<Student> studentOptional = repository.findById(1L);Student student = studentOptional.get();Long id = student.getId();String name = student.getName();Classroom classroom = student.getClassroom();
}
有没有发现这两段代码有什么不同?
没错,下面的方法上面有一个 @Transactional 注解!
@Transactional 注解并不单单只是用于常用的异常回滚操作,还能让查询数据库时连接数据库的 session 不关闭!所以,有了这个不关闭 session 的操作,就可以使用延迟加载了。
解决 LazyInitializationException 的常见策略
到这里就可以引入解决的策略了,上面已经给出了一种解决办法,这里会系统的给出解决办法。
解决方法主要分成两种,第一种就是直接配置不懒加载了,设置成直接加载,那么问题就直接解决!但是缺点是一旦面临关联实体中数据量庞大的情况,就会出现性能的问题。
第二种方式就是在查询的方法上增加 @Transactional 注解,这里需要注意一下,如果该方法只是查询,没有其他操作,可以直接写成 @Transactional(readOnly = true) ,这样也会提升性能,但是我并没有测试过会提升多少。
在上述查询方式中,主要有三种查询的方式:方法名称、@Query 和 Specifications。这三种查询方式,方法名称和 Specifications 都是可以直接在方法上增加 @Transactional 注解,而 @Query 注解查询的方式,除了在方法上增加 @Transactional 注解,还可以通过原生 SQL 的方式进行设置直接加载,通过在 JOIN 后面增加 FETCH 进行直接全部加载:
@Query("SELECT stu FROM Student stu JOIN FETCH stu.classroom classroom WHERE classroom.name = :classroomName")
List<Student> findUsersByClassroomName(@Param("classroomName") String classroomName);
除了讲述到的这两种解决办法,还有一些其他的操作,比如做一个延迟加载过滤器,让 session 重新连接,这种方式我就强烈不太推荐了,因为这种会破坏分层架构的能力定义,并且增加后面维护的很多问题。
还需要注意的是,一定要合理的配置加载策略(EAGER or LAZY),也可以在业务初期全部都设置直接加载的方式,避免出现 LazyInitializationException 异常。但是随着业务数据的增长,查询效率下降时,就得考虑 LazyInitializationException 的问题了。总之,有利有弊!
2、N+1 查询问题
问题
N+1 查询问题是使用 ORM(对象关系映射)框架时常见的性能隐患之一。当你在查询数据库中的实体及其关联关系时,如果处理方式不当,程序可能会在不经意间发送过多的 SQL 查询,导致性能明显降低。
比如实体 Classroom,它与另一个实体 Student 存在一对多的关联关系。例如,你需要查询所有的 Classroom,然后对每个 Classroom 实例访问其关联的 Student 集合。理想情况下,你希望能够通过少量的 SQL(甚至一条查询)就拿到所有所需数据。但在使用延迟加载(Lazy Loading)或映射策略不当的情况下,ORM 框架可能会产生以下查询模式:
- 首先执行 1 条查询 来获取 Classroom 实体的列表,假设返回 N 条记录。
- 当你在代码中遍历这 N 条 Classroom 实体时,每次访问 Classroom 的关联属性(比如 classroom.getStudent())时,都会单独为当前 Classroom 实例发送 1 条查询 到数据库,以获取其关联的 Student 列表。
此时你所面临的情况是:
- 第一次获取所有 Classroom 实体的查询为 1 条。
- 然后每个 Classroom 的关联 Student 数据又各产生 N 条查询。
总共 = 1 + N = N+1 条查询。
当 N 很大时,这将导致大量 SQL 查询的执行,显著增加数据库负载和应用程序的响应时间。下面是一段错误的代码示例(查询方式是延迟加载):
public void queryClassroom() {// 延迟加载查询List<Classroom> all = classroomRepository.findAll();for (Classroom classroom : all) {// 一个 Classroom 对应多个 StudentList<Student> students = classroom.getStudents();}
}
解决办法
解决办法的核心就一个,就是让查询的数据一次性全部查询出来,不要一个一个再去查询了,
1、@Query 注解中的原生 SQL 使用 JOIN FETCH;
2、实体中关系注解(@OneToMany)配置直接加载(默认就是直接全部加载);
3、批量操作
在 Spring Data JPA 执行批量操作(主要指批量插入)中,虽然也是一个数据库连接,但是 JPA 中会一个一个的 SQL 往数据库发送。比如插入 1000 条数据,那么就会生成 1000 条 INSERT 语句,就算是一个连接,消耗的时间也是非常大的。
批量插入的解决办法有这么三个吧。
第一种是直接用 Mybatis-Plus 框架进行专门处理批量插入的问题;
第二种是通过设置配置(JDBC配置)来解决批量插入的条数:
properties 文件格式:
spring.jpa.properties.hibernate.jdbc.batch_size=1000
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
yml 文件格式:
spring:jpa:properties:hibernate:jdbc:batch_size: 1000order_inserts: trueorder_updates: true
配置说明:
-
hibernate.jdbc.batch_size:设置批处理的大小(如每 1000 条 SQL 语句为一个批次)。
-
hibernate.order_inserts 和 hibernate.order_updates:优化批处理,通过将相同类型的 SQL 语句集中在一起,提高批处理效率。
第三种是通过 JPA 中的 EntityManager 进行操作,下面我给一个示例:
@SpringBootTest(classes = DemoApplication.class)
public class MethodNameQueryStudentRepositoryTest {@Autowiredprivate EntityManager entityManager;@Test@Transactionalpublic void testSaveAndFindStudent() {// 多个学生List<Student> all = ....;int batchSize = 50;for (int i = 0; i < all.size(); i++) {// 插入新学生entityManager.persist(all.get(i));if (i > 0 && i % batchSize == 0) {// 将持久化上下文中的更改同步到数据库entityManager.flush();// 清理持久化上下文,释放内存entityManager.clear();}}// 将持久化上下文中的更改同步到数据库entityManager.flush();// 清理持久化上下文,释放内存entityManager.clear();}
}
4、缓存机制
缓存机制在现在的应用程序中扮演着至关重要的角色,尤其是在数据密集型应用中。通过有效利用缓存,可以显著提升应用的性能、降低数据库负载,并改善用户体验。Spring Data JPA 提供了多层次的缓存机制,主要包括一级缓存(First-Level Cache)、二级缓存(Second-Level Cache)以及查询缓存(Query Cache)。
在 Spring Data JPA 中,缓存机制主要分为以下几类:
-
一级缓存(First-Level Cache) :与 EntityManager 或 Session 关联的缓存。
-
二级缓存(Second-Level Cache) :跨 EntityManager 或 Session 的共享缓存。
-
查询缓存(Query Cache) :缓存查询结果,提高查询性能。
一级缓存(First-Level Cache)
一级缓存是 JPA 中的一个核心概念,它与每个 EntityManager(在 Spring 中通常由 @Transactional 管理的事务)或 Hibernate 的 Session 实例相关联。一级缓存的主要作用是存储在当前持久化上下文中管理的实体实例。
下面给一个示例:
@SpringBootTest(classes = DemoApplication.class)
public class CacheStudentRepositoryTest {@Autowiredprivate MethodNameQueryStudentRepository repository;@Test@Transactional(readOnly = true)public void queryAll() {Optional<Student> studentOptional1 = repository.findById(1L);Student student1 = studentOptional1.get();System.out.println("查询第一次...");Optional<Student> studentOptional2 = repository.findById(1L);Student student2 = studentOptional2.get();System.out.println("查询第二次...");}
}
打印输出:
Hibernate: select student0_.id as id1_1_0_, student0_.age as age2_1_0_, student0_.classroom_id as classroo5_1_0_, student0_.email as email3_1_0_, student0_.name as name4_1_0_, student0_.student_card_id as student_6_1_0_, classroom1_.id as id1_0_1_, classroom1_.grade as grade2_0_1_, classroom1_.name as name3_0_1_, studentcar2_.id as id1_2_2_, studentcar2_.card_number as card_num2_2_2_, studentcar2_.issue_date as issue_da3_2_2_ from student student0_ left outer join classroom classroom1_ on student0_.classroom_id=classroom1_.id left outer join student_card studentcar2_ on student0_.student_card_id=studentcar2_.id where student0_.id=?
查询第一次...
查询第二次...
能够看到第一次的时候先查询数据库,而在第二次查询的时候,却没有查询数据库,这就是一级缓存,只要是在当前这个事务中,它就是生效的。一级缓存不需要我们做任何事情,它本身默认就是开启的。
二级缓存(Second-Level Cache)
二级缓存是一个跨 EntityManager 或 Session 的共享缓存,旨在缓存常用实体或查询结果,以减少对数据库的访问次数。与一级缓存不同,二级缓存是 全局的,可以被多个持久化上下文共享。
启用二级缓存我们需要一些外部的框架,比如我们常说的 Redis。当然,对于很多小型项目来说,引入 Redis 会让项目的技术栈变得复杂,这样的话也可以使用一些本地缓存框架,比如 Ehcache、Caffeine、JCache 等。据目前各个公司内部,用的最多的还是 Ehcache,这里说的是小项目,中大型项目还是 Redis 最多。
在这里我就以 Ehcache 举例,如果你的项目中不想使用这个缓存框架,想要换掉,可以直接把 Ehcache 框架的依赖和陪着去掉,换成其他的缓存框架配置和依赖。
这里使用 Spring 规定的 Cache 规范,后期换框架的话方便一些,Spring 提供的缓存抽象,通过注解如 @
Cacheable、@CachePut 和 @CacheEvict 管理方法级别的缓存,下面就一步一步的进行配置:
依赖:
<!-- SpringBoot JPA -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency><!-- SpringBoot Cache -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><!-- Ehcache -->
<dependency><groupId>org.ehcache</groupId><artifactId>ehcache</artifactId><version>3.10.6</version>
</dependency><!-- Hibernate Ehcache -->
<dependency><groupId>org.hibernate</groupId><artifactId>hibernate-ehcache</artifactId><version>5.6.15.Final</version>
</dependency>
在 src/main/resources
目录下创建 ehcache.xml,我这里用的是 Ehcache 2 的配置写法:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache><!-- 默认缓存 --><defaultCachemaxElementsInMemory="10000"eternal="false"timeToIdleSeconds="120"timeToLiveSeconds="120"overflowToDisk="false"maxElementsOnDisk="10000000"diskPersistent="false"diskExpiryThreadIntervalSeconds="120"memoryStoreEvictionPolicy="LRU"/><!-- Student 缓存 --><cache name="studentCache"maxElementsInMemory="2000"eternal="false"timeToIdleSeconds="4"timeToLiveSeconds="4"overflowToDisk="false"/></ehcache>
application.yml 中配置:
spring:jpa:# 打印 SQLshow-sql: trueproperties:hibernate:cache:# 启用二级缓存
use_second_level_cache: trueuse_query_cache: true# 配置缓存区域工厂
region:factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory# Spring 指定 Ehcache 配置文件
cache:ehcache:config: classpath:ehcache.xmltype: ehcache
启动类配置,增加 @EnableCaching 注解,用来启用 Spring 的缓存支持:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;@EnableCaching
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
Repository 查询接口配置,在 Repository 方法上使用 @Cacheable 注解,实现方法级别的缓存。这里的缓存区域为 queryCache,对应 ehcache.xml 中的查询缓存配置:
import com.demo.entity.Student;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;/**
* 缓存
*/
public interface CacheStudentRepository extends JpaRepository<Student, Long> {@Query("SELECT u FROM Student u WHERE u.name = :name")@Cacheable(cacheNames = "studentCache", key = "'findByName:' + #name")Student findByName(@Param("name") String name);@CacheEvict(cacheNames = "studentCache", allEntries = true)void removeById(Long id);@CacheEvict(cacheNames = "studentCache", allEntries = true)@CachePut(cacheNames = "studentCache", key = "'allUsers'")Student save(Student student);}
使用缓存测试一下,我上面配置的缓存时间是 4 秒,这里先查询然后再暂停 3 秒试试:
import com.demo.DemoApplication;
import com.demo.entity.Student;
import com.demo.repository.CacheStudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.PostConstruct;@SpringBootTest(classes = DemoApplication.class)
public class CacheStudentRepositoryTest {@Autowiredprivate CacheStudentRepository repository;@PostConstructpublic void init() {Student student = new Student();student.setId(1L);student.setName("张三");student.setEmail("zhangsan@xxx.com");student.setAge(18);repository.save(student);}@Testpublic void test1() throws InterruptedException {Student student = repository.findByName("张三");System.out.println("111 " + student);Thread.sleep(3000L);Student student1 = repository.findByName("张三");System.out.println("222 " + student1);}
}
打印结果:
Hibernate: select student0_.id as id1_1_, student0_.age as age2_1_, student0_.classroom_id as classroo5_1_, student0_.email as email3_1_, student0_.name as name4_1_, student0_.student_card_id as student_6_1_ from student student0_ where student0_.name=?
111 Student(id=1, name=张三, email=zhangsan@xxx.com, age=18, classroom=null, studentCard=null)
222 Student(id=1, name=张三, email=zhangsan@xxx.com, age=18, classroom=null, studentCard=null)
超过缓存时间试试:
import com.demo.DemoApplication;
import com.demo.entity.Student;
import com.demo.repository.CacheStudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.PostConstruct;@SpringBootTest(classes = DemoApplication.class)
public class CacheStudentRepositoryTest {@Autowiredprivate CacheStudentRepository repository;@PostConstructpublic void init() {Student student = new Student();student.setId(1L);student.setName("张三");student.setEmail("zhangsan@xxx.com");student.setAge(18);repository.save(student);}@Testpublic void test1() throws InterruptedException {Student student = repository.findByName("张三");System.out.println("111 " + student);Thread.sleep(5000L);Student student1 = repository.findByName("张三");System.out.println("222 " + student1);}
}
打印结果:
Hibernate: select student0_.id as id1_1_, student0_.age as age2_1_, student0_.classroom_id as classroo5_1_, student0_.email as email3_1_, student0_.name as name4_1_, student0_.student_card_id as student_6_1_ from student student0_ where student0_.name=?
111 Student(id=1, name=张三, email=zhangsan@xxx.com, age=18, classroom=null, studentCard=null)
Hibernate: select student0_.id as id1_1_, student0_.age as age2_1_, student0_.classroom_id as classroo5_1_, student0_.email as email3_1_, student0_.name as name4_1_, student0_.student_card_id as student_6_1_ from student student0_ where student0_.name=?
222 Student(id=1, name=张三, email=zhangsan@xxx.com, age=18, classroom=null, studentCard=null)
能够看到两次查询数据库,这样就成功了!
五、Github 项目地址
最后再附上 Spring Data JPA 入门的项目地址,项目内是已经配置好了配置,可以直接通过单元测试直接进行测试,无需数据库。
相关文章:
Spring Data JPA 入门
文章目录 前言、Spring Data JPA 是什么?1、背景2、优势3、Spring Data JPA 和 MyBatis-Plus 对比4、Spring Data JPA 与 JPA 的关系是什么? 一、准备1、依赖引入Spring Boot 框架依赖引入:非 Spring Boot 框架依赖引入: 2、定义实…...
操作系统用户界面
实验目的: LINUX操作系统提供了图形用户界面和命令行界面,本实验主要熟悉命令行界面,为后续的实验编程做准备。 二、实验内容: 写出每个命令的功能及格式 1、有关目录和文件操作的命令 1.1cat 功能: 1.一次显示整…...
区块链dapp 开发详解(VUE3.0)
1、安装metamask 插件。 2、使用封装的工具包: wagmi . 3、 wagmi 操作手册地址:connect | Wagmi 4、注意事项: 因为最初是react 版本,所以在VUE版的官方文档有很多地方在 import 用的是 wagmi,需要改为 wagmi/vue 。 连接成功后打印的内容如下&…...
BTC密码学原理
文章目录 比特币的密码学基础哈希函数(Hash Function)非对称加密(Asymmetric Cryptography)数字签名(Digital Signature) 工作量证明(Proof of Work)区块链技术的密码学保障区块链的…...
【NLP 9、实践 ① 五维随机向量交叉熵多分类】
目录 五维向量交叉熵多分类 规律: 实现: 1.设计模型 2.生成数据集 3.模型测试 4.模型训练 5.对训练的模型进行验证 调用模型 你的平静,是你最强的力量 —— 24.12.6 五维向量交叉熵多分类 规律: x是一个五维(索引)向量ÿ…...
Linux -文件系统的备份
本文为Ubuntu Linux操作系统- 第九弹~~ 今天接着上文的内容,讲Linux磁盘存储管理最后一部分内容~ 上期回顾:Linux 图形界面工具管理磁盘分区和文件系统 😎黑犀铠甲合体,流星枪之狂瀑扎帖,碎魔伏暴,灭于狂瀑…...
【Linux基础五】Linux开发工具—上(apt和vim)
【Linux基础五】Linux开发工具—上(apt和vim) 1.apt包管理工具2.Linux中的编辑器vim2.1命令模式2.2插入模式2.3底行模式2.4替换模式2.5视图模式2.6多线程操作2.7配置vim 1.apt包管理工具 在 Linux 系统中,软件包管理工具是用户安装、更新和管…...
力扣--LCR 134.Pow(x,n)
题目 实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。 示例 1: 输入:x 2.00000, n 10 输出:1024.00000 示例 2: 输入:x 2.10000, n 3 输出:9.2610…...
java抽奖系统(一)2.0
1. 项⽬介绍 1.1 背景 随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客⼾。抽奖活动作为⼀种有效的营 销⼿段,能够显著提升⽤⼾参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项⽬,通过这个项⽬提供⼀…...
读取电视剧MP4视频的每一帧,检测出现的每一个人脸并保存
检测效果还不错,就是追踪有点难做 import cv2 import mediapipe as mp import os from collections import defaultdict# pip install msvc-runtime# 初始化OpenCV的MultiTracker # multi_tracker = cv2.MultiTracker_create() # multi_tracker = cv2.legacy.MultiTracker_cre…...
【MySQL 进阶之路】事务并发情况分析
MySQL事务并发控制分析笔记 在数据库系统中,事务并发控制至关重要,能够确保多个事务并发执行时的数据一致性、隔离性和正确性。MySQL通过不同的锁机制控制并发操作,以确保事务的隔离性。以下是对事务A和事务B并发行为的详细分析,…...
基于SSM的线上考试系统的设计与实现(计算机毕业设计)+万字说明文档
系统合集跳转 源码获取链接 一、系统环境 运行环境: 最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。 IDE环境: Eclipse,Myeclipse,IDEA或者Spring Tool Suite都可以 tomcat环境: Tomcat 7.x,8.x,9.x版本均可 操作系统…...
redis击穿,穿透,雪崩以及解决方案
目录 击穿 解决方案一 解决方案二 穿透 解决方案 雪崩 解决方案 击穿 指的是单个key在缓存中查不到,去数据库查询,这样如果并发不大或者数据库数据量不大的话是没有什么问题的。 如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压…...
Flask返回中文Unicode编码(乱码)解决方案
大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…...
EasyExcel改名为FastExce做了那些改变呢
回到:github原作者地址:https://github.com/CodePhiliaX/fastexcel 中文 |English | 什么是 FastExcel FastExcel 是由原 EasyExcel 作者创建的新项目。2023 年我已从阿里离职,近期阿里宣布停止更新 EasyExcel,作者他本人决定继…...
数据结构之初始二叉树(1)
找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程(ಥ_ಥ)-CSDN博客 所属专栏:数据结构(Java版) 目录 树型结构 树的概念 与树的有关概念 树的表示形式 树的应用 二叉树 概念 两种特殊的…...
利用Python爬虫按图搜索淘宝商品(拍立淘)
在当今数字化时代,能够通过图片搜索商品的功能(如淘宝的“拍立淘”)为用户提供了极大的便利。本文将详细介绍如何利用Python爬虫技术实现按图搜索淘宝商品,并提供相应的代码示例。 1. 拍立淘功能简介 “拍立淘”是淘宝提供的一项…...
微信小程序中使用miniprogram-sm-crypto实现SM4加密攻略
在微信小程序开发过程中,数据安全至关重要。本文将为大家介绍如何在微信小程序中使用miniprogram-sm-crypto插件进行SM4加密,确保数据传输的安全性。 一、SM4加密简介 SM4是一种对称加密算法,由国家密码管理局发布,适用于商密领…...
基于注解的方式实现分布式锁的AOP功能
使用场景: 在分布式项目中,涉及到事务,且会操作多张表,那么涉及到并发场景,应当避免同一时间有多个用户并发操作同一份数据,例如:商品秒杀。 解决方案: 使用Around注解实现在方法…...
22. 五子棋小游戏
文章目录 概要整体架构流程技术名词解释技术细节小结 1. 概要 🔊 JackQiao 对 米粒 说:“今天咱们玩个五子棋小游戏,电脑与你轮流在一个 nn 的网格上放置棋子(X 或 O),网格由你输入的正整数n决定࿰…...
import是如何“占领满屏“
import是如何“占领满屏“的? 《拒绝使用模块重导(Re-export)》 模块重导是一种通用的技术。在腾讯、字节、阿里等各大厂的组件库中都有大量使用。 如:字节的arco-design组件库中的组件:github.com/arco-design… …...
传奇996_53——后端ui窗口局部刷新
描述:一个大窗口,点击某个键,弹出小窗口。 小窗口中将msg存进变量中 大窗口中判断一个参数是否为null,如果不为null,说明界面不是第一次打开,而是被刷新了。就加上小窗口的那个变量 有时小窗口中还有其他…...
云原生API网关:2024年的性能与智能化突破
API网关已经成为现代应用世界的流量控制器,并正在成为现代应用架构的重要组成部分。然而,自成立以来,他们的角色发生了巨大变化。它们正在减轻重量,变得更加可编程,并与云原生环境携手合作。 让我们来探索这一激动人心…...
专题二十五_动态规划_两个数组的 dp (含字符串数组)_算法专题详细总结
目录 动态规划_两个数组的 dp (含字符串数组) 1. 最⻓公共⼦序列(medium) 解析: 1. 状态表⽰: 2. 状态转移⽅程: 3. 初始化:编辑 4. 填表顺序:编辑 5. 返回值…...
day09 接口测试(5)——使用postman 完成项目实战
【没有所谓的运气🍬,只有绝对的努力✊】 目录 1、项目简介 2、技术栈 2.1 技术栈 2.2 技术架构图 3、初始化项目环境!!! 3.1 新建用例集 3.2 创建环境变量 3.3 登录模块 3.3.1 从测试用例文档提取数据 3.3.2…...
【Solidity】变量详解:类型、作用域与最佳实践
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 Solidity变量详解:类型、作用域与最佳实践引言1. 变量的类型与声明1…...
知识图谱9:知识图谱的展示
1、知识图谱的展示有很多工具 Neo4j Browser - - - - 浏览器版本 Neo4j Desktop - - - - 桌面版本 graphX - - - - 可以集成到Neo4j Desktop Neo4j 提供的 Neo4j Bloom 是用户友好的可视化工具,适合非技术用户直观地浏览图数据。Cypher 是其核心查询语言&#x…...
scss文件内引入其他scss文件报错
1、今天在编译一些老项目的时候,老是提示下面信息 2、而且有很多Sass import rules are deprecated and will be removed in Dart Sass 3.0.0.警告 3、用npm view sass versions看,其中sass的最新版本是1.82.0 4、经过测试"sass": "1.75…...
使用 ASP.NET Core HttpLoggingMiddleware 记录 http 请求/响应
我们发布了一个应用程序,该应用程序运行在一个相当隐蔽的 WAF 后面。他们向我们保证,他们的产品不会以任何方式干扰我们的应用程序。这是错误的。他们删除了我们几乎所有的“自定义”标头。为了“证明”这一点,我构建了一个中间件,…...
沃丰科技智能客服在跨境电商独立站中的核心角色
随着全球化进程的加速和互联网技术的不断发展,跨境电商行业蓬勃兴起,为消费者提供了更广阔、更便捷的购物选择。在这样一个竞争激烈的市场环境中,优质的客户服务成为了企业脱颖而出的关键。沃丰科技智能客服凭借其先进的技术和人性化的设计理…...
基于Springboot滑雪物品在线租赁网站设计与实现(作品+论文+开题报告)
博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育、辅导。 所有项目都配有从入门到精通的基础知识视频课程ÿ…...
android 富文本及展示更多组件
模拟微博 #热贴 和 用户 的这种 富文本形式组件,不说了, 直接上代码 package com.tongtong.feat_watch.viewimport android.content.Context import android.graphics.Color import android.util.AttributeSet import android.view.LayoutInflater impo…...
Java-JMX 组件架构即详解
JMX架构由三个主要组件构成: MBeans(Managed Beans):代表可管理的资源,是JMX的核心。MBean可以是Java类或接口,提供了管理操作的接口,如获取系统信息、设置参数等。MBeanServer&#x…...
开源轻量级文件分享服务Go File本地Docker部署与远程访问
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
Rust快速入门(五)
生命周期 生命周期的主要作用是避免悬垂引用。 这里我们详细说说借用检查: {let r; // ----------- a// |{ // |let x 5; // --- b |r &x; // | |} /…...
租赁系统|租赁小程序|租赁小程序成品
租赁系统是现代企业管理中不可缺少的数字化工具,它通过高效的信息整合与流程管理,为企业带来极大的便利和效益。一个完善的租赁系统开发应具备以下必备功能: 一、用户管理 用户管理模块负责系统的访问控制,包括用户注册、登录验证…...
后端-pageHelp分页查询
在pom.xml文件中先导入分页的坐标 PageResult里面有两个后端返回给前端的参数,我们最后把PageResult再封装到Result中, PageResult和Result都是工具类 EmployeeDTO中是前端页面中的模糊查询字段和分页的两个值 注意! 括号中的参数Employee…...
MySQL | 尚硅谷 | 第15章_存储过程与函数
MySQL笔记:第15章_存储过程与函数 文章目录 MySQL笔记:第15章_存储过程与函数第15章_存储过程与函数 1. 存储过程概述1.1 理解1.2 分类 2. 创建存储过程2.1 语法分析2.2 代码举例 3. 调用存储过程3.1 调用格式3.2 代码举例3.3 如何调试存储过程演示代码 …...
汽车零部件设计之——麦弗逊悬架KC特性分析仿真APP介绍
汽车零部件是汽车工业的基石,是构成车辆的基础元素。一辆汽车通常由上万件零部件组成,包括发动机系统、传动系统、制动系统、电子控制系统等,它们共同确保了汽车的安全、可靠性及高效运行。在汽车产业快速发展的今天,汽车零部件需…...
代码随想录-算法训练营day41(动态规划04:01背包,01背包滚动数组,分割等和子集)
第九章 动态规划part04● 01背包问题,你该了解这些! ● 01背包问题,你该了解这些! 滚动数组 ● 416. 分割等和子集 正式开始背包问题,背包问题还是挺难的,虽然大家可能看了很多背包问题模板代码…...
Vue Loader的作用
Vue Loader是一个专门用于处理Vue单文件组件(SFCs,即Single File Components)的webpack加载器(loader)。以下是Vue Loader的具体作用: 1、解析和转换Vue单文件组件 Vue Loader能够解析和转换.vue文件&…...
SRS 服务器入门:实时流媒体传输的理想选择
在当今视频流媒体需求爆炸式增长的时代,如何选择一款高效、稳定且功能强大的流媒体服务器成为了许多开发者和企业关注的焦点。而 SRS(Simple Realtime Server)作为一款开源的流媒体服务器,以其卓越的性能和灵活的功能,…...
4K高清壁纸网站推荐
1. Awesome Wallpapers 官网: https://4kwallpapers.com/ 主题: 创意、摄影、人物、动漫、绘画、视觉 分辨率: 4K Awesome Wallpapers 提供了丰富的高质量图片,分为通用、动漫、人物三大类,可以按屏幕比例和分辨率检索,满足你对壁纸的各种…...
如何保证数据库和缓存双写一致性?
数据库和缓存(redis)双写数据一致性问题再高并发的场景下,是一个很严重的问题,无论在工作中,还是面试,遇到的概率非常大,这里就聊一聊目前的常见解决方案以及最优方案。 常见方案 缓存的主要目…...
QT 多级嵌套结构体,遍历成员--半自动。<模板+宏定义>QTreeWidget树结构显示
Qt的QTreeWidget来显示嵌套结构体的成员,并以树形结构展示。 #include <QApplication> #include <QTreeWidget> #include <QTreeWidgetItem> #include <QString> #include <cstdint>// 假设这些是你的结构体定义 struct BaseMeterPa…...
《深入浅出HTTPS》读书笔记(17):公开密钥算法
公开密钥算法(Public Key Cryptography),也称为非对称加密算法(Asymmetrical Cryptography)。 公开密钥算法的功能比较多,可以进行加密解密、密钥协商、数字签名。 【密钥是一对】 公开密钥算法的密钥是一对…...
React 中为什么不直接使用 requestIdleCallback?
首先看下 requestIdleCallback是什么? 简介 requestIdleCallback 是一个在浏览器空闲时执行低优先级任务的 API。 定义与用途 requestIdleCallback 方法允许开发者在浏览器的空闲时段内调度函数的执行。这些函数通常用于执行非关键性的、低优先级的任务,…...
工作:SolidWorks从3D文件导出2D的DWG或DXF类型文件方法
工作:SolidWorks从3D文件导出2D的DWG或DXF类型文件方法 SolidWorks从3D文件导出2D的DWG或2D DXF类型文件方法(一)打开3D文件(二)从装配体到工程图(三)拖出想要的角度的图型(四&#…...
element-ui radio和checkbox禁用时不置灰还是原来不禁用时的样式
把要紧用的内容加上一个class"notEdit-page" z注意要在style里面写不能加上scoped /*//checkBox自定义禁用样式*//*//checkBox自定义禁用样式*/ .notEdit-page.el-checkbox__input.is-disabled.is-checked.el-checkbox__inner::after {border-color: #fff; } .notEdi…...
MySQL 8.0 安装与配置技术文档(Ubuntu22.04)
MySQL 8.0 安装与配置技术文档 目录 环境准备下载 MySQL 安装包检查是否已安装 MySQL彻底卸载 MySQL安装 MySQL配置 MySQL创建用户并允许外网访问修改 root 用户密码参考链接 1. 环境准备 确保系统为 Ubuntu 22.04,并安装了以下基础工具: sudo apt-ge…...