如何使用spring data jpa查询jsonb列?

Posted

技术标签:

【中文标题】如何使用spring data jpa查询jsonb列?【英文标题】:How do I use spring data jpa to query jsonb column? 【发布时间】:2017-10-09 13:45:39 【问题描述】:

我在获取这个针对 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 指定。

我在方法调用之前直接记录了参数,它们都是提供的。

我不确定为什么#&gt;&gt; 在日志中显示?。我需要逃避#&gt;&gt;吗?我需要为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 看起来它不适用于 eclipselink。我最终走了一条不同的路线。 【参考方案1】:

我发现 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 here 的答案。 希望对您有所帮助。

【讨论】:

你能创建一个号码的查询吗?见this 它工作得很好,但我想用对象(不仅仅是字符串)搜索,所以我将return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.&lt;String&gt;get("title"), builder.literal(locale)), titleToSearch); 更改为return builder.equal(builder.function("jsonb_extract_path_text", Object.class, root.&lt;String&gt;get("title"), builder.literal(locale)), titleToSearch); 并抛出异常:o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - ERROR: operator does not exist,我该如何处理? 【参考方案2】:

如果由于某种原因将运算符转换为问号,那么您应该尝试使用该函数。您可以在 psql 控制台中使用\doS+ #&gt;&gt; 找到相应的函数。它告诉我们调用的函数是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 操作? 嗯?他们使用 Hibernate 中的本机查询。您使用的是 EclipseLink 还是不太流行的实现之一?无论如何,在 Postgres 中,表达式 field IN (val1,val2) 被转换成等价的 field = ANY('val1,val1')。如果你将它作为一个数组传递给field = ANY ( ? ),并带有括号,它的工作原理是一样的。 NOT IN 的等效项是 field &lt;&gt; ALL( ? )。这当然会迫使您自己创建数组的字符串表示形式,确保正确转义任何字符串。 是的,我们正在使用 Eclipselink。我不确定为什么这比 Hibernate 更受欢迎。你会推荐什么?无论如何,一旦我更改为jsonb_extract_path_text,查询就会失败并出现新异常。当我搜索异常时,我发现了***.com/a/6279110/918608 和***.com/a/41564377/918608。看来我的列表中必须有恒定数量的值,并且我必须在我的本机查询中枚举它们。不是这样吗? 这很丑 :-),很高兴看到我们在使用 Spring Data 在 MongoDB 中使用 JSON 时看到的类似 @Document 注释。这实际上对通过 Spring Data Rest 快速构建 REST API 产生了重大影响。【参考方案3】:

也许这是一个老话题,但我在这里使用spring规范按字段搜索jsonb。

如果您想使用“LIKE”进行搜索,您需要使用以下代码创建 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" + " %"));任何帮助都会得到帮助,谢谢【参考方案4】:

您还可以使用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);

【讨论】:

如果您依赖于专有的 jsonb_extract_path_text 函数,为什么不再使用本机查询很重要?【参考方案5】:

我建议不要遵循这种方式,我更喜欢遵循通用的 CRUD 方式(对于 Spring Data Rest maven 插件,也可以使用 StrongLoop Loopback 的高级自动生成的 DAO 方法,但它目前只是实验性的)。 但是有了这个 JSON,现在该怎么办......我正在通过 @Document 注释在 Spring Data 中寻找类似于 MongoDB JSON 处理的东西,但是这还不可用。但是还有其他方法:-)

一般来说,它是关于实现你的 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;

在此处查看另一篇相关文章: Mapping PostgreSQL JSON column to Hibernate value type

完整的实现示例可在此处获得:

https://github.com/nzhong/spring-data-jpa-postgresql-json https://github.com/mariusneo/postgres-json-jpa

此处提供了类似但略有不同的示例: http://www.wisely.top/2017/06/27/spring-data-jpa-postgresql-jsonb/?d=1

【讨论】:

【参考方案6】:

分享我自己的例子,因为我努力分解提供的答案以满足我的特定需求。希望这对其他人有帮助。我的示例在 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)
    

【讨论】:

以上是关于如何使用spring data jpa查询jsonb列?的主要内容,如果未能解决你的问题,请参考以下文章

序列化表单为json对象,datagrid带额外参提交一次查询 后台用Spring data JPA 实现带条件的分页查询

如何使用spring data jpa查询jsonb列?

Spring Data JPA:查询ManyToMany,如何从映射类中获取数据?

如何使用 Spring Data JPA Repository 从 2 个表中查询?

Spring Data JPA:如何形成条件查询?

如何在 Spring Data (JPA) 派生查询中按多个属性排序?