【SpringBoot】微服务数据持久化方案(SpringBootJPA+Hiberate)

2024-04-24 15:29:49 浏览数 (1)

介绍

我们从一个简单的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。
代码语言:shell复制
 @Bean
    public DateTimeProvider dateTimeProvider(){
        return ()-> Optional.of(OffsetDateTime.now());
    }
  • 3.最后,我们添加以下注解 @EnableJpaAuditing(dateTimeProviderRef = “dateTimeProvider”) 。就像时间戳一样,我们还可以添加一个 auditorAwareRef,它返回一个<T> AuditorAware 。让我们向 ProductEntity 添加一个新列
代码语言:shell复制
    @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腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞