自学内容网 自学内容网

Spring Data JPA

1. Spring Data JPA 中的 CRUD 操作

Spring Data JPA 简化了数据库的 CRUD(创建、读取、更新、删除)操作,通过 Repository 接口的自动生成方法,开发者无需手动编写 SQL 语句即可完成大部分操作。下面详细讲解如何定义 Repository 接口、使用自动生成的 CRUD 方法、以及自定义方法。

1.1 定义 Repository 接口

这个接口的定义是在数据持久层(DAO 层,Repository 层),主要用于自定义一些查询方法(这是这个接口最主要的作用)。接口内部可以通过命名规则定义自定义查询方法,以便在业务逻辑层调用。(1.3中的方法命名规则)。

在 Spring Data JPA 中,你可以通过继承 CrudRepository JpaRepository 接口来定义 Repository,用于处理特定实体的 CRUD 操作。Spring 会自动生成这些接口中的方法实现。

  • CrudRepository:这是最基本的接口,提供基本的增删改查操作,如 save()findById()delete()count()
  • JpaRepository:继承自 CrudRepository,在其基础上增加了更多 JPA 特性(如分页和排序等功能),一般情况直接实现这个接口,因为这个接口中包含了增删改查操作
public interface UserRepository extends JpaRepository<User, Long> {
    // 自定义查询方法可以在这里定义
}

解释 JpaRepository<User, Long>

  • User 是实体类的类型。
  • Long 是实体类的主键类型。 这两个参数是泛型,具体化了接口中的操作,告诉 JPA 你要管理哪个实体类以及实体类的主键是什么类型。

1.2 常见 CRUD 方法

这些方法的调用是在业务逻辑层(Service 层)中,以下的常见CRUD方法是你不用在那个接口中去定义也能调用的方法,如果想调用自定义的查询方法,需要在上面的接口中按照方法命名规则去定义,然后才能在业务逻辑层中调用。

当你定义了 Repository 接口后,Spring Data JPA 会自动为你生成常见的 CRUD 方法,你可以直接调用这些方法来进行操作。

在 Spring 中,你不需要手动实例化 Repository。通过依赖注入(@Autowired 注解),Spring 会自动将 UserRepository 注入到你的服务类中,管理其生命周期。你可以直接在服务中使用 userRepository 的方法:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public void saveUser(User user) {
        userRepository.save(user);
    }
}

以下的CRUD方法是调用的时候都要调要先依赖注入userRepository对象,然后再调用该对象的以下这些方法

1.2.1 save()

用于保存或更新实体。如果实体对象没有主键,会执行保存(插入);如果有主键且已经存在,则执行更新。

User user = new User();
user.setName("John");
userRepository.save(user);  // 插入新用户

1.2.2 findById()

根据主键查找实体类对象。findById()方法返回一个 Optional<User>Optional 是 Java 8 引入的类,用于防止空指针异常。

关于 OptionalOptional 是为了安全地处理可能为空的值。你需要通过 userOptional.get() 来获取实际的 User 对象。使用 Optional 可以避免直接处理 null,从而减少空指针异常。

Optional<User> userOptional = userRepository.findById(1L);
if (userOptional.isPresent()) {
    User user = userOptional.get();
    System.out.println(user.getName());
}

这里实体类User的主键就是 Long类型的ID,通过ID来查找用户。

1.2.3 findAll()

返回所有实体对象的列表,相当于执行 SELECT * FROM 语句。

List<User> users = userRepository.findAll();

1.2.4 delete()

根据实体对象或主键删除数据。

userRepository.deleteById(1L);  // 根据 ID 删除用户

1.2.5 count()

返回数据库中实体的总数。

long userCount = userRepository.count();

这些方法的实现都由 Spring Data JPA 自动生成,你只需要调用它们,无需手动实现。

 1.3 自定义 Repository 接口

除了使用 Spring Data JPA 提供的内置方法之外,你还可以通过定义自定义查询方法来扩展 Repository 接口。Spring Data JPA 支持根据方法名称推断 SQL 语句,这样无需编写复杂的 JPQL 或 SQL。

例如,根据用户的电子邮件查找用户:

public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);
}

Spring Data JPA 会根据方法名 findByEmail 自动生成相应的查询逻辑,相当于执行如下 SQL 语句:

SELECT * FROM user WHERE email = ?;

常见的方法命名规则:

  1. findBy[FieldName]:根据字段查找实体。例如:findByName(String name)
  2. countBy[FieldName]:统计符合条件的实体数量。例如:countByEmail(String email)
  3. deleteBy[FieldName]:根据字段删除实体。例如:deleteByEmail(String email)
  4. 支持查询条件组合:通过 AndOr 连接多个条件。例如:findByNameAndEmail(String name, String email)

findByName(String name) 的自动实现机制:Spring Data JPA 提供了一种基于方法名解析查询的机制。你只需要在接口中定义符合命名规则的方法(如 findByName),Spring 会根据方法名自动生成相应的 SQL 查询。比如 findByName 会自动生成类似于 SELECT * FROM user WHERE name = ? 的查询逻辑。

 

你只需定义方法,不需要手动实现,Spring 会在运行时根据方法名推断查询条件。Spring Data JPA 的方法命名规则非常强大,允许你通过简洁的命名实现复杂的查询。

1.4 关于查询方法的层层调用

1.4.1 Repository 层(持久层)

public interface UserRepository extends JpaRepository<User, Long> {
    User findByName(String name);
}
  • Repository 层定义了与数据库的直接交互方法。
  • 例如,UserRepository 定义 findByName() 方法来查询用户。

1.4.2 Service 层(业务逻辑层)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User findUserByName(String name) {
        return userRepository.findByName(name);
    }
}
  • Service 层会调用 Repository 层提供的方法来处理业务逻辑。它将数据访问层和业务逻辑分离,以确保代码的可维护性。

1.4.3 Controller 层(表现层)

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public User getUserByName(@RequestParam String name) {
        return userService.findUserByName(name);
    }
}
  • Controller 层负责处理用户的请求(如 REST API 请求),它会调用 Service 层来执行业务逻辑。

 这是个层层调用的状态,Repository层定义了,然后在Rervice层定义的方法中调用,然后再在Controller层定义的方法中调用Service层定义的方法。(这里要注意三个层中的方法名很相似但不一样)

2. 实体映射与关系建模

实体映射与关系建模是 JPA 中的核心概念,帮助开发者将 Java 对象与关系数据库中的表进行关联,并定义对象之间的关系。

2.1 基本注解

2.1.1 @Entity

  • 作用:标记一个 Java 类为 JPA 实体类,表示它将映射到数据库中的表。

  • 使用:每个实体类都需要加上 @Entity 注解,才能被 JPA 识别为持久化实体。

@Entity
public class User {
    @Id
    private Long id;
    private String name;
}

2.1.2 @Id

  • 作用:标记实体类中的字段为主键。

  • 使用:每个实体类必须有一个主键字段,用 @Id 注解标识。

@Id
private Long id;

2.1.3 @GeneratedValue

  • 作用:用于定义主键的生成策略。
  • 常见的生成策略
    • IDENTITY:数据库使用自增列生成主键(适用于 MySQL 等支持自增列的数据库)。
    • SEQUENCE:使用数据库序列生成主键(适用于支持序列的数据库如 PostgreSQL)。
    • AUTO:JPA 自动选择适合当前数据库的生成策略。
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

2.1.4 @Table

  • 作用:指定实体类对应的数据库表名称。默认情况下,实体类名就是表名,但可以通过 @Table 注解指定不同的表名。

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    private String name;
}

2.2 字段映射

2.2.1 @Column

  • 作用:映射实体类的字段到数据库表的列,可以指定列名、长度、是否可为空等。

@Column(name = "user_name", length = 50, nullable = false)
private String name;

2.2.2 @Transient

  • 作用:忽略不需要持久化的字段,加上 @Transient 注解的字段不会映射到数据库中。

@Transient
private String tempData;

2.2.3 @Temporal

@Temporal 注解用于将 Java 中的 java.util.Datejava.util.Calendar 类型映射到数据库中的特定时间类型。它可以将 Java 的日期时间类型分解为 DATETIMETIMESTAMP 类型。

  • 作用:主要用于精确映射日期和时间字段。数据库存储时,不同的类型可以反映出不同的精度。
    • DATE:仅存储日期部分(年、月、日),不存储时间。
    • TIME:仅存储时间部分(时、分、秒),不存储日期。
    • TIMESTAMP:存储完整的日期和时间。
@Temporal(TemporalType.DATE)
private Date birthDate;

这段代码将Java中特有的Date类型属性在映射到数据库中的字段时进行了处理,如果是TemporalType.DATE的情况,在数据库中只会存储birthDate的日期,而不会存储具体的时间(Java中的Date类型属性包含了日期与精确时间,在这里进行了切割处理)。

2.3 关系映射

关系映射处理实体类之间的关联关系,包括一对一、一对多、多对多等关系,通常通过外键实现。

2.3.1 一对一(@OneToOne

  • 作用:映射一对一的实体关系。通常在数据库中通过外键或共享主键来实现一对一关系。

@OneToOne
@JoinColumn(name = "profile_id")
private UserProfile profile;

假设这段代码在User类中,User类映射到了User表,而在User类中具有一个UserProfile类的对象作为其属性,现在就相当于将User表与UserProfile表进行了一对一关联,每个User表中的记录都与一个UserProfile表中的记录相关联。

 

至于这里的@JoinColumn(name = "profile_id"),其实与@Column注解一样,就是将所注解的属性映射为数据库中的一个字段,区别是,@JoinColumn注解标记的是一个对象属性,这个对象是属性没办法作为一个字段存入User表中,只能将其映射为一个名为profile_id的字段(这个字段作为User表的外键列指向表UserProfile的主键列,所以这个profile_id字段还是存在User表中,自动与表UserProfile中的主键列字段匹配)@JoinColumn注解相较于@Column注解多了个Join的作用就是将该字段标记为指向其他表的主键列的外键列字段。

2.3.2 一对多/多对一(@OneToMany, @ManyToOne

  • @OneToMany:一个实体可以关联多个其他实体。例如,一个用户可以有多个订单。

  • @ManyToOne:多个实体可以关联同一个实体。例如,多个订单可以关联同一个用户。

  • mappedBy:定义了关系的维护方,mappedBy 后面跟的是在对方实体中表示关系的属性名,而不是数据库表中的外键列名。

User 类:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders;
}
  • mappedBy = "user":这里的 "user" 指的是 Order 类中的 user 属性。它告诉 JPA,这个一对多关系的外键是由 Order 类中的 user 属性来管理的。

意思就是在User表中不存在外键列指向Order表中的主键列,只有Order表中存在外键列指向User表的主键列,这里mappedBy = "user"的user是在Order类中存在的一个属性,下面的Order类中可以看到,有一个 private User user属性。

Order 类:

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")  // 指定外键列为 user_id
    private User user;
}
  • Order 类中,@ManyToOne 注解和 @JoinColumn(name = "user_id") 表示 user 属性将被映射为数据库表中的 user_id 外键列,这个列会保存关联的 User 的主键值。

2.3.3 多对多(@ManyToMany

  • 作用@JoinTable 主要用于多对多(@ManyToMany)的关系。因为在多对多的关系中,通常会有一个独立的中间表来管理两个实体之间的关联关系,这个表通常包含两个外键,分别指向两个实体的主键。

  • 使用场景:当两个实体之间存在多对多的关系时,需要一个中间表来保存两个实体的主键关联。@JoinTable 用于定义这个中间表的名称,以及中间表中的外键列。

  • 例子

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",  // 中间表的名称
        joinColumns = @JoinColumn(name = "student_id"),  // 当前实体(Student)的外键列
        inverseJoinColumns = @JoinColumn(name = "course_id")  // 对方实体(Course)的外键列
    )
    private List<Course> courses;
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
}

在这个例子中,@JoinTable 用于定义 student_course 中间表,它包含两个外键列:student_id(指向 Student 实体的主键)和 course_id(指向 Course 实体的主键)。 

两个表因为多对多的关系,所以两个表中按理来说都要有都具有自己的主键列与指向对方主键列的外键列 ,所以这里就创建了一个中间表,这个表中的每一条记录都包含两个表的外键列字段,通过这一张表将两个表中的记录相关联。    

示例如下:

  • Student 表:

    idname
    1Alice
    2Bob
  • Course 表:

    idname
    101Math
    102Science
  • student_course 中间表:

    student_idcourse_id
    1101
    1102
    2101

2.3.4级联操作和抓取策略

1. CascadeType

  • 作用:用于配置级联操作,意味着对父实体的操作会影响其关联的子实体。例如,如果删除了一个用户,级联操作可以删除与之关联的所有订单。

  • 常用的 CascadeType

    • ALL:应用所有级联操作。
    • PERSIST:当保存父实体时,保存其关联的实体。
    • REMOVE:当删除父实体时,删除其关联的实体。
    • MERGE:当合并父实体时,合并其关联的实体。

    示例

    @OneToMany(cascade = CascadeType.ALL)
    private List<Order> orders;
    

2. FetchType(抓取策略)

  • 作用:控制如何加载实体之间的关系。FetchType.LAZYFetchType.EAGER 是最常用的两种抓取策略。

    • LAZY:延迟加载。当访问关联实体时才会进行加载。
    • EAGER:急加载。在加载父实体时,立即加载其关联的所有实体。

    示例

    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;
    

2.4 使用 @Embeddable@Embedded

2.4.1 @Embeddable

  • 作用:将类定义为可嵌入的类。这个类不会独立存在,它的字段会被嵌入到宿主实体中。

    @Embeddable
    public class Address {
        private String street;
        private String city;
    }
    

2.4.2 @Embedded

  • 作用:在实体中嵌入 @Embeddable 类的实例,将其字段作为宿主实体的一部分。

    @Entity
    public class User {
        @Id
        private Long id;
    
        @Embedded
        private Address address;
    }
    

    解释@Embedded 的作用是将嵌入类的字段映射到主实体表中,而不是作为独立的表。例如,Address 类的字段会直接映射到 User 表的列中。就是为主实体表增添一些字段。 

3. Spring Data JPA 的查询机制

Spring Data JPA 提供了多种查询方式,涵盖了从简单的基于方法名称的查询,到复杂的 JPQL 和原生 SQL 查询。通过灵活的查询机制,开发者可以轻松地获取数据,同时保持与对象模型的集成。

  • 以下对于查询方法的定义都是在实现Repository接口的类中进行。
public interface UserRepository extends JpaRepository<User, Long> {
    // 自定义查询方法可以在这里定义
}
  • 以下对于查询方法的定义基本上没有函数体,要么方法名成来定义查询方法,要么靠注解中的语句来定义查询方法。

3.1 基于方法名称的查询

基于方法名称的查询是一种非常便捷的方式,Spring Data JPA 可以通过遵循特定命名规则的接口方法,自动生成相应的 SQL 查询。

1. 方法名称的约定与规则

Spring Data JPA 允许开发者通过定义符合命名规则的方法来生成查询,无需手动编写 SQL。根据方法的命名,Spring Data JPA 自动生成查询逻辑。

  • 常见方法命名模式

    • findBy[Field]:根据指定字段查询,如 findByName()
    • findBy[Field]And[Field]:根据多个字段查询,如 findByNameAndAge()
    • findBy[Field]Between:根据字段的范围查询,如 findByAgeBetween()

示例

List<User> findByName(String name);
List<User> findByAgeBetween(int startAge, int endAge);
List<User> findByAddress_City(String city);

这种基于基于方法名称的查询方法的参数必须是数据库中的字段(用于筛选记录)当然还有第二种情况,如下:

Spring Data JPA 特别支持 SortPageable 参数,因为它们用于控制查询结果的表现形式,而不是直接用于构建查询条件。以下是它们的作用:

  • Sort:定义结果的排序规则(按哪一列排序,升序或降序)。
  • Pageable:定义分页行为(查询哪一页、每页多少条记录等)。

它们的存在并不会影响生成的查询条件,只是在查询结果被返回时进行处理。因此,Spring Data JPA 允许 SortPageable 作为额外的参数来增强查询的灵活性。

2. 方法名称与自动生成的 SQL

  • findByName(String name):自动生成的 SQL 类似于 SELECT * FROM user WHERE name = ?
  • findByAgeBetween(int startAge, int endAge):自动生成的 SQL 类似于 SELECT * FROM user WHERE age BETWEEN ? AND ?
  • findByAddress_City(String city):自动生成的 SQL 类似于 SELECT * FROM user WHERE address_city = ?

3. 方法名称的灵活性

Spring Data JPA 的命名约定十分灵活,支持条件组合(AndOr)、比较运算符(LessThanGreaterThan)、排序等操作。例如:

List<User> findByAgeGreaterThan(int age);
List<User> findByNameOrAge(String name, int age);

以上所有的 List<User> findByName(String name);都是定义了一个方法,这些方法的返回值是一个以User为节点的线性表,括号内的参数名称要与数据库表中的字段名一致才行,这些方法在Repository层中定义了之后,再被Service层调用。


3.2 JPQL(Java Persistence Query Language)

JPQL 是 JPA 的查询语言,语法与 SQL 类似,但它操作的是实体类及其属性,而不是数据库的表和列。JPQL 提供了一种面向对象的查询方式,允许开发者使用实体类和关系模型进行查询。

3.2.1 基本 JPQL 语法

@Query("SELECT <实体别名> FROM <实体类名> <实体别名> WHERE <条件表达式包含命名参数>")
返回类型 方法名(参数类型 参数名1, 参数类型 参数名2);

3.2.2 JPQL中使用命名参数

  • 命名参数:以 : 开头,如 :name,使用 @Param 注解绑定参数。

 使用 命名参数 的 JPQL 示例:

@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") int age);

WHERE u.name = :name AND u.age > :age:查询条件使用了命名参数。:name 是绑定到 String name 参数,:age 是绑定到 int age 参数。 

3.2.3 面向对象的特性

JPQL 提供了类似 SQL 的功能,包括 JOINGROUP BYORDER BY 等,但它操作的是对象及其关系。

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u JOIN u.orders o WHERE o.status = :status")
    List<User> findUsersByOrderStatus(@Param("status") String status);
}
  • @Query 注解:使用 JPQL 定义自定义查询。
  • SELECT u FROM User u JOIN u.orders o
    • uUser 实体的别名。
    • u.orders 表示 User 实体中的 orders 属性,这个属性代表的是 UserOrder 之间的关联关系(即用户的订单)。
    • JOIN 表示通过对象关系进行连接。oorders 的别名,用于在查询中引用订单的属性。
  • WHERE o.status = :status:筛选条件,查询订单状态为指定值的用户。

3.2.4 查询结果映射到 DTO

有时你可能不希望返回完整的实体对象,而是只需要一些特定的字段。你可以通过自定义 DTO(数据传输对象)来接收查询结果。 

在 JPQL 中,处理 DTO 的方法是使用 new 关键字来调用 DTO 的构造函数。JPQL 会将查询的结果映射到 DTO 对象中。这个方法非常方便,因为 JPA 会自动将结果映射到 DTO 对象,而不需要手动提取和赋值。 

  • 定义 DTO 类

    public class UserDTO {
        private String name;
        private int age;
    
        // 构造函数
        public UserDTO(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        // getters and setters
    }
    
  • 使用 JPQL 映射到 DTO

    @Query("SELECT new com.example.dto.UserDTO(u.name, u.age) FROM User u WHERE u.status = :status")
    List<UserDTO> findUserDTOsByStatus(@Param("status") String status);
    
  • 说明

    • new com.example.dto.UserDTO(u.name, u.age):使用 new 关键字调用 DTO 的构造函数,将查询结果中的 u.nameu.age 传递给构造函数。
    • DTO 的构造函数需要匹配查询结果中列的类型和顺序。

JPQL 处理 DTO 的特点

  1. 自动映射:JPQL 自动将查询结果映射到 DTO,通过调用 DTO 的构造函数创建 DTO 对象。
  2. 简洁:只需在查询中使用 new 关键字指定 DTO 的构造函数,JPA 会自动处理映射。
  3. 适用场景:适合需要返回部分字段或自定义数据结构的场景,避免返回整个实体。

 


3.3 原生 SQL 查询

原生 SQL 查询允许你直接编写数据库特定的 SQL 语句。这种方式适合在 JPQL 无法满足需求的情况下,例如需要执行复杂的数据库查询或使用数据库特定的功能时。

3.3.1 基本原生SQL 语法

@Query(value = "SELECT <列名1>, <列名2> FROM <表名> WHERE <条件表达式包含命名参数>", nativeQuery = true)
List<Object[]> 方法名(@Param("参数名1") 参数类型 参数名1, @Param("参数名2") 参数类型 参数名2);
  • 这里value的作用就是标记这个为原生SQL查询。
  • 查询得到的结果会返回到以数组Object为节点的线性表中,这里一个节点的数组Object就代表了一行,这个数组中的Object[1]、Object[2]就代表了你查询到的记录的相应字段。

3.3.2 原生SQL中使用位置参数 

  • 位置参数:使用 ?1, ?2 等来占位,表示参数的顺序。

使用 位置参数 的 JPQL 示例:

@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age > ?2")
List<User> findByNameAndAge(String name, int age);

WHERE u.name = ?1 AND u.age > ?2:这是查询条件。?1 是第一个位置参数,对应方法参数 String name?2 是第二个位置参数,对应方法参数 int age。 

3.3.3 原生 SQL 查询结果的处理 

查询结果会自动映射到方法的返回类型。例如,如果返回类型是 List<Object[]>,那么查询结果的每一行都会被映射为 Object[] 数组,数组中的每个元素对应查询语句中的列。 

@Query(value = "SELECT u.name, u.age FROM users u WHERE u.status = :status", nativeQuery = true)
List<Object[]> findUserDataByStatus(@Param("status") String status);

public List<UserDTO> findUserDTOsByStatus(String status) {
    List<Object[]> results = userRepository.findUserDataByStatus(status);
    List<UserDTO> dtos = new ArrayList<>();

    for (Object[] result : results) {
        String name = (String) result[0];
        int age = (int) result[1];
        dtos.add(new UserDTO(name, age));
    }

    return dtos;
}
  • 在原生 SQL 查询中,返回的 List<Object[]> 列表中,每个 Object[] 数组代表一行记录,数组中的每个元素代表查询结果中的一个列值。
  • 开发者需要通过数组下标(如 result[0], result[1])来访问特定列的值。
  • 这种方法灵活,但需要手动处理数据,将其映射到 DTO 或其他数据对象中。

3.4 JPQL与原生SQL的区别 

3.4.1 操作对象的不同

  • SQL:SQL 查询语句中使用的是数据库表名字段名。SQL 直接操作表和列,查询结果是表中的数据行。

  • JPQL:JPQL 查询语句中不能使用表名字段名,而是使用实体类名属性名。JPQL 操作的是 Java 实体类及其属性,查询结果是实体类对象。

    示例

    • SQLSELECT * FROM users WHERE name = 'John';
    • JPQLSELECT u FROM User u WHERE u.name = 'John';

    在 JPQL 中,users 表名被替换为 User 实体类名,name 列名被替换为 name 属性。

3.4.2 连接操作的不同

  • SQL:在 SQL 中,连接操作通过外键字段显式进行,JOIN 后直接使用的是表名。例如,通过外键连接两个表时,必须指定表名并使用外键进行连接。

  • JPQL:在 JPQL 中,不能直接在 JOIN 后使用表名。连接操作是基于实体类的关联属性来进行的,使用实体类的属性而不是表名来实现连接。连接的目标是实体类关联的属性,而不是表本身。

    示例

    • SQLSELECT u.*, o.* FROM users u JOIN orders o ON u.id = o.user_id;
    • JPQLSELECT u FROM User u JOIN u.orders o WHERE :id = o.user_id;

    在 JPQL 中usersorders 表名被替换为 User 实体类和其 orders 属性。连接是通过 u.orders 实体属性进行的,而不是通过外键字段。

3.4.3 参数化查询的不同

  • SQL:SQL 查询中使用位置参数(?)进行参数化查询,参数按顺序绑定。SQL 只支持位置参数,无法直接使用命名参数。

  • JPQL:JPQL 支持命名参数(如 :paramName)。命名参数在 JPQL 中更加灵活,可以通过名称绑定具体的参数值。

    示例

    • SQLSELECT * FROM users WHERE name = ? AND age > ?;
    • JPQLSELECT u FROM User u WHERE u.name = :name AND u.age > :age;

 总的来说,JPQL可以将查询到的结果直接映射为DTO对象,还是更为方便。

4. 分页和排序

在 Spring Data JPA 中,分页查询是处理大数据集时的常见需求。它可以让你按照页数逐步获取数据,而不是一次性获取所有数据。分页查询通常与排序结合使用,以确保数据按预期顺序返回。Spring Data JPA 提供了灵活的分页和排序机制,主要使用 PageablePageRequestSort 类。

4.1 分页查询 

4.1.1 使用 PageablePageRequest 进行分页查询

Pageable 是分页查询的抽象接口,包含了分页信息(如页码、每页大小、排序等)。PageRequestPageable 的实现类,提供了具体的分页参数生成功能

示例代码:

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findByStatus(String status, Pageable pageable);
}

在使用这个方法时,你需要传入一个 Pageable 对象,通常使用 PageRequest.of() 来生成。

@GetMapping("/users")
public Page<UserDTO> getUsersByStatus(@RequestParam String status,
                                      @RequestParam int page,
                                      @RequestParam int size) {
    Pageable pageable = PageRequest.of(page, size);  // 创建 Pageable 对象
    return userService.getUsersByStatus(status, pageable);  // 传递 Pageable 参数
}

详细解释:

  1. Pageable pageable 作为参数时的传参
    • 通过 PageRequest.of(page, size) 创建一个 Pageable 实例。
    • page 是页码,从 0 开始,size 是每页的记录数。将 Pageable 作为参数传递给方法,以控制分页行为。
    • size 的作用是决定每页的数据量,即每页显示多少条记录。PageRequest.of(page, size) 会按照这个 size 的值将数据库的记录分成多页。然后,page 参数用来指定要获取第几页的数据。

 这里的getUsersByStatus()方法是讲Page类对象作为返回类型,也可以将Slice类对象作为返回对象,下面会讲解二者的区别。

4.1.2 PageSlice 的对比

Page

示例代码:

Page<User> userPage = userRepository.findByStatus(status, pageable);
List<User> users = userPage.getContent();  // 获取当前页数据
long totalElements = userPage.getTotalElements();  // 获取总记录数
boolean hasNextPage = userPage.hasNext();  // 是否有下一页
  • 功能Page 是一个完整的分页结果,包含当前页的数据以及分页元数据信息,如总页数、总记录数、是否有下一页等。
  • 使用场景:适用于需要完整分页信息的场景,例如需要知道总页数或总记录数的情况。
  • 开销:因为 Page 需要计算总记录数,所以在查询大数据集时性能开销较大,适合需要精确分页控制的场景。

Slice

示例代码:

Slice<User> userSlice = userRepository.findByStatus(status, pageable);
List<User> users = userSlice.getContent();  // 获取当前页数据
boolean hasNextSlice = userSlice.hasNext();  // 是否有下一页
  • 功能Slice 提供了部分分页功能,主要用于判断是否有下一页,而不计算总记录数或总页数。Slice 返回的数据和 Page 类似,但性能开销较小,因为不需要执行 count 查询。
  • 使用场景:适用于 "加载更多" 或滚动加载的场景,适合不需要总记录数的情况。
  • 开销Slice 不计算总记录数,性能比 Page 高,在处理大数据集时更加高效。
  • getContent():返回当前页的数据列表,通常为 List<User>
  • Page:提供完整的分页信息(当前页数据、总记录数、总页数、是否有下一页),适合需要精确分页控制的场景。
  • Slice:只提供部分分页信息(当前页数据、是否有下一页),适合性能敏感、需要 "加载更多" 风格分页的场景。

4.1.3 自定义分页参数

在实际应用中,你可以根据需求自定义分页参数(如页码、每页大小)。通常使用 @RequestParam 来接收这些分页参数,并传递给 PageRequest

示例代码:

@GetMapping("/users")
public Page<UserDTO> getUsersByStatus(@RequestParam String status,
                                      @RequestParam(defaultValue = "0") int page,
                                      @RequestParam(defaultValue = "10") int size) {
    Pageable pageable = PageRequest.of(page, size);  // 自定义分页参数
    return userService.getUsersByStatus(status, pageable);
}
  • @RequestParam(defaultValue = "0"):当客户端不传递分页参数时,使用默认值进行分页查询。默认情况下,第一个页是 0,每页返回 10 条记录。

4.2 排序查询

排序查询是通过指定字段或多个字段对查询结果进行排序,Spring Data JPA 使用 Sort 对查询结果进行排序

Sort 是 Spring Data JPA 提供的用于查询排序的类,允许通过指定属性名和排序方向(升序或降序)来对查询结果排序。

示例代码:

public List<User> findByStatus(String status, Sort sort) {
    return userRepository.findByStatus(status, sort);
}

在控制层中,可以使用 Sort 来指定排序规则:

@GetMapping("/users")
public List<UserDTO> getUsersByStatus(@RequestParam String status,
                                      @RequestParam String sortBy,
                                      @RequestParam String direction) {
    Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy);
    return userService.getUsersByStatus(status, sort);
}
  • status:查询条件,用于过滤用户的状态(例如 activeinactive 等)。此参数与数据库中 status 字段对应,用于确定应该获取哪些用户。
  • sortBy:排序字段,用于指定查询结果按照哪个字段进行排序(例如 nameage 等)。这个参数对应于数据库中的字段,用于定义按哪个字段进行排序。
  • direction:排序方向,用于指定排序的方式。可能的值有 "asc"(升序)或 "desc"(降序)。该参数用于控制查询结果的排列顺序。

1. Sort.by():创建 Sort 对象,指定排序方向(升序或降序)和排序字段。

以下解释Sort.by()这个方法中的两个参数

 

2.解释参数 Sort.Direction.fromString(direction)

  • direction:客户端传递的字符串,如 "asc""desc"
  • Sort.Direction.fromString(direction):将字符串 "asc" 转换为枚举值 Sort.Direction.ASC,或将 "desc" 转换为 Sort.Direction.DESC。这里规定的第一个参数必须是枚举类的Sort.Direction.ASC(升序)或Sort.Direction.DESC(降序)。

3.解释参数 sortBy

  • sortBy:指定排序字段,例如 nameage

4.3 实现分页查询与排序结合

Spring Data JPA 允许通过 PageRequest.of() 方法创建分页请求时同时指定排序规则。这个方法不仅接收页码和每页大小,还可以接受 Sort 对象来定义排序方式。

示例:结合分页和排序的查询

假设我们有一个 User 实体类,我们希望按照用户的 name 升序排列,同时进行分页查询。你可以使用以下方式实现分页和排序的结合:

public Page<User> findUsersByStatus(String status, Pageable pageable);

在控制层中,我们可以这样传递分页和排序信息:

@GetMapping("/users")
public Page<UserDTO> getUsersByStatus(@RequestParam String status,
                                      @RequestParam int page,
                                      @RequestParam int size,
                                      @RequestParam String sortBy,
                                      @RequestParam String direction) {
    // 使用 PageRequest 创建分页和排序对象
    Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(direction), sortBy));
    return userService.getUsersByStatus(status, pageable);  // 执行分页和排序查询
}

解释代码中的细节:

  1. PageRequest.of(page, size, Sort.by(...))

    • page:表示要查询的页码(从 0 开始)。
    • size:表示每页返回的记录数。
    • Sort.by(Sort.Direction.fromString(direction), sortBy):定义排序规则,其中 sortBy 是要排序的字段名,direction 是排序的方向(升序 ASC 或降序 DESC)。

 相当于就是为PageRequest.of()多加了一个参数Sort。

5. 错误处理与调试

在 Spring Data JPA 和 Hibernate 开发中,错误处理和调试是保证代码健壮性和提高性能的重要环节。通过有效的错误处理可以避免系统崩溃或不一致性问题,而调试和日志记录可以帮助开发者更好地理解查询的行为、性能瓶颈以及其他潜在的问题。

5.1 常见错误处理

在 JPA 使用中,会遇到一些常见的异常或错误。了解这些异常的原因和处理方式,可以有效地提高系统的健壮性和稳定性。

5.1.1 LazyInitializationException

  • 场景LazyInitializationException 通常发生在使用 延迟加载 (Lazy Loading) 时。如果你试图访问一个延迟加载的关联数据(如 @OneToMany@ManyToOne 的关联对象),而 EntityManager 已经关闭,Hibernate 就无法再初始化该对象,因此抛出该异常。

    原因:在 EntityManager 关闭后,尝试访问与数据库连接相关的延迟加载数据。

    解决方法

    • 调整加载策略:将关联的加载策略从 LAZY 改为 EAGER,这样数据会在查询时立即加载,而不会等到关联对象被访问时才加载。
    • 显式加载:在事务内通过 fetch join 或调用实体的 get 方法来手动加载关联数据。

    示例:调整加载策略

    @OneToMany(fetch = FetchType.EAGER)
    private List<Order> orders;
    

5.1.2 OptimisticLockException

  • 场景OptimisticLockException 通常在并发更新时发生。使用 乐观锁 (Optimistic Locking) 时,多个事务可能同时修改同一条记录,而乐观锁通过比较版本号(@Version 注解字段)来检测冲突。如果版本号不匹配,则会抛出该异常。

    原因:两个并发事务修改了同一个实体对象,提交时版本号不匹配,导致乐观锁检查失败。

    解决方法

    • 捕获异常并重试:在代码中捕获 OptimisticLockException,并根据业务逻辑决定是否重新尝试事务。
    • 合理设计并发逻辑:确保并发事务对相同数据的修改尽量减少,或者通过事务管理重试机制处理并发失败。

    示例:使用版本控制

    @Version
    private Long version;
    

5.1.3 处理查询结果为空的情况

  • 场景:当查询结果为空时,通常需要进行相应处理,避免空指针异常或者业务逻辑异常。

    常见解决方法

    • 返回 Optional:Spring Data JPA 提供了 Optional 类型的方法返回值,可以有效地处理查询结果为空的情况。通过 Optional 的方法如 isPresent()orElse() 进行安全访问。
    • 异常处理:通过抛出自定义异常,如 EntityNotFoundException,来标记查询结果为空的情况。

    示例:使用 Optional 处理空查询

    Optional<User> user = userRepository.findById(userId);
    return user.orElseThrow(() -> new EntityNotFoundException("User not found"));
    

5.2 配置 Spring Data JPA 的 SQL 日志输出

为了查看 Hibernate 生成的 SQL 查询以及它的执行过程,可以通过配置日志输出,记录 SQL 查询和参数。这对调试查询错误、优化查询性能都非常有帮助。

如何启用 SQL 日志

  • application.properties 文件中配置:

    示例:启用 SQL 日志输出

    # 显示生成的 SQL 语句
    spring.jpa.show-sql=true
    
    # 格式化 SQL 输出,便于阅读
    spring.jpa.properties.hibernate.format_sql=true
    
    # 输出 SQL 参数值
    logging.level.org.hibernate.type.descriptor.sql=TRACE
    

    详细说明

    • spring.jpa.show-sql=true:开启 SQL 输出,可以在控制台看到执行的 SQL 语句。
    • spring.jpa.properties.hibernate.format_sql=true:格式化 SQL 输出,使得长查询语句更容易阅读。
    • logging.level.org.hibernate.type.descriptor.sql=TRACE:打印 SQL 语句中的参数值(默认情况下,SQL 参数不会显示)。

 

 


原文地址:https://blog.csdn.net/m0_73837751/article/details/142497681

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!