Spring-Data-Jpa

JPA、Hibernate 和 Spring Data JPA 中的数据库审计

1、概览 就 ORM 而言,数据库审计指的是跟踪和记录与实体相关的事件,或者简单地说是实体版本管理。受 SQL 触发器的启发,这些事件是对实体的插入、更新和删除操作。数据库审计的好处类似于源代码版本控制。 本文将带你了解在应用中使用审计的三种方法。首先介绍来自于 JPA 标准的审计实现、然后再介绍由 Hibernate 和 Spring Data 分别提供的审计的扩展实现。 下面是本文中使用的相关实体 Bar 和 Foo 示例: 2、JPA 审计 JPA 并没有明确包含审计 API,但我们可以通过使用实体生命周期事件来实现这一功能。 2.1、@PrePersist、@PreUpdate 和 @PreRemove 在 JPA 实体类中,我们可以使用 @PrePersist、@PreUpdate 和 @PreRemove 注解指定一个方法作为回调,以在相应的 DML 操作前执行它。 @Entity public class Bar { @PrePersist public void onPrePersist() { ... } @PreUpdate public void onPreUpdate() { ... } @PreRemove public void onPreRemove() { ... } } 实体内部的回调方法应是非 static 的,返回类型为 void,且不带参数。访问权限任意。 注意,JPA 中的 @Version 注解与本文的主题没有关系,它是一种乐观锁,与审计数据无关。

在 Spring Data JPA 中使用 Stream(流式)查询

简介 本文将带你了解在 Spring Data JPA 中使用 Stream(流式)查询的最佳方式。 当需要获取较大的结果集时,使用 Java Stream 的好处是可以逐步迭代查询结果集,避免一次性获取所有数据可能导致的内存溢出异常。 JPA Stream 方法 自 2.2 版起,你可以使用 JPA 的 getResultStream 方法以 Stream 的形式处理结果集。 getResultStream 使用 JDBC ResultSet 对给定查询返回的记录进行流式处理。特别是在处理大结果集的时候,这种方法很有效率。 Spring Data JPA Stream 查询方法 如果要对查询结果集进行流式处理,则需要在 Spring Data JPA 查询方法中返回 Java Stream 类型,如下例所示: @Repository public interface PostRepository extends BaseJpaRepository<Post, Long> { @Query(""" select p from Post p where date(p.createdOn) >= :sinceDate """ ) @QueryHints( @QueryHint(name = AvailableHints.HINT_FETCH_SIZE, value = "25") ) // 返回 Stream Stream<Post> streamByCreatedOnSince(@Param("sinceDate") LocalDate sinceDate); } 对于 PostgreSQL 和 MySQL,指定 FETCH_SIZE JPA QueryHint 是必要的,它指示 JDBC 驱动每次迭代的时候最多预取 25 条记录。否则,PostgreSQL 和 MySQL JDBC 驱动会在遍历底层 ResultSet 之前预取所有查询结果。

Spring Data JPA 检索最大值(Max Value)

1、简介 本文将带你了解如何使用 Spring Data JPA 检索数据列中的最大值(Max Value)。 2、示例 首先,添加 spring-boot-starter-data-jpa 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> 然后,定义一个简单的 Employee 实体表示员工: @Entity public class Employee { @Id @GeneratedValue private Integer id; private String name; private Long salary; // 工资 // 构造函数、Getter、Setter 方法省略 } 接下来,看看有哪些方法可以检索出所有员工中薪水(salary)列的最大值。 3、使用 Repository 的派生查询 Spring Data JPA 提供了一个强大的机制,可以使用 Repository 方法来定义自定义查询。其中之一是派生查询,它允许我们通过声明方法名来实现SQL查询。 创建 Employee 实体类的 Repository 接口: public interface EmployeeRepository extends JpaRepository<Employee, Integer> { Optional<Employee> findTopByOrderBySalaryDesc(); } 如上,我们实现了一个方法 findTopByOrderBySalaryDesc,该方法使用查询派生机制生成相应的 SQL。根据方法名,它将按照工资(salary)降序对所有员工进行排序,然后返回第一个员工,即工资最高的员工。 该方法会返回一个加载了所有属性的实体。如果我们只想检索一个工资(salary)值,可以使用投影查询: 创建 EmployeeSalary 投影接口:

在 Spring Data JPA 查询中使用枚举(Enum)

1、概览 在使用 Spring Data JPA 构建持久层时,经常要处理带有枚举字段的实体。这些枚举字段代表一组固定的常量,例如订单的状态、用户的角色或业务的某个阶段。 本文将带你了解如何使用标准的 JPA 方法和原生查询来查询实体类中声明的枚举字段。 2、应用设置 2.1、数据模型 首先,定义数据模型,包括一个枚举字段。 我们示例中的中心实体是 Article 类,它声明了一个枚举字段 ArticleStage,用于表示文章可能处于的不同阶段: public enum ArticleStage { TODO, IN_PROGRESS, PUBLISHED; } ArticleStage 枚举包含三个可能的阶段,代表文章从最初创建到最终发布的生命周期。 接下来,创建声明了 ArticleStage 枚举字段的 Article 实体类: @Entity @Table(name = "articles") public class Article { @Id private UUID id; private String title; private String author; @Enumerated(EnumType.STRING) private ArticleStage stage; // 构造函数/Getter/Setter 方法省略 } 我们将 Article 实体类映射到 articles 数据库表。此外,还使用 @Enumerated 注解指定 stage 字段应作为字符串在数据库中持久化。 2.2、Repository 层 定义好数据模型后,就可以创建一个继承了 JpaRepository 的 Repository 接口,以便与数据库交互:

解决 Spring Data JPA ConverterNotFoundException: No converter found

1、概览 在使用 Spring Data JPA 时,我们经常会利用派生和自定义查询,以我们喜欢的格式返回结果。一个典型的例子就是 DTO 投影,它提供了一种只 SELECT 某些特定列以减少不必要数据开销的好方法。 然而,DTO 投影并不总是那么容易,如果实现不当,可能会导致 ConverterNotFoundException 异常。本文将带你了解 ConverterNotFoundException 异常出现的原因,以及如何在使用 Spring Data JPA 时避免 ConverterNotFoundException 异常。 2、在实践中理解异常 通过一个实际例子来理解异常。 为了简单起见,使用 H2 数据库。首先,在 pom.xml 文件中添加其依赖: <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> 2.1、H2 配置 Spring Boot 提供了对 H2 嵌入式数据库的支持。默认情况下,它会配置应用使用用户名 sa 和空密码连接到 H2。 将数据库连接凭证添加到 application.properties 文件中: spring.datasource.url=jdbc:h2:mem:mydb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= 如上就是使用 Spring Boot 设置 H2 配置所需的全部内容。 2.2、Entity 类 们定义一个 JPA 实体类 Employee: @Entity public class Employee { @Id private int id; @Column private String firstName; @Column private String lastName; @Column private double salary; // Getter/Setter 方法省略 } 如上,员工类(Employee)定义了 id、firstName、lastName 和 salary 属性。

JPA 级联保存实体中的子实体

1、概览 本文将带你了解 JPA 如何自动保存复杂的实体模型(即由父实体和子实体元素组成的复杂模型)以及常见的问题。 2、缺失关系注解 我们可能会忽略的第一件事就是添加关系注解。 创建一个子实体: @Entity public class BidirectionalChild { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; //Get/Set 方法省略 } 创建一个包含 List<BidirectionalChild> 的父实体: @Entity public class ParentWithoutSpecifiedRelationship { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private List<BidirectionalChild> bidirectionalChildren; //Get/Set 方法省略 } 如上,bidirectionalChildren 字段上没有注解。尝试用这些实体创建一个 EntityManagerFactory: @Test void givenParentWithMissedAnnotation_whenCreateEntityManagerFactory_thenPersistenceExceptionExceptionThrown() { PersistenceException exception = assertThrows(PersistenceException.class, () -> createEntityManagerFactory("jpa-savechildobjects-parent-without-relationship")); assertThat(exception) .hasMessage("Could not determine recommended JdbcType for Java type 'com.baeldung.BidirectionalChild'"); } 运行测试,出现了异常,无法确定子实体的 JdbcType。单向和双向关系都会出现类似的异常,其根本原因是父实体中缺失 @OneToMany 注解。

Spring JPA 从序列(SEQUENCE)中获取下一个值

1、简介 Sequence (序列)是用于生成唯一 ID 的数字生成器,可避免数据库中出现重复记录。Spring JPA 为大多数情况提供了自动处理序列的方法。不过,在某些特定情况下,我们可能需要在持久化实体之前手动检索下一个序列值。例如,在将订单(Order)详细信息保存到数据库之前,需要生成一个唯一的订单号。 本文将带你了解使用 Spring Data JPA 从数据库序列中获取下一个值的几种方法。 2、设置项目依赖 首先要在 Maven pom.xml 文件中添加 Spring Data JPA 和 PostgreSQL 驱动依赖,并在数据库中创建序列。 2.1、Maven 依赖 首先,在 pom.xml 中添加必要的依赖项: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> 2.2、测试数据 下面是我们在运行测试用例之前用来准备数据库的 SQL 脚本,可以将该脚本保存为 .sql 文件,并将其放在项目的 src/test/resources 目录中: DROP SEQUENCE IF EXISTS my_sequence_name; CREATE SEQUENCE my_sequence_name START 1; 该命令创建一个从 1 开始的序列,每调用一次 NEXTVAL 就递增一次。 然后,在测试类中使用 @Sql 注解,并将 executionPhase 属性设置为 BEFORE_TEST_METHOD,以便在每个测试方法执行之前将测试数据插入数据库: @Sql(scripts = "/testsequence.sql", executionPhase = Sql.

Spring Data JPA 异常 “IllegalArgumentException: Not a Managed Type”

1、概览 使用 Spring Data JPA 时,应用启动出现异常。大致如下: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerAdapter' ... Caused by: java.lang.IllegalArgumentException: Not a managed type: ...OurEntity at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:583) at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:85) ... 大意是说,一些 Bean 创建失败了,导致应用启动失败。 根异常是 IllegalArgumentException:“Not a managed type”,本文将带你了解出现这个异常的原因,以及如何解决该异常。 2、缺少 @Entity 注解 出现这种异常的一个可能原因是,忘记使用 @Entity 注解来标记实体。 2.1、重现问题 假设有以下实体类: public class EntityWithoutAnnotation { @Id private Long id; } 及其对应的 Spring Data JPA repository: public interface EntityWithoutAnnotationRepository extends JpaRepository<EntityWithoutAnnotation, Long> { } 最后是 Application 启动类,它会扫描上面定义的所有类: @SpringBootApplication public class EntityWithoutAnnotationApplication { } 尝试使用此 Application 来启动 Spring Context:

Spring Data JPA 实现 updateOrInsert(更新或保存)

1、简介 在应用开发中,执行 “更新或插入” 操作(也称为 “upsert”)的需要是很常见的。这个操作涉及将新记录存入数据库表中,如果记录不存在,则插入新记录;如果记录已经存在,则更新现有记录。 本文将带你了解使用 Spring Data JPA 执行 “更新或插入” 操作的不同方法。 2、实体类 定义 CreditCard 实体类用于演示: @Entity @Table(name="credit_card") public class CreditCard { @Id @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "credit_card_id_seq") @SequenceGenerator(name = "credit_card_id_seq", sequenceName = "credit_card_id_seq", allocationSize = 1) private Long id; private String cardNumber; private String expiryDate; private Long customerId; // Get / Set 方法省略 } 3、实现 本文介绍三种不同的方法来实现 “更新或插入”。 3.1、使用 Repository 方法 使用从 CrudRepository 接口继承的 save(entity) 方法在 Repository 中编写一个带事务的 default 方法。

Spring Data JPA 出现异常后不回滚,继续处理事务

1、概览 JPA 中的事务机制是一个功能强大的工具,它通过提交所有更改或在出现异常时回滚更改来确保原子性和数据完整性。然而,在某些情况下,我们可能需要在遇到异常的情况下继续进行事务处理而不回滚数据更改。 2、出现异常后事务自动回滚 在事务中可能会出现两种主要的异常情况。 2.1、在 Service 层出现异常后回滚事务 我们可能遇到回滚(Rollback)的第一个地方是在 Service 层,外部异常可能会影响数据库更改。 让我们通过下面的示例更仔细地了解一下这种情况。首先,添加 InvoiceEntity,作为数据模型: @Entity @Table(uniqueConstraints = {@UniqueConstraint(columnNames = "serialNumber")}) public class InvoiceEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; // 自增 ID private String serialNumber; // 序列号 private String description; // 说明 // Getter / Setter } 如上,包含了一个自增 ID、一个需要在整个系统中唯一的序列号和一个说明。 现在,创建负责发票事务操作的 InvoiceService: @Service public class InvoiceService { @Autowired private InvoiceRepository repository; @Transactional public void saveInvoice(InvoiceEntity invoice) { repository.save(invoice); sendNotification(); } private void sendNotification() { throw new NotificationSendingException("Notification sending is failed"); } } 在 saveInvoice() 方法中,我们添加了事务性保存发票(invoice)和发送相关通知的逻辑。但是,在发送通知的过程中,会抛出异常: