PostgreSQL, Spring Data JPA: Integer null interpreted as bytea - postgresql

In PostgreSQL I have the table
CREATE TABLE public.my_table
(
id integer NOT NULL,
...
I want to perform the query: Show me the rows with a given id. If id is null, show me all rows.
I tried it with
public interface MyRepository extends JpaRepository<MyTable, Integer> {
#Query(value = "SELECT * FROM my_table WHERE (?1 IS NULL OR id = ?1)", nativeQuery = true)
List<MyTable> findAll(Integer id);
If id != null, everything is fine. But if id == null, I will receive the error
org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:261) ~[spring-orm-4.3.13.RELEASE.jar:4.3.13.RELEASE]
...
Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:106) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
...
Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: integer = bytea
Hint: No operator matches the given name and argument type(s). You might need to add explicit type casts.
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2440) ~[postgresql-42.2.5.jar:42.2.5]
...
Obviously short circuit evaluation does not work and null is transformed into bytea.
As a workaround I have changed the query value into
SELECT * FROM my_table WHERE (?1 IS NULL OR id = (CAST (CAST(?1 AS character varying) AS integer)))
But this is not nice, because the int is cast to string and to int again. Do you have a better solution, e.g. a better cast or sql query?

Another workaround for this is to create the query manually from the EntityManager (em in the example) and call setParameter on it once with a non-null value, then again with the real value.
private static final Integer exampleInt = 1;
List<MyTable> findAll(Integer id) {
return em.createNativeQuery("SELECT * FROM my_table WHERE (:id IS NULL OR id = :id)", MyTable.class)
.setParameter("id", exampleInt)
.setParameter("id", id)
.resultList();
}
This ensures that Hibernate knows the type of value the next second time it's called, even if it's null.
The fault is in the PostgreSQL server, and not in Hibernate, but they have refused to fix it, because it works as intended. You just have a few hundred types of SQL NULL on the server and they're mostly incompatible with each-other, even though it's supposed to be one singular special value.

Related

JPA Criteria on PostgreSQL boolean type creates invalid query

I have a regular JPA Entity which has a boolean field:
#Entity("UserAccount")
public class UserAccount {
...
#Column(name = "isActive")
private boolean isActive = false;
...
}
This is backed by a table in PostgreSQL 15. In the table definition, the field is of type BOOLEAN (not integer!):
CREATE TABLE UserAccount(
...
isActive BOOLEAN NOT NULL,
...
)
When I attempt to use the JPA Criteria API with it:
criteriaBuilder.isTrue(root.get("isActive"))
... then it will be rendered as = 1 in the SQL query which is sent to PostgreSQL:
SELECT ...
FROM UserAccount
WHERE isActive = 1
PostgreSQL rejects this query with the following error (which is pretty clear):
PSQLException: ERROR: operator does not exist: boolean = integer
Hint: No operator matches the given name and argument types. You might need to add explicit type casts.
My question is: how can I tell Hibernate that I'm using an actual PostgreSQL-native BOOLEAN type in my table instead of encoding the boolean as a number?

Spring data native query filtering by List<String>

Given:
Interface projection model with
String getType();
Postgres 13;
Hibernate+JPA+Spring data;
Query filter:
" where ... "
+ " and (COALESCE(:types) is null or model.type in (:types)) "
Pageable method in repository contains param
#Param("types") #Nullable List<String> types
generated SQL when null types is being passed:
and (COALESCE(?) is null or folderModel.contentType in (?))
error after it thrown:
org.postgresql.util.PSQLException: ERROR: operator does not exist: character varying = bytea
Tips: No operator matches the given name and argument types. You might need to add explicit type casts.
There is no problem when I pass any nullable params in 'where' clause likewise when types isn't null and contains valid types. I tried to cast(model.type to varchar) with no any success. Any ideas?

Error while ignoring null query parameters - Spring JPA

I am using Spring JPA in my application to fetch certain records from the DB. Now, one of the query parameters that I am passing can be null in certain criteria. So I have designed the query in such a way that if the query parameter is not null, then the query parameter is used for extraction otherwise it is ignored.
Query
#Query(value = "SELECT * FROM fulfilment_acknowledgement WHERE entity_id = :entityId " +
"and item_id = :itemId " +
"and (fulfilment_id is null OR :fulfilmentId is null OR fulfilment_id = :fulfilmentId) " +
"and type = :type", nativeQuery = true)
FulfilmentAcknowledgement findFulfilmentAcknowledgement(#Param(value = "entityId") String entityId, #Param(value = "itemId") String itemId,
#Param(value = "fulfilmentId") Long fulfilmentId, #Param(value = "type") String type);
NOTE: The type of fulfilment_id in the table fulfilment_acknowledgement is int8. It is a Postgres RDS.
Now, if I encounter a scenario where the fulfilmentId is actually blank, I am getting the below error:
2022-06-23 15:31:56,997 89645 [boundedElastic-5] DEBUG org.hibernate.SQL - SELECT * FROM fulfilment_acknowledgement WHERE entity_id = ? and item_id = ? and (fulfilment_id is null OR ? is null OR fulfilment_id = ?) and type = ?
2022-06-23 15:31:57,154 89802 [boundedElastic-5] DEBUG o.h.e.jdbc.spi.SqlExceptionHelper - could not extract ResultSet [n/a]
Exception while creating Fulfilment Acknowledgement: [could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet]
Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: bigint = bytea
Hint: No operator matches the given name and argument types. You might need to add explicit type casts.
I have a solution in mind where I can update the fulfilmentId to a default value like -1 if it is null, but I need to understand why is it failing? What am I missing here?
I was running into similar issue for a native query, which find records with matching column values. When the value for any parameter is null, hibernate uses the wrong type throws casting exceptions. After spending some time I found that in such cases need to use Hibernate's TypedParameterValue
So in your case #Param(value = "fulfilmentId") Long fulfilmentId would be
#Param(value = "fulfilmentId") TypedParameterValue fulfilmentId
Also from the service you need to convert your parameter accordingly from Long to TypedParameterValue as below:
TypedParameterValue fulfilmentIdParam = new TypedParameterValue(StandardBasicTypes.LONG, fulfilmentId);

Call jsonb_contains function (postgres) using JPA criteria and JsonBinaryType

I have a JPA/Hibernate entity which has a JSONB column (using https://github.com/vladmihalcea/hibernate-types ) for storing a list of strings. This works fine so far.
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
#Type(type = "jsonb")
#Column(name = "TAGS", columnDefinition = "jsonb")
private List<String> tags;
Now I want to check if another string is contained in the list of strings.
I can do this by writing a native query and use the #> operator from Postgres. Because of other reasons (the query is more complex) I do not want to go in that direction. My current approach is calling the jsonb_contains method in a Spring Data specification (since the operator is just alias to this function), e.g. jsonb_contains('["tag1", "tag2", "tag3"]','["tag1"]'). What I am struggling with is, getting the second parameter right.
My initial approach is to also use a List of Strings.
public static Specification<MyEntity> hasTag(String tag) {
return (root, query, cb) -> {
if (StringUtils.isEmpty(tag)) {
return criteriaBuilder.conjunction();
}
Expression<Boolean> expression = criteriaBuilder.function("jsonb_contains",
Boolean.class,
root.get("tags"),
criteriaBuilder.literal(List.of(tag)));
return criteriaBuilder.isTrue(expression);
};
}
This results in the following error.
Caused by: org.postgresql.util.PSQLException: ERROR: function jsonb_contains(jsonb, character varying) does not exist
Hinweis: No function matches the given name and argument types. You might need to add explicit type casts.
Position: 375
It does know that root.get("tags") is mapped to JSONB but for the second parameter it does not. How can I get this right? Is this actually possible?
jsonb_contains(jsob, jsonb) parameters must be jsonb type.
You can not pass a Java String as a parameter to the function.
You can not do casting in Postgresql via JPA Criteria.
Using JSONObject or whatever does not help because Postgresql sees it as
bytea type.
There are 2 possible solutions:
Solution 1
Create jsonb with jsonb_build_object(text[]) function and send it to jsonb_contains(jsonb, jsonb) function:
public static Specification<MyEntity> hasTag(String tag) {
// get List of key-value: [key1, value1, key2, value2...]
List<Object> tags = List.of(tag);
// create jsonb from array list
Expression<?> jsonb = criteriaBuilder.function(
"jsonb_build_object",
Object.class,
cb.literal(tags)
);
Expression<Boolean> expression = criteriaBuilder.function(
"jsonb_contains",
Boolean.class,
root.get("tags"),
jsonb
);
return criteriaBuilder.isTrue(expression);
}
Solution 2
Create custom function in your Postgresql and use it in Java:
SQL:
CREATE FUNCTION jsonb_contains_as_text(a jsonb, b text)
RETURNS BOOLEAN AS $$
SELECT CASE
WHEN a #> b::jsonb THEN TRUE
ELSE FALSE
END;$$
LANGUAGE SQL IMMUTABLE STRICT;
Java Code:
public static Specification<MyEntity> hasTag(String tag) {
Expression<Boolean> expression = criteriaBuilder.function(
"jsonb_contains_as_text",
Boolean.class,
root.get("tags"),
criteriaBuilder.literal(tag)
);
return criteriaBuilder.isTrue(expression);
}
I think that the reason is that you pass the varchar as the second param. jsonb_contains() requires two jsonb params.
To check a jsonb array contains all/any values from a string array you need to use another operators: ?& or ?|.
The methods bindings for them in PSQL 9.4 are: jsonb_exists_all and jsonb_exists_any correspondingly.
In your PSQL version, you could check it by the following command:
select * from pg_operator where oprname = '?&'

JdbcTemplate query returns BadSqlGrammarException

Postgres db:
CREATE TYPE pr_status_name AS ENUM ('CREATED', 'SUCCESS', 'FAILED');
create table payment_request_statuses(
status_id serial PRIMARY KEY,
pr_status_name pr_status_name NOT NULL
);
INSERT INTO payment_request_statuses(pr_status_name) VALUES ('CREATED');
INSERT INTO payment_request_statuses(pr_status_name) VALUES ('SUCCESS');
INSERT INTO payment_request_statuses(pr_status_name) VALUES ('FAILED');
when I am trying to execute the method:
Map<String, Object> data = jdbcTemplate.queryForMap("select * from payment_request_statuses where pr_status_name = ?", new Object[]{"CREATED"});
I am getting the following error:
rg.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [select * from payment_request_statuses where pr_status_name = ?]; nested exception is org.postgresql.util.PSQLException: ERROR: operator does not exist: pr_status_name = character varying
Hint: No operator matches the given name and argument types. You might need to add explicit type casts.
Seems, jdbcTemplate for some reason is not able transform/match String to db Enum object that is surprise for me.
How can I fix it?
You need to cast from String to Enum in SQL.
Map<String, Object> data = jdbcTemplate.queryForMap("select * from payment_request_statuses where pr_status_name = ?::pr_status_name", new Object[]{"CREATED"});