如何将 Postgres any 子句与 JPA/Hibernate 本机查询(数组参数)一起使用
How to use the Postgres any-clause with JPA/Hibernate native queries (array parameters)
所以我们有大量的 Postgres SQL 查询存储在文件中并从 PHP 中使用。任务是将 PHP 替换为 Java。我们希望重用尽可能多的查询 "as is" 以缩短迁移路径。我无法使 Array 参数起作用。
这是一个查询示例:
update user_devices
set some_date = now()
where some_id in (
select distinct some_id from user_devices
where user_id = any(:userIDs) and device_id = any(:deviceIDs)
and exists (select 1 from users where user_id = any(:userIDs) and customer_id = :customerID)
);
请注意导致问题的 "any" 子句,因为它们需要数组类型。
这就是我们在 PHP 中使用它们的方式:
$this->allValues['userIDs'] = '{' . implode ( ",", $userIdNodes ) . '}';
$this->allValues['deviceIDs'] = '{' . implode ( ",", $deviceIdNodes ) . '}';
$this->allValues['customerID'] = customerID;
$this->db->runQuery ( $this->getQuery ( 'my_query' ), $this->allValues );
所以作为参数,数组类型看起来像“{111,222}”。
这是我在 Java 中尝试过的:
Integer customerID = 1;
int[] userIDs = new int[]{111,222};
int[] deviceIDs= new int[]{333,444};
//List<Integer> userIDs = Arrays.asList(111,222);
//List<Integer> deviceIDs= Arrays.asList(333,444);
//java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
//java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
//java.sql.Array userIDs = toArray("integer", Arrays.asList(111,222)));
//java.sql.Array deviceIDs= toArray("integer", Arrays.asList(333,444)));
//String userIDs = "{111,222}";
//String deviceIDs= "{333,444}";
//String userIDs = "ARRAY[111,222]";
//String deviceIDs= "ARRAY[333,444]";
Query nativeQuery = em.createNativeQuery(queryString);
nativeQuery.setParameter("userIDs", userIDs);
nativeQuery.setParameter("deviceIDs", deviceIDs);
nativeQuery.setParameter("customerID", customerID);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), userIDs);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), deviceIDs);
//nativeQuery.setParameter(createParameter("customerID", Integer.class), customerID);
query.executeUpdate();
//[...]
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}
private <T> Parameter<T> createParameter(final String name, final Class<?> clazz) {
return new Parameter<T>() {
@Override
public String getName() {
return name;
}
@Override
public Integer getPosition() {
return null; // not used
}
@Override
public Class<T> getParameterType() {
return (Class<T>) clazz;
}
};
}
None 这些将工作我将得到这些异常之一:
使用 "toArray" 方法时:
Caused by: org.hibernate.HibernateException: Could not determine a type for class: org.postgresql.jdbc4.Jdbc4Array
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:550)
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:534)
at org.hibernate.internal.AbstractQueryImpl.determineType(AbstractQueryImpl.java:519)
at org.hibernate.internal.AbstractQueryImpl.setParameter(AbstractQueryImpl.java:487)
at org.hibernate.jpa.internal.QueryImpl$ParameterRegistrationImpl.bindValue(QueryImpl.java:247)
at org.hibernate.
或者当使用 int[] 或字符串时,我会得到:
Caused by: org.postgresql.util.PSQLException: ERROR: op ANY/ALL (array) requires array on right side
Position: 137
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2270)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1998)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:255)
at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:570)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:420)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:366)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.postgresql.ds.jdbc23.AbstractJdbc23PooledConnection$StatementHandler.invoke(AbstractJdbc23PooledConnection.java:453)
at com.sun.proxy.$Proxy274.executeUpdate(Unknown Source)
at com.sun.gjc.spi.base.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:125)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
jpa.spi.BaseQueryImpl.setParameter(BaseQueryImpl.java:582)
当两个 API 都与数据库通信时,我使用 Wireshark 发现了这一点:
Image: Comparison of database calls with Wireshark
select oid, typname from pg_type where oid in (0, 23, 1043) order by oid;
oid |typname
------+-------
23 |int4
1043 |varchar
有没有人设法将数组参数与使用 Hibernate 作为 JPA EntityManager 后端的本机查询一起使用?如果是这样:如何?
我能够通过从 EntityManager 解开 Hibernate 会话并使用 JDBC PreparedStatement 来解决这个问题,它毫无怨言地吃掉 java.sql.Array 参数。
NamedParameterStatement used in the example below is described here(我已根据需要对其进行了修改)。它委托给 PreparedStatement。
其余代码有点像这样:
public int executeUpdate(...){
//....
Integer customerID = 1;
java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
final AtomicInteger rowsModifiedRef = new AtomicInteger();
final Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
session.doWork((c) -> {
try (final NamedParameterStatement statement = new NamedParameterStatement(c, queryString)) {
statement.setObject("deviceIDs", userIDs);
statement.setObject("userIDs", userIDs);
statement.setObject("customerID", userIDs);
rowsModifiedRef.set(statement.executeUpdate());
}
});
return rowsModifiedRef.get();
}
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}
将查询从 where user_id = any(:userIDs)
更改为 where user_id IN (:userIDs)
,并将 userIDs
数组更改为集合,例如List<Long>
。您将不得不额外保护它的空列表,但它会起作用。
所以我们有大量的 Postgres SQL 查询存储在文件中并从 PHP 中使用。任务是将 PHP 替换为 Java。我们希望重用尽可能多的查询 "as is" 以缩短迁移路径。我无法使 Array 参数起作用。
这是一个查询示例:
update user_devices
set some_date = now()
where some_id in (
select distinct some_id from user_devices
where user_id = any(:userIDs) and device_id = any(:deviceIDs)
and exists (select 1 from users where user_id = any(:userIDs) and customer_id = :customerID)
);
请注意导致问题的 "any" 子句,因为它们需要数组类型。 这就是我们在 PHP 中使用它们的方式:
$this->allValues['userIDs'] = '{' . implode ( ",", $userIdNodes ) . '}';
$this->allValues['deviceIDs'] = '{' . implode ( ",", $deviceIdNodes ) . '}';
$this->allValues['customerID'] = customerID;
$this->db->runQuery ( $this->getQuery ( 'my_query' ), $this->allValues );
所以作为参数,数组类型看起来像“{111,222}”。
这是我在 Java 中尝试过的:
Integer customerID = 1;
int[] userIDs = new int[]{111,222};
int[] deviceIDs= new int[]{333,444};
//List<Integer> userIDs = Arrays.asList(111,222);
//List<Integer> deviceIDs= Arrays.asList(333,444);
//java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
//java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
//java.sql.Array userIDs = toArray("integer", Arrays.asList(111,222)));
//java.sql.Array deviceIDs= toArray("integer", Arrays.asList(333,444)));
//String userIDs = "{111,222}";
//String deviceIDs= "{333,444}";
//String userIDs = "ARRAY[111,222]";
//String deviceIDs= "ARRAY[333,444]";
Query nativeQuery = em.createNativeQuery(queryString);
nativeQuery.setParameter("userIDs", userIDs);
nativeQuery.setParameter("deviceIDs", deviceIDs);
nativeQuery.setParameter("customerID", customerID);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), userIDs);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), deviceIDs);
//nativeQuery.setParameter(createParameter("customerID", Integer.class), customerID);
query.executeUpdate();
//[...]
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}
private <T> Parameter<T> createParameter(final String name, final Class<?> clazz) {
return new Parameter<T>() {
@Override
public String getName() {
return name;
}
@Override
public Integer getPosition() {
return null; // not used
}
@Override
public Class<T> getParameterType() {
return (Class<T>) clazz;
}
};
}
None 这些将工作我将得到这些异常之一: 使用 "toArray" 方法时:
Caused by: org.hibernate.HibernateException: Could not determine a type for class: org.postgresql.jdbc4.Jdbc4Array
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:550)
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:534)
at org.hibernate.internal.AbstractQueryImpl.determineType(AbstractQueryImpl.java:519)
at org.hibernate.internal.AbstractQueryImpl.setParameter(AbstractQueryImpl.java:487)
at org.hibernate.jpa.internal.QueryImpl$ParameterRegistrationImpl.bindValue(QueryImpl.java:247)
at org.hibernate.
或者当使用 int[] 或字符串时,我会得到:
Caused by: org.postgresql.util.PSQLException: ERROR: op ANY/ALL (array) requires array on right side
Position: 137
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2270)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1998)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:255)
at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:570)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:420)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:366)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.postgresql.ds.jdbc23.AbstractJdbc23PooledConnection$StatementHandler.invoke(AbstractJdbc23PooledConnection.java:453)
at com.sun.proxy.$Proxy274.executeUpdate(Unknown Source)
at com.sun.gjc.spi.base.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:125)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
jpa.spi.BaseQueryImpl.setParameter(BaseQueryImpl.java:582)
当两个 API 都与数据库通信时,我使用 Wireshark 发现了这一点:
Image: Comparison of database calls with Wireshark
select oid, typname from pg_type where oid in (0, 23, 1043) order by oid;
oid |typname
------+-------
23 |int4
1043 |varchar
有没有人设法将数组参数与使用 Hibernate 作为 JPA EntityManager 后端的本机查询一起使用?如果是这样:如何?
我能够通过从 EntityManager 解开 Hibernate 会话并使用 JDBC PreparedStatement 来解决这个问题,它毫无怨言地吃掉 java.sql.Array 参数。
NamedParameterStatement used in the example below is described here(我已根据需要对其进行了修改)。它委托给 PreparedStatement。
其余代码有点像这样:
public int executeUpdate(...){
//....
Integer customerID = 1;
java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
final AtomicInteger rowsModifiedRef = new AtomicInteger();
final Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
session.doWork((c) -> {
try (final NamedParameterStatement statement = new NamedParameterStatement(c, queryString)) {
statement.setObject("deviceIDs", userIDs);
statement.setObject("userIDs", userIDs);
statement.setObject("customerID", userIDs);
rowsModifiedRef.set(statement.executeUpdate());
}
});
return rowsModifiedRef.get();
}
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}
将查询从 where user_id = any(:userIDs)
更改为 where user_id IN (:userIDs)
,并将 userIDs
数组更改为集合,例如List<Long>
。您将不得不额外保护它的空列表,但它会起作用。