如何使用Spring Data JPA查询JSONB列?

24
我在与一个Postgres 9.4实例进行本地查询时遇到了问题。
我的存储库有一个方法:
 @Query(value = "SELECT t.* " +
            "FROM my_table t " +
            "WHERE t.field_1 = ?1 " +
            "AND t.field_2 = 1 " +
            "AND t.field_3 IN ?2 " +
            "AND t.jsonb_field #>> '{key,subkey}' = ?3",
            nativeQuery = true)
    List<Entity> getEntities(String field1Value,
                                   Collection<Integer> field3Values,
                                   String jsonbFieldValue);

但是日志显示了这个:
SELECT t.* FROM my_table t 
WHERE t.field_1 = ?1 
  AND t.field_2 = 1 
  AND t.field_3 IN ?2 
  AND t.jsonb_field ? '{key,subkey}' = ?3

我遇到了这个异常:
内部异常:org.postgresql.util.PSQLException:未为参数2指定值。
我在方法调用之前直接记录了参数,它们都已经提供了。
我不确定为什么日志中的“#>>”显示为“?”。我需要转义“#>>”吗?我需要为“IN”格式化集合吗?我需要转义JSON路径吗?
当我直接对数据库执行查询时,它是有效的。例如:
SELECT *
FROM my_table t
WHERE t.field_1 = 'xxxx'
  AND t.field_2 = 1
  AND t.field_3 IN (13)
  AND t.jsonb_field #>> '{key,subkey}' = 'value'

你能解决这个问题吗?如果是从答案中找到的,你能选择一个吗?如果不是,你能把你使用的解决方案发布为答案吗? - JoeG
@JoeG 看起来它不适用于eclipselink。我最终选择了另一种方法。 - Josh C.
7个回答

26

我觉得Spring Data的Specification API非常有用。
假设我们有一个名为Product的实体和一个名为title的属性,类型为JSON(B)。
我假设这个属性包含了以不同语言表示的产品标题。例如:{"EN":"Multicolor LED light", "EL":"Πολύχρωμο LED φώς"}.
下面的源代码根据传递的标题和语言环境参数查找一个(或多个,如果它不是唯一的字段)产品。

@Repository
public interface ProductRepository extends JpaRepository<Product, Integer>, JpaSpecificationExecutor<Product> {
}


public class ProductSpecification implements Specification<Product> {

    private String locale;
    private String titleToSearch;

    public ProductSpecification(String locale, String titleToSearch) {
        this.locale = locale;
        this.titleToSearch = titleToSearch;
    }

    @Override
    public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.<String>get("title"), builder.literal(this.locale)), this.titleToSearch);
    }
}


@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public List<Product> findByTitle(String locale, String titleToSearch) {
        ProductSpecification cs = new ProductSpecification(locale, titleToSearch);
        return productRepository.find(cs);
        // Or using lambda expression - without the need of ProductSpecification class.
//      return productRepository.find((Root<ProductCategory> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
//          return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.<String>get("title"), builder.literal(locale)), titleToSearch);
//      });
    }
}

在这里,您可以找到有关如何使用Spring Data的另一个答案:此处
希望对您有所帮助。


你能为一个数字创建查询吗?请参考这里 - Morteza Malvandi
它完美地工作,但我想使用Object进行搜索(不仅仅是String),所以我将return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.<String>get("title"), builder.literal(locale)), titleToSearch);更改为return builder.equal(builder.function("jsonb_extract_path_text", Object.class, root.<String>get("title"), builder.literal(locale)), titleToSearch);,但它抛出异常:o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - ERROR: operator does not exist,我该如何处理? - ktcl

5
您也可以使用FUNC JPQL关键字来调用自定义函数,而无需使用原生查询。
像这样的东西,
@Query(value = "SELECT t FROM my_table t "
        + "WHERE t.field_1=:field_1 AND t.field_2=1 AND t.field_3 IN :field_3 "
        + "AND FUNC('jsonb_extract_path_text', 'key', 'subkey')=:value")
List<Entity> getEntities(@Param("field_1") String field_1, @Param("field_3") Collection<Integer> field_3, @Param("value") String value);

3
如жһњдҢ дңқиµ–дғҺдё“жњ‰зљ„jsonb_extract_path_textе‡Ңж•°пәЊй‚Әд№€дёҚе†ҚдҢүз”Ёжњ¬ењ°жџӨиҮұдәљжњ‰д»Ђд№€еҢ±е“Қе‘ұпәџ - mvd
因为编译器至少能够识别查询的某些部分,而不是完全没有编译时检查,所以我认为这是有意义的。 - Klesun

5

如果操作符因某种原因被转换为问号,那么您应该尝试使用函数。您可以在psql控制台中使用\doS+ #>>查找相应的函数。它告诉我们调用的函数是jsonb_extract_path_text。这将使您的查询变为:

@Query(value = "SELECT t.* " +
        "FROM my_table t " +
        "WHERE t.field_1 = ?1 " +
        "AND t.field_2 = 1 " +
        "AND t.field_3 IN ?2 " +
        "AND jsonb_extract_path_text(t.jsonb_field, '{key,subkey}') = ?3",
        nativeQuery = true)

我想我已经接近了答案。显然,在本地查询中,不允许在列表参数上使用IN子句。是否可能使用JPA / JPQL进行jsonb操作而无需使用本地查询? - Josh C.
什么?他们在Hibernate中使用本地查询。您是使用EclipseLink还是其中一个不太流行的实现?无论如何,在Postgres中,表达式field IN(val1,val2)被转换为其等效的field = ANY('{val1,val1}')。如果您将其作为数组传递给带有括号的field = ANY(?),它仍然可以正常工作。 NOT IN的等效物是field <> ALL(?)。这当然迫使您自己创建数组的字符串表示形式,并确保正确转义任何字符串。 - coladict
是的,我们正在使用Eclipselink。我不确定为什么它比Hibernate更受欢迎。你有什么建议吗?无论如何,一旦我改用jsonb_extract_path_text,查询就会失败并出现新的异常。当我搜索这个异常时,我发现了https://dev59.com/TW015IYBdhLWcg3w_Q0_#6279110和https://dev59.com/JJ7ha4cB1Zd3GeqPdg9d#41564377。看起来我必须在我的列表中有一个恒定的值数量,并且我必须在我的本地查询中枚举它们。不是这种情况吗? - Josh C.
这很丑陋 :-),如果我们在使用Spring Data处理JSON时能够看到类似的@Document注释,那将是很好的。实际上,这对于通过Spring Data Rest快速构建REST API有着非常重要的影响。 - kensai

3
也许这是一个老话题,但我在这里使用Spring规范来搜索jsonb字段。如果你想使用“LIKE”进行搜索,你需要使用以下代码创建像异或逻辑:
final Predicate likeSearch = cb.disjunction();

在此之后,我们假设您的对象中有一个名为地址的jsonb字段,而地址包含5个字段。要在所有这些字段中进行搜索,您需要为所有字段添加“LIKE”表达式:

for (String field : ADDRESS_SEARCH_FIELDS) {
                likeSearch.getExpressions().add(cb.like(cb.lower(cb.function("json_extract_path_text", String.class,
                        root.get("address"), cb.literal(field))), %searchKey%));
            }

其中cb是相同的criteriaBuilder。 %searchKey%是您要在地址字段中搜索的内容。

希望这可以帮到您。


我有一个Json对象数组,每个json对象都有一个“id”键,我想根据每个id进行过滤,我尝试了如下代码但未得到结果:likeSearch.getExpressions().add(cb.like(cb.lower(cb.function("json_extract_path_text", String.class,root.get("testList"), cb.literal("*.id"))),"%" + "1234abc" + "%"));如能提供帮助将不胜感激,谢谢。 - jagga

2

我想分享我的经验,因为我在分解提供的答案以满足我的特定需求时遇到了困难。希望这能帮助其他人。我的例子是用Groovy编写的,并与Postgres SQL数据库集成。这是一个简单的示例,演示如何在名为“name”的字段上搜索JSON列并使用分页。

JSON支持类

@TypeDefs([@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)])
@MappedSuperclass
class JSONSupport {}

实体类:

@Entity
@Table(name = "my_table")
class MyEntity extends JSONSupport {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long pk

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    String jsonData
}

规范类
class NameSpecification implements Specification<MyEntity> {

    private final String name

    PhoneNumberSpecification(String name) {
        this.name = name
    }

    @Override
    Predicate toPredicate(Root<ContactEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        return builder.equals(
                   builder.function(
                       "jsonb_extract_path_text",
                       String.class,
                       root.<String>get("jsonData"),
                       builder.literal("name")
                   ),
                   this.name
              )
    }
}

该存储库。
interface MyEntityRepository extends PagingAndSortingRepository<MyEntity, Long>, JpaSpecificationExecutor<MyEntity> {}

使用方法

@Service
class MyEntityService {
    
    private final MyEntityRepository repo
    
    MyEntityService(MyEntityRepository repo) {
         this.repo = repo
    }    

    Page<MyEntity> getEntitiesByNameAndPage(String name, Integer page, Integer pageSize) {
        PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("pk"))

        NameSpecification spec = new NameSpecification(name)
        return repo.findAll(spec, pageRequest)
    }
}

1
我建议不要按照这种方式进行,我更喜欢按照通用的CRUD方式进行(也在像StrongLoop Loopback一样开发高级自动生成DAO方法,对于Spring Data Rest Maven插件来说,但目前仅处于实验阶段)。 但是有了这个JSON,现在该怎么办呢...我正在寻找类似于MongoDB JSON处理的Spring Data @Document注释,但目前还没有可用的。但还有其他方法 :-)
一般来说,它是关于实现您的JSON用户类型(UserType接口):
public class YourJSONBType implements UserType {

最后,您需要增强JPA类并指定实现的用户类型:
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@TypeDef(name = "JsonbType", typeClass = YourJSONBType.class)
public class Person {
    @Id
    @GeneratedValue
    private Long id;

    @Column(columnDefinition = "jsonb")
    @Type(type = "JsonbType")
    private Map<String,Object> info;
}

在这里查看其他相关文章: 将PostgreSQL JSON列映射到Hibernate值类型

完整的实现示例在此处提供:

这里还有一个类似但略有不同的例子: http://www.wisely.top/2017/06/27/spring-data-jpa-postgresql-jsonb/?d=1


0
在Postgres数据库中创建一张表。
CREATE TABLE shared.my_data (
id serial PRIMARY KEY,
my_config jsonb
);

将数据插入表中

INSERT into shared.my_data (id, my_config) VALUES( 1,
'{"useTime": true,
"manualUnassign": false,
"require":true,
"blockTime":10,
"additionalHours":1,
"availablegroups":[10,20,30]
}')

检查表中的数据:

select * from shared.tenant_data 

Spring Boot Java项目 Java版本:11 Spring版本:2.7.1

POM.xml文件中的Maven依赖。 对于Postgres JOSNB,我们需要特定的

vladmihalcea依赖版本2.14.0

    <dependency>
        <groupId>com.vladmihalcea</groupId>
        <artifactId>hibernate-types-52</artifactId>
        <version>2.14.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
        <version>2.7.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>2.7.1</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.1</version>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

JSON对象类

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

public class MyConfig {
@JsonProperty("useTime")
private boolean useTime;
@JsonProperty("manualUnassign")
private boolean manualUnassign;
@JsonProperty("require")
private boolean require;
@JsonProperty("additionalHours")
private int additionalHours;
@JsonProperty("blockTime")
private int blockTime;
@JsonProperty("availableGroup")
private List<Integer> availableGroup;
}

[实体]根对象用于封装表行中的列

import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;

@Data
@Entity
@Table(name = "my_data", schema = "shared")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class MyData {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;

@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private MyConfig myConfig;
}

仓储层

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MyDataRepo extends JpaRepository<MyData, Long> {
}

服务层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class MyDataService {

@Autowired
private MyDataRepo myDataRepo;

public List<MyData> getAllMyspecificData(){
    List<MyData> allMyData = myDataRepo.findAll();
    return allMyData;
}
 }

REST 端点

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping(path = "/my")
public class MyResouce {
@Autowired
MyDataService myDataService;

@GetMapping("/data")
public ResponseEntity<Object> getAllMyData() {
     List<MyData> myDataList = 
     myDataService.getAllMyspecificData();

    return new ResponseEntity<>(myDataList, HttpStatus.OK);
}
}

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接