问题
So we've got a whole lot of Postgres SQL queries stored in files and used from PHP. The task is to replace PHP with Java. We want to reuse as much of the queries "as is" to keep the migration path short. I can't get the Array parameters to work.
Here's a query example:
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)
);
Note the "any" clauses, which cause the problem, because they expect an array type. This is how we used them from PHP:
$this->allValues['userIDs'] = '{' . implode ( ",", $userIdNodes ) . '}';
$this->allValues['deviceIDs'] = '{' . implode ( ",", $deviceIdNodes ) . '}';
$this->allValues['customerID'] = customerID;
$this->db->runQuery ( $this->getQuery ( 'my_query' ), $this->allValues );
So as parameters the array types look like "{111,222}".
This is what I tried in 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 of these will work I will get one of these exceptions: When using the "toArray" method:
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.
Or when using int[] or Strings, I'll get:
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)
Using Wireshark I found this when both APIs are talking to the database:
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
Has anyone managed to use array-parameters with native queries using Hibernate as backend for the JPA EntityManager? If so: How?
回答1:
I was able to work around this problem by unwrapping the Hibernate session from the EntityManager and use a JDBC PreparedStatement, which eats the java.sql.Array parameters without any complaint.
The NamedParameterStatement used in the example below is described here (I've modified it to my needs). It delegates to a PreparedStatement.
The rest of the code goes a little something like this:
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();
}
回答2:
Change your query from where user_id = any(:userIDs)
to where user_id IN (:userIDs)
, and change the userIDs
array to a collection e.g. List<Long>
. You will have to additionally protect it empty lists, but it will work.
来源:https://stackoverflow.com/questions/36601318/how-to-use-the-postgres-any-clause-with-jpa-hibernate-native-queries-array-para