1. 简介
Spring Data JPA提供了一个强大的抽象层,用于与关系型数据库交互。但是,传统的关系表可能不适合存储复杂的半结构化数据(如产品详细信息或用户偏好),这就是JSONB数据类型的用武之地。在本教程中,我们将探索使用Spring Data JPA查询JSONB列的各种方法。
2. JSONB列
JSONB(数据库的JavaScript对象表示法)是一种专门用于在PostgreSQL等关系型数据库中存储JSON数据的数据类型,它允许我们在单个列中使用键值对和嵌套对象来表示复杂的数据结构。与JPA提供程序(例如Hibernate)结合使用,Spring Data JPA允许我们将这些JSONB列映射到实体类中的属性。
3. 映射JSONB列
我们可以使用带有columnDefinition属性的@Column注解来明确定义实体类中的列类型:
@Column(columnDefinition = "jsonb")
private String attributes;
这种方法主要与PostgreSQL相关,它本身支持jsonb数据类型。通过将此注解添加到实体类中的相应属性,我们可以向数据库提供有关所需列类型的提示。Spring Data JPA通常会 根据数据库列定义自动检测jsonb数据类型,因此在许多情况下此注解是可选的。
4. 设置项目依赖和测试数据
我们将创建一个基本的Spring Boot项目,其中包含测试JSONB查询所需的依赖和测试数据。
4.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>
4.2 实体类
接下来,让我们创建一个名为Product的Java类来代表我们的实体:
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(columnDefinition = "jsonb")
private String attributes;
// Getters and Setters
}
此类使用id、name和properties字段定义Product实体。attributes字段是一个字符串,用于保存产品详细信息的序列化JSON数据。我们使用@Column(columnDefinition = “jsonb”)提示数据库以JSONB类型创建属性列。
定义Product实体类后,Hibernate将在应用程序启动并初始化数据库模式时生成以下SQL:
CREATE TABLE Product (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(255),
attributes JSONB
);
4.3 准备测试数据
下面是在运行测试用例之前我们用来准备数据库的SQL脚本,我们可以将此脚本保存在.sql文件中,并将其放在项目的src/test/resources目录中:
DELETE FROM product;
INSERT INTO product (name, attributes)
VALUES ('Laptop', '{"color": "red", "size": "15 inch"}');
INSERT INTO product (name, attributes)
VALUES ('Phone', '{"color": "blue", "size": "6 inch"}');
INSERT INTO product (name, attributes)
VALUES ('Headphones', '{"brand": "Sony", "details": {"category": "electronics", "model": "WH-1000XM4"}}');
INSERT INTO product (name, attributes)
VALUES ('Laptop', '{"brand": "Dell", "details": {"category": "computers", "model": "XPS 13"}}');
然后,我们在测试类中使用@Sql注解并将executionPhase属性设置为BEFORE_TEST_METHOD,在每次测试方法执行之前将测试数据插入数据库:
@Sql(scripts = "/testdata.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
5. 使用@Query进行原生查询
在本节中,我们将利用原生SQL查询和PostgreSQL JSONB运算符来根据JSONB列中的值过滤和检索数据,我们使用@Query注解在Spring Data JPA Repository接口中定义这些原生查询。
5.1 使用JSONB运算符进行查询
@Query注解允许我们使用原生SQL语法在Repository接口中定义自定义查询,这种方法在使用JSONB列时特别有用,因为它使我们能够直接在查询中使用PostgreSQL的原生JSONB运算符。
让我们使用JSONB运算符编写一个查询来查找具有特定颜色值的所有产品:
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query(value = "SELECT * FROM product WHERE attributes ->> ?1 = ?2", nativeQuery = true)
List<Product> findByAttribute(String key, String value);
}
findByAttribute()方法接收两个参数,表示JSON属性的键(例如“color”),并根据表示该属性所需值的值进行筛选。@Query注解定义了一个原生SQL查询,该查询利用-»运算符来访问属性JSONB列中的键。
然后,查询中的占位符?1和?2将被替换为执行期间作为方法参数提供的键和值的实际名称。接下来,让我们创建一个测试用例来验证findByAttribute()方法的功能:
List<Product> redProducts = productRepository.findByAttribute("color", "red");
assertEquals(1, redProducts.size());
assertEquals("Laptop", redProducts.get(0).getName());
5.2 使用JSONB运算符进行嵌套查询
我们利用PostgreSQL的->和-»运算符来查询嵌套的JSONB数据:
- ->运算符用于访问JSONB对象内的特定键
- -»运算符用于以文本形式访问JSONB对象中特定键对应的值
让我们编写另一个查询来处理嵌套键:
@Query(value = "SELECT * FROM product WHERE attributes -> ?1 ->> ?2 = ?3", nativeQuery = true)
List<Product> findByNestedAttribute(String key1, String key2, String value);
此查询允许我们搜索特定嵌套属性与给定值匹配的实体。例如,如果我们想查找“details.category”为“electronics”的产品,我们将使用“details”、“category”和“electronics”作为参数调用此方法:
List<Product> electronicProducts = productRepository.findByNestedAttribute("details", "category", "electronics");
assertEquals(1, electronicProducts.size());
assertEquals("Headphones", electronicProducts.get(0).getName());
5.3 使用jsonb_extract_path_text函数进行查询
我们还可以利用原生SQL查询中的jsonb_extract_path_text函数从JSONB数据中提取特定值,jsonb_extract_path_text是一个PostgreSQL函数,用于根据给定路径从JSONB列中提取特定值。
jsonb_extract_path_text函数以文本形式返回提取的值,如果JSONB结构中不存在指定的路径,则该函数返回NULL。
让我们看一个使用jsonb_extract_path_text的例子:
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query(value = "SELECT * FROM product WHERE jsonb_extract_path_text(attributes, ?1) = ?2", nativeQuery = true)
List<Product> findByJsonPath(String path, String value);
}
findByJsonPath()方法接收两个参数,第一个参数是路径字符串,指示要提取的JSONB对象中的特定键;第二个参数表示提取属性的预期值:
List<Product> redProducts = productRepository.findByJsonPath("color", "red");
assertEquals(1, redProducts.size());
assertEquals("Laptop", redProducts.get(0).getName());
5.4 使用jsonb_extract_path_text函数进行嵌套查询
让我们看看如何使用jsonb_extract_path_text函数调整查询以处理嵌套键:
@Query(value = "SELECT * FROM product WHERE jsonb_extract_path_text(attributes, ?1, ?2) = ?3", nativeQuery = true)
List<Product> findByNestedJsonPath(String key1, String key2, String value);
我们提供key1和key2作为路径元素来遍历嵌套的JSONB结构:
List<Product> electronicProducts = productService.findByNestedJsonPath("details", "category", "electronics");
assertEquals(1, electronicProducts.size());
assertEquals("Headphones", electronicProducts.get(0).getName());
在此示例中,findByNestedJsonPath(“details”, “category”, “electronics”)将定位嵌套“details.category”值为“electronics”的产品。
6. 使用自定义JPA Specification方法
JPA Specification是封装用于过滤数据的标准的接口,它们定义需要根据特定条件检索哪些数据。此接口由Spring Data JPA提供,允许Repository实现接收Specification<T>实例作为参数的自定义查询方法。
我们可以创建一个实现Specification<T>接口的类,此类定义toPredicate()方法,该方法利用CriteriaBuilder根据提供的条件(用于过滤的键和值)构造实际的查询谓词:
public class ProductSpecification implements Specification<Product> {
private final String key;
private final String value;
public ProductSpecification(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(
cb.function("jsonb_extract_path_text", String.class, root.get("attributes"), cb.literal(key)),
value
);
}
}
toPredicate()方法定义如何使用作为参数提供的CriteriaBuilder构建过滤谓词,在此示例中,我们假设键位于JSONB数据的顶层。
要使用此自定义Specification,ProductRepository需要扩展JpaSpecificationExecutor<Product>:
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}
以下是根据JSONB列中的属性过滤产品的自定义Specification的基本示例:
ProductSpecification spec = new ProductSpecification("color", "red");
Page<Product> redProducts = productRepository.findAll(spec, Pageable.unpaged());
assertEquals(1, redProducts.getContent().size());
assertEquals("Laptop", redProducts.getContent().get(0).getName());
7. 总结
在本文中,我们探讨了使用Spring Data JPA查询JSONB列的各种方法。对于基本过滤条件,原生SQL查询可能提供直接的解决方案。但是,在处理复杂的过滤逻辑或可重用性需求时,JPA Specification提供了一种引人注目的替代方案。
Post Directory
