如何在原生JPA查询中将一个对象作为筛选条件?

人工智能 2026-07-10

我有一个原生查询(native query),它接收4个(可选)参数来进行过滤。4个参数对可读性来说有点多,因此我在考虑用一个对象来替代。期间遇到了两个不同的问题。

首先,我只是尝试把一个POJO作为过滤器:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class QuestionCategoryFilter {
    private String name;
    private Boolean active;
    private Integer orderNumber;
    private String answerTypeName;
}

但随后出现了如下错误:

org.springframework.orm.jpa.JpaSystemException: A problem occurred in the SQL executor : No JDBC mapping could be inferred for parameter - org.hibernate.sql.exec.internal.JdbcParameterImpl@6c173735

后来我偶然发现直接在Hibernate的类上按Ctrl点击就能找到一个解决方案,我的搜索结果指向了org.hibernate.type.QueryParameterJavaObjectType

扩展这个类确实解决了问题,我的查询可以正常执行,但后来在项目中添加Swagger-UI时,我遇到了一个问题:原本简单的4字段对象其实变得庞大无比:

{
  "jdbcType": {
    "integer": true,
    "float": true,
    "number": true,
    "binary": true,
    "castType": "STRING",
    "ddlTypeCode": 0,
    "duration": true,
    "nationalized": true,
    "string": true,
    "interval": true,
    "friendlyName": "string",
    "lobOrLong": true,
    "jdbcTypeCode": 0,
    "temporal": true,
    "stringLike": true,
    "lob": true,
    "defaultSqlTypeCode": 0,
    "decimal": true
  },
  "jdbcValueBinder": {},
  "jdbcValueExtractor": {},
  "jdbcLiteralFormatter": {},
  "name": "string",
  "active": true,
  "orderNumber": 0,
  "answerTypeName": "string",
  "mutable": true,
  "associationType": true,
  "componentType": true,
  "collectionType": true,
  "anyType": true,
  "entityType": true,
  "castType": "STRING",
  "registrationKeys": [
    "string"
  ],
  "javaTypeDescriptor": {
    "javaType": {
      "typeName": "string"
    },
    "typeName": "string",
    "defaultValue": {},
    "comparator": {},
    "mutabilityPlan": {
      "mutable": true
    },
    "longSqlLength": 0,
    "temporalType": true
  },
  "valueConverter": {
    "relationalJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "domainJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    }
  },
  "mappedType": {
    "mappedJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    }
  },
  "jdbcTypeCount": 0,
  "jdbcMapping": {
    "jdbcJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "jdbcType": {
      "integer": true,
      "float": true,
      "number": true,
      "binary": true,
      "castType": "STRING",
      "ddlTypeCode": 0,
      "duration": true,
      "nationalized": true,
      "string": true,
      "interval": true,
      "friendlyName": "string",
      "lobOrLong": true,
      "jdbcTypeCode": 0,
      "temporal": true,
      "stringLike": true,
      "lob": true,
      "defaultSqlTypeCode": 0,
      "decimal": true
    },
    "jdbcValueBinder": {},
    "castType": "STRING",
    "valueConverter": {
      "relationalJavaType": {
        "javaType": {
          "typeName": "string"
        },
        "typeName": "string",
        "defaultValue": {},
        "comparator": {},
        "mutabilityPlan": {
          "mutable": true
        },
        "longSqlLength": 0,
        "temporalType": true
      },
      "domainJavaType": {
        "javaType": {
          "typeName": "string"
        },
        "typeName": "string",
        "defaultValue": {},
        "comparator": {},
        "mutabilityPlan": {
          "mutable": true
        },
        "longSqlLength": 0,
        "temporalType": true
      }
    },
    "jdbcTypeCount": 0,
    "mappedJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "javaTypeDescriptor": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "jdbcLiteralFormatter": {},
    "singleJdbcMapping": "string",
    "jdbcValueExtractor": {}
  },
  "mappedJavaType": {
    "javaType": {
      "typeName": "string"
    },
    "typeName": "string",
    "defaultValue": {},
    "comparator": {},
    "mutabilityPlan": {
      "mutable": true
    },
    "longSqlLength": 0,
    "temporalType": true
  },
  "expressibleJavaType": {
    "javaType": {
      "typeName": "string"
    },
    "typeName": "string",
    "defaultValue": {},
    "comparator": {},
    "mutabilityPlan": {
      "mutable": true
    },
    "longSqlLength": 0,
    "temporalType": true
  },
  "singleJdbcMapping": {
    "jdbcJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "jdbcType": {
      "integer": true,
      "float": true,
      "number": true,
      "binary": true,
      "castType": "STRING",
      "ddlTypeCode": 0,
      "duration": true,
      "nationalized": true,
      "string": true,
      "interval": true,
      "friendlyName": "string",
      "lobOrLong": true,
      "jdbcTypeCode": 0,
      "temporal": true,
      "stringLike": true,
      "lob": true,
      "defaultSqlTypeCode": 0,
      "decimal": true
    },
    "jdbcValueBinder": {},
    "castType": "STRING",
    "valueConverter": {
      "relationalJavaType": {
        "javaType": {
          "typeName": "string"
        },
        "typeName": "string",
        "defaultValue": {},
        "comparator": {},
        "mutabilityPlan": {
          "mutable": true
        },
        "longSqlLength": 0,
        "temporalType": true
      },
      "domainJavaType": {
        "javaType": {
          "typeName": "string"
        },
        "typeName": "string",
        "defaultValue": {},
        "comparator": {},
        "mutabilityPlan": {
          "mutable": true
        },
        "longSqlLength": 0,
        "temporalType": true
      }
    },
    "jdbcTypeCount": 0,
    "mappedJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "javaTypeDescriptor": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "jdbcLiteralFormatter": {},
    "singleJdbcMapping": "string",
    "jdbcValueExtractor": {}
  },
  "persistenceType": "ENTITY",
  "sqmType": {
    "sqmType": "string",
    "tupleLength": 0,
    "typeName": "string",
    "expressibleJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    },
    "relationalJavaType": {
      "javaType": {
        "typeName": "string"
      },
      "typeName": "string",
      "defaultValue": {},
      "comparator": {},
      "mutabilityPlan": {
        "mutable": true
      },
      "longSqlLength": 0,
      "temporalType": true
    }
  },
  "tupleLength": 0,
  "typeName": "string",
  "relationalJavaType": {
    "javaType": {
      "typeName": "string"
    },
    "typeName": "string",
    "defaultValue": {},
    "comparator": {},
    "mutabilityPlan": {
      "mutable": true
    },
    "longSqlLength": 0,
    "temporalType": true
  },
  "jdbcJavaType": {
    "javaType": {
      "typeName": "string"
    },
    "typeName": "string",
    "defaultValue": {},
    "comparator": {},
    "mutabilityPlan": {
      "mutable": true
    },
    "longSqlLength": 0,
    "temporalType": true
  }
}

而且我不能添加 @JsonIgnore 或其他类似的方式来避免让它们暴露给swagger,因为除了这4个字段外,其余字段都来自我不拥有的类。

那么,针对我的问题,常见的解决方案是什么?

Should I forgo using an object within the query and unwrap the object when calling it?
Should I create a separate Filter object for the API that I then convert into the JPA-compatible filter object?
Or is there a way simpler JavaType for me to extend instead of org.hibernate.type.QueryParameterJavaObjectType?

编辑1:我也不知道为什么之前一直没把源代码贴上来,下面是相关部分:

    @Transactional(readOnly = true)
    @Query(nativeQuery = true, value = """
        SELECT qc.id, qc.name, qc.active, qc.order_number as orderNumber, qc.answer_type_id as answerTypeId,
            at.name as answerTypeName, at.max_value as 'maxValue',
            q.id as questionId, q.question, q.active AS questionActive, q.order_number AS questionOrderNumber,
            ai.num_answer as numAnswer,
            a.answer_time as answerTime, a.active AS answerActive,
            u.name as username, u.ldap_id as ldapId, u.active as userActive
        FROM QUESTION_CATEGORIES qc
            INNER JOIN ANSWER_TYPES at on at.id = qc.answer_type_id
            INNER JOIN QUESTIONS q ON q.category_id = qc.id
            LEFT JOIN ANSWER_ITEMS ai ON ai.question_id = q.id
            LEFT JOIN ANSWERS a ON a.id = ai.answer_id
            LEFT JOIN USERS u ON (u.id = a.user_id AND u.ldap_id = :username)
        WHERE q.active = true
            AND :filter IS NULL OR (
                (:#{#filter.getName()} IS NULL OR LOWER(qc.name) LIKE CONCAT('%', LOWER(:#{#filter.getName()}), '%'))
                AND (:#{#filter.getActive()} IS NULL OR qc.active = :#{#filter.getActive()})
                AND (:#{#filter.getOrderNumber()} IS NULL OR qc.order_number = :#{#filter.getOrderNumber()})
                AND (:#{#filter.getAnswerTypeName()} IS NULL OR at.name = :#{#filter.getAnswerTypeName()})
            )
        ORDER BY qc.order_number
    """)
    List<QuestionCategoryDAO> findAllResponsesByFilterAndUser(@Param("filter") QuestionCategoryFilter filter, @Param("username") String username);

这确实可用,但只是因为 public class QuestionCategoryFilter extends QueryParameterJavaObjectType。如果没有对 QueryParameterJavaObjectType 的扩展,我就会得到我之前提到的错误。

我现在发现我跟随的每个示例似乎只有一个参数,即过滤对象,而我却有对象和一个额外的参数。

最终我解决了我的问题,原因是 lombok 生成的构造函数参数数量荒诞,而 springdoc 随后把它们直接暴露到了 swagger-ui 界面。

但为了将来参考,我想知道正式的解决方案应该是什么(不是靠反向工程Hibernate并靠运气得到一个扩展),或者说,为查询参数再包装一个对象并附带额外参数,是否只是坏实践并且有意不被支持。

解决方案

我将基于一个类似的问答来给出我的回答,该问答在评论中被提及(来自Andrew S)。这条问答专门指出:https://stackoverflow.com/a/76779816/1688441,它建议在一个非常相似的问题上使用SpEL支持。

所以,如果我理解正确,你只是想传入一个对象,但用它来包裹4个参数。

大致像这样?

    @Query("select * from questionCategory qc
    where (:#{#qcf.name} is null or qc.name = :#{#qcf.name})
      and (:#{#qcf.active} is null or qc.active = :#{#qcf.active})
      and (:#{#qcf.orderNumber} is null or qc.orderNumber = :#{#qcf.orderNumber})
      and (:#{#qcf.answerTypeName} is null or qc.answerTypeName = :#{#qcf.answerTypeName})
    ")
    List<QuestionCategory> findByFilter(@Param("qcf") QuestionCategoryFilter f);

我对你的对象有一些假设……(例如questionCategory与 questionCategoryFilter的区别)

免责声明/参考

此回答基于:

它也引用:

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章