介绍
我们从一个简单的hello world应用程序开始,然后介绍了如何设置数据库Schema的Flyway。今天我们准备学习一些将与数据库交互的代码。在我们开始编写代码之前,让我们先看一下历史。
Java 有一个很好的 JDBC API,可以帮助我们查询数据库。以它为基础,许多 ORM 工具应运而生,如Hibernate、Mybatis、Toplink 等等。ORM 弥合了 JDBC 和面向对象之间的差距,以及我们如何执行数据库操作并将它们映射到某些对象。看一下现在的 Java 的应用程序,JPA Hibernate 已经成为关系数据库事实上的选择。
Spring 的出现带来了更多的实用性,让开发人员的生活变得更加轻松。这篇文章不是 Hibernate 或 JPA 教程,而是一个简单的 Spring 教程,介绍如何使用 Spring 对 JPA 和 Hibernate 的支持。
一、依赖
像往常一样,我们有一个名为 spring-boot-starter-jpa 的启动器,添加依赖项如下:
代码语言:shell复制 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
依赖项包含核心依赖项和 JPA 依赖项如下:
提示:由于命名权限问题,以前称为 Java Persistence API 的 JPA 现在已重命名为 Jakarta Persistence API。
Spring data jpa 提供如下能力:
- 用于自动生成大多数样板查询模式的 Repository 接口。
- 支持标注驱动的事务机制。
- 轻松审计实体。
- 支持分页、筛选器等。
二、代码
我们已经添加了依赖项,现在开始编写代码,实体类定义如下所示:
代码语言:java复制@Getter
@Setter
@Entity
@Table(schema = "inv", name = "products")
public class Product{
@Id
@GeneratedValue
private UUID id;
private String name;
private Long stock;
private String manufacturer;
@CreatedDate
private OffsetDateTime createdOn;
}
它是一个简单的 JPA 实体,以 id 字段为标识符。
您需要做的就是定义一个存储库,如下所示 :
代码语言:shell复制@Repository
public interface ProductRepository extends JpaRepository<Product, UUID>{
}
Spring 将生成所有样板基础查询,例如 persists、findAll 等等。JpaRepository 还支持生成查询以通过实体的某些列进行查找,例如 id、name、stock、manufacturer、created on。我们所需要的只是一个名为 findBy 的方法<propertyName>。
下面是 ProductService,它将产品 DTO 作为输入并存储到数据库中。
代码语言:shell复制@Service
@RequiredArgsConstructor
public class ProductService{
private final ProductRepository productRepository;
public Product save(Product productDTO){
ProductEntity productEnity = new ProductEntity();
productEnity.setName(productDTO.getName());
productEnity.setManufacturer(productDTO.getManufacturer());
productEnity.setStock(productDTO.getStock());
productEnity.setCreatedOn(OffsetDateTime.now());
ProductEntity savedEntity = productRepository.save(productEnity);
return toProductDTO(savedEntity);
}
private Product toProductDTO(ProductEntity productEntity){
return Product.builder()
.id(productEntity.getId())
.createdOn(productEntity.getCreatedOn())
.manufacturer(productEntity.getManufacturer())
.name(productEntity.getName())
.stock(productEntity.getStock())
.build();
}
}
创建实体后,我们需要做的就是调用
代码语言:shell复制productRepository.save(productEnity) 。
我没有使用任何事务,因为 JpaRepository 本身在事务中工作。同样在这个简单示例中,我没有从实体中延迟加载任何属性,因此可以省略事务。
三、日志
我们可能想插卡Hibernate SQL生成的内容,我们可以使用以下属性:
代码语言:shell复制spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
输出如下:
代码语言:shell复制Hibernate:
insert
into
inv.products
(created_on, manufacturer, name, stock, id)
values
(?, ?, ?, ?, ?)
如果我们想查看 insert 语句中传递的实际输入,该怎么办?好吧,没有直接属性,但我们可以启用如下日志:
代码语言:shell复制logging:
level:
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
日志输出如下:
代码语言:shell复制2024-04-08 12:09:36.682 TRACE 40492 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [TIMESTAMP] - [2022-05-08T12:09:36.651572 02:00]
2025-04-08 12:09:36.683 TRACE 40492 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [test-mfg1]
2024-04-08 12:09:36.683 TRACE 40492 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [test-product5]
2024-04-08 12:09:36.683 TRACE 40492 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [BIGINT] - [100]
2024-04-08 12:09:36.683 TRACE 40492 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [5] as [OTHER] - [dd7d89fa-c201-4338-871f-a00fed464bd5]
让我们尝试查询所有产品 ,我们将再次从 Spring JPA 存储库中获取信息,代码如下:
代码语言:shell复制public List<Product> getAllProducts(){
return productRepository.findAll()
.stream()
.map(this::toProductDTO)
.collect(Collectors.toList());
}
四、分页
假如我们查询的产品可能数量很大,在这种情况下,我们需要分页支持。我们需要稍微修改一下我们的 ProductRepository 类 -
代码语言:shell复制@Repository
public interface ProductRepository extends PagingAndSortingRepository<ProductEntity, UUID>{
}
我们的方法返回分页数据,采用 Pageable 类型,修改后的方法如下所示:
代码语言:shell复制public Page<Product> getAllProducts(Pageable pageRequest){
return productRepository.findAll(pageRequest)
.map(this::toProductDTO);
}
请注意返回类型如何从 List<Product> 更改为<Product> Page ,页面类型包含总页数和总项目数等信息。
我们还可以在应用程序日志中验证 select 查询是否未使用 limit 和 offset,而不是执行 select all 。
代码语言:shell复制Hibernate:
select
productent0_.id as id1_0_,
productent0_.created_on as created_2_0_,
productent0_.manufacturer as manufact3_0_,
productent0_.name as name4_0_,
productent0_.stock as stock5_0_
from
inv.products productent0_ limit ? offset ?
五、审计
如果我们在 ProductService 中查看我们的保存方法,我们会将 createdOn 字段的值设置为当前日期时间,尽管演示上下文中这样做没有错,但有一种更好的方法来填充此字段,Spring data jpa 通过 AuditingEntityListener 提供审计功能。这提供了一堆在事件之前或之后填充字段的注释。
让我们尝试填充我们的 createdOn 字段。
- 1.我们首先需要将 @EntityListeners(AuditingEntityListener.class) 添加到我们的 ProductEntity 类中。
- 2.我们需要提供一个 DateTimeProvider 类型的 bean,它将负责提供当前时间。因为我们使用的是 OffsetDatetime,所以我们创建了一个如下所示的 bean,它给出了一个 OffsetDatetime。
@Bean
public DateTimeProvider dateTimeProvider(){
return ()-> Optional.of(OffsetDateTime.now());
}
- 3.最后,我们添加以下注解 @EnableJpaAuditing(dateTimeProviderRef = “dateTimeProvider”) 。就像时间戳一样,我们还可以添加一个 auditorAwareRef,它返回一个<T> AuditorAware 。让我们向 ProductEntity 添加一个新列
@CreatedBy
private String createdBy;
创建Bean如下:
代码语言:shell复制 @Bean
public AuditorAware<String> auditorAwareRef(){
return () -> Optional.of("test-user");
}
我们现在创建一个新产品,我们将看到 test-user 已在数据库中设置为 createdBy。
注意:添加常量 test-user 仅用于示例目的。获取真实用户名可能涉及从 ThreadLocal、SecurityContext、Auth Header 或适合您的上下文的任何其他内容获取它。
我们还有其他注释 LastModifiedBy 和 LastModifiedOn 来捕获修改审计。
六、更多特性
- @Query - 有时存储库方法也不足以满足我们的用例,可能需要一个更复杂的查询,在这种情况下,我们可以添加一个方法并使用@Query注解来指定我们的 sql 查询。如果我们设置 native=true,我们可以提供原生 SQL 查询,而不是 JPQL 查询。
- 自定义标准 - 我们也可以从 JpaSpecificationExecutor 继承,它提供了采用 Specification 类型的方法。我们可以利用 JPA 标准来构建更细致和复杂的查询。
小结
本节我们学习了Spring Data JPA,我们创建一个实体,并知道如何持久化它并查询它。Spring data jpa 是一个大模块,并不是所有内容都可以在一篇文章中涵盖,在以后的博客中,我们将看到spring-data-jpa的更多功能。
我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!