教程

JPA 中实体的继承与组合

1、简介 继承(Inheritance)和组合(Composition)是面向对象编程(OOP)中的两个基本概念,我们也可以在 JPA 中利用它们进行数据建模。在 JPA 中,继承和组合都是对实体间关系进行建模的技术,但它们代表的是不同类型的关系。本文将带你了解这两种方法及其影响。 2、JPA 中的继承 继承是一种 “is-a” 关系,即子类继承超类的属性和行为。这允许子类从超类继承属性和方法,从而促进了代码的重用。JPA 提供了几种策略来模拟实体与其对应的数据库表之间的继承关系。 2.1、单表继承(STI) 单表继承(Single Table Inheritance,STI)将所有子类映射到单个数据库表中。通过利用 区分列 来区分子类实例,这简化了 Schema 管理和查询执行过程。 首先,使用 @Entity 注解将 Employee 实体类定义为超类。接下来,将继承策略设置为 InheritanceType.SINGLE_TABLE,这样所有子类都会映射到同一个数据库表*。 然后,使用 @DiscriminatorColumn 注解来指定 Employee 类中的 区分列。该列用于区分单个表中不同类型的实体。 示例如下,使用 name = "employee_type" 将列名称指定为 employee_type,并使用 discriminatorType = DiscriminatorType.STRING 表示列包含字符串值: @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "employee_type", discriminatorType = DiscriminatorType.STRING) public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Get / Set 方法省略 } 对于每个子类,使用 @DiscriminatorValue 注解来指定与该子类相对应的 区别列 的值。在本例中,我们使用 manager 和 developer 分别作为 Manager 和 Developer 子类的 区别值:

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 Boot 设置日期(Date/LocalDate/LocalDateTime)的 JSON 格式化

1、概览 本文将带你了解如何在 Spring Boot 应用中格式化 JSON Date 字段。 Spring Boot 默认使用 Jackson 作为 JSON 的序列化/反序列化框架。 2、在 Date 字段上使用 @JsonFormat 2.1、设置格式化 我们可以使用 @JsonFormat 注解来格式化特定字段: public class Contact { // 其他字段 @JsonFormat(pattern="yyyy-MM-dd") private LocalDate birthday; @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private LocalDateTime lastUpdate; // getter/setter 方法省略 } 如上,用了 Java 8 的日期类型,它在处理时间类型时非常方便。birthday 字段只显示日期,而 lastUpdate 字段则包括了时间。 当然,如果需要使用 java.util.Date 等传统类型,也可以同样的方式使用注解: public class ContactWithJavaUtilDate { // 其他字段 @JsonFormat(pattern="yyyy-MM-dd") private Date birthday; @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Date lastUpdate; // getter/setter 方法省略 } 最后,来看看使用 @JsonFormat 格式化日期字段后的输出:

配置 Mybatis 的 SQL 查询日志

1、概览 MyBatis 是 Java 界流行的持久化框架,它通过将 SQL 查询映射到 Java 方法来简化数据库操作。 在使用 MyBatis 开发应用时,查看正在执行的 SQL 查询通常对调试很有用,本文将带你了解如何在 MyBatis 中将 SQL 查询日志输出到控制台。 2、支持的日志实现 MyBatis 是一个灵活的框架,可以与各种日志框架集成,包括 SLF4J、Apache Commons Logging、Log4j 2 和 JDK Logging。本文主要关注 Stdout (标准输出,即控制台)日志和 SLF4J。 Stdout 日志在本地功能开发过程中非常有用,它提供了一种简单的调试方法。而 SLF4J 更适合生产应用,它提供了更高级的抽象,可与其他的日志框架无缝集成。 3、在 MyBatis 中配置 Stdout 日志 使用 stdout 记录 MyBatis SQL,可以直接在控制台上查看执行的 SQL 语句。这种方法在开发和调试过程中非常方便。 要启用 MyBatis SQL 的 stdout 日志,需要在应用的 mybatis-config 文件中添加日志设置: <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration> 将 logImpl 属性配置为 STDOUT_LOGGING 后,MyBatis 将在执行 SQL 查询时输出原始 SQL 语句、查询参数和查询结果。输出通常包括执行的 SQL、绑定的参数和返回的结果集等详细信息:

Spring AI - 结构化输出

科学处理事物的片段和碎片,并假设存在连续性,而艺术则只关注事物的连续性,假设存在片段和碎片。- 罗伯特·M·皮尔西格 LLM(大型语言模型)生成结构化输出的能力对于依赖于可靠解析输出值的下游应用非常重要。开发人员希望将 AI 模型的结果快速转化为数据类型,如 JSON、XML 或 Java 类,以便传递给应用中的其他函数和方法。 Spring AI Structured Output Converter(结构化输出转换器)有助于将 LLM 输出转换为结构化格式。如下图所示,这种方法围绕 LLM 文本补全端点进行操作: 使用通用的补全 API 从大型语言模型(LLM)生成结构化输出需要对输入和输出进行仔细处理。结构化输出转换器在 LLM 调用之前和之后发挥着关键作用,确保实现所需的输出结构。 在进行 LLM 调用之前,转换器会将格式指令附加到提示中,为模型提供明确的指导,以生成所需的输出结构。这些指令充当蓝图,使模型的响应符合指定的格式。 在 LLM 调用之后,转换器会获取模型的输出文本,并将其转换为结构化类型的实例。转换过程包括解析原始文本输出,并将其映射到相应的结构化数据表示,如 JSON、XML 或特定领域(Domain)的数据结构。 注意,AI 模型不能保证按要求返回结构化输出。它可能不理解提示,也可能无法按要求生成结构化输出。 TIP: 如果你不想深入了解 API 的细节,可以过下一段,直接看 “使用转换器 ”部分。 1、结构化输出 API StructuredOutputConverter 接口定义如下: public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider { } 它以目标结构化类型 T 为参数,结合了 Spring Converter<String, T> 接口和 FormatProvider 接口: public interface FormatProvider { String getFormat(); } 下图说明了通过结构化输出 API 组件的数据流程。

解决 Spring Boot H2 JdbcSQLSyntaxErrorException “Table not found”

1、简介 H2 是一个简单的轻量级内存数据库,Spring Boot 可以自动对其进行配置,使开发人员可以轻松测试数据访问逻辑。 通常情况下,org.h2.jdbc.JdbcSQLSyntaxErrorException 是用于表示与 SQL 语法相关的错误的异常。“Table not found” 表示 H2 无法找到指定的表。 本文将带你了解 H2 抛出 JdbcSQLSyntaxErrorException 异常的原因以及解决办法。 2、示例 既然知道了异常背后的根本原因,来看看如何重现异常。 2.1、H2 配置 Spring Boot 会配置应用使用用户名 sa 和空密码连接到可嵌入的数据库 H2。将这些属性添加到 application.properties 文件中: spring.datasource.url=jdbc:h2:mem:mydb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= 现在,假设有一个名为 person 的表。在此,使用一个基本的 SQL 脚本为数据库添加数据。默认情况下,Spring Boot 会加载 data.sql 文件: INSERT INTO "person" VALUES (1, 'Abderrahim', 'Azhrioun'); INSERT INTO "person" VALUES (2, 'David', 'Smith'); INSERT INTO "person" VALUES (3, 'Jean', 'Anderson'); 2.2、对象关系映射 接下来,使用 JPA 注解将表 person 映射到一个实体。

一种极简单的 Spring Boot 单元测试方法

本文主要介绍了一种单元测试方法,力求零基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实提高交付代码的质量。极简体现在除了 POM 依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的 Spring Boot 项目。 1、POM依赖 Springboot版本: 2.6.6 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.12.4</version> </dependency> 2、单元测试类示例 主要有两种。 第一种,偏集成测试 需要启动项目,需要连接数据库、RPC 注册中心等。 主要注解:@SpringBootTest + @RunWith(SpringRunner.class) + @Transactional + @Resource + @SpyBean + @Test @SpringBootTest + @RunWith(SpringRunner.class) 启动了一套 Spring Boot 的测试环境; @Transactional 对于一些修改数据库的操作,会执行回滚,能测试执行 sql,但是又不会真正的修改测试库的数据; @Resource 主要引入被测试的类; @SpyBean Spring Boot 环境下 mock 依赖的 Bean,可以搭配 Mockito.doAnswer(...).when(xxServiceImpl).xxMethod(any()) Mock 特定方法的返回值; @Test 标识一个测试方法; TIP:对于打桩有这几个注解 @Mock @Spy @MockBean @SpyBean,每一个都有其对应的搭配,简单说 @Mock 和 @Spy 要搭配 @InjectMocks 去使用,@MockBean 和 @SpyBean 搭配 @SpringBootTest + @RunWith(SpringRunner.

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 WebClient 中的 exchange() 和 retrieve() 方法

1、概览 WebClient 是一个简化 HTTP 请求执行过程的接口。与 RestTemplate 不同,它是一个响应式非阻塞客户端,可以消费和操作 HTTP 响应。虽然它被设计为非阻塞型,但也可用于阻塞型场景。 本文将带你了解 WebClient 接口中的关键方法,包括 retrieve()、exchangeToMono() 和 exchangeToFlux(),以及它们之间的差异。 2、示例项目设置 首先,创建一个 Spring Boot 应用,在 pom.xml 中添加 spring-boot-starter-webflux 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>3.2.4</version> </dependency> 该依赖提供了 WebClient 接口,用于执行 HTTP 请求。 另外,来看看 https://jsonplaceholder.typicode.com/users/1 请求的 GET 响应示例: { "id": 1, "name": "Leanne Graham", // ... } 创建一个名为 User 的 POJO 类: class User { private int id; private String name; // 构造函数、Getter、Setter 方法省略 } 来自 JSONPlaceholder API 的 JSON 响应将被反序列化并映射到 User 类的实例。

在 Spring Boot 应用中设置默认时区(Timezone)

1、概览 有时,我们希望能够指定应用使用的时区。我们可以通过几种不同的方法来实现这一目标。一种方法是在执行应用时使用 JVM 参数。另一种方法是在启动生命周期的不同阶段以编程式在代码中进行更改。 本文将带你了解在 Spring Boot 应用中设置默认时区的几种方法。 2、主要概念 TimeZone 的默认值基于运行 JVM 的机器的操作系统。我们可以: 通过使用 user.timezone 参数传递 JVM 参数,可以根据运行任务或 JAR 的不同情况,以不同的方式传递参数。 在程序中使用 Bean 生命周期配置选项(在创建 Bean 时/创建 Bean 前),甚至在类内执行过程中使用这些选项。 在 Spring Boot 应用中设置默认 TimeZone 会影响不同的组件,如日志的时间戳、调度程序(Scheduler)、JPA/Hibernate 时间戳等。这意味着我们选择在何处执行取决于何时需要它生效。例如,是希望在创建某个 Bean 时生效,还是在初始化 WebApplicationContext 后生效? 精确地确定何时设置该值非常重要,因为这可能会导致不必要的应用行为。例如,警报服务可能会在时区更改生效前设置警报,从而导致警报在错误的时间启动。 在决定采用哪种方案之前,另一个需要考虑的因素是可测试性。使用 JVM 参数是比较简单的选择,但测试起来可能比较麻烦,也更容易出现错误。我们无法保证单元测试能以与生产部署相同的 JVM 参数运行。 3、设置 bootRun 任务的默认时区 如果使用 bootRun 任务运行应用,我们可以在命令行中使用 JVM 参数传递默认 TimeZone。 在这种情况下,我们设置的值从一开始执行就可用: mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Duser.timezone=Europe/Athens" 4、在执行 JAR 时设置默认时区 与运行 bootRun 任务类似,我们可以在执行 JAR 文件时在命令行中传递默认的 TimeZone 值。 同样,我们设置的值在执行之初就可用: java -Duser.timezone=Europe/Athens -jar spring-core-4-0.