无法在 Java (java.sql.PreparedStatement) 中准备 Postgres 语句
Unable to Prepare a Postgres Statement in Java (java.sql.PreparedStatement)
考虑我可能想从 Java.
发送到我的服务器的两个语句
- 简单SQL:这是典型的插入语句。
insert into table_things (thing_1_value, thing_2_value) values(?, ?);
- PlPgSQL:我想通过在数据库中进行一些登录来避免往返数据库。我们不允许在数据库中使用存储过程或函数(原因似乎是有效的)。
do $$
declare
my_thing1 varchar(100) = ?;
my_thing2 varchar(100) = ?;
begin
insert into table_things
(
thing_1_value
, thing_2_value
)
values
(
my_thing1
, my_thing2
)
;
end
$$;
执行这些语句的代码在下面 Java8 个测试用例中表示:
package com.somecompany.someservice.test.database;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.Assert;
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Types;
public class PreparedStatementDatabaseTest {
private static final String CONNECTION_URI = "jdbc:postgresql://localhost:5432/somedb?user=someuser&password=somepass";
private static final String PLPGSQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" my_thing1 varchar(100) = ?;\n" +
" my_thing2 varchar(100) = ?;\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" thing_1_value\n" +
" , thing_2_value\n" +
" )\n" +
" values\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
private static final String EASY_SQL_STATEMENT = "insert into table_things (thing_1_value, thing_2_value) values(?, ?);";
@Test
public void testPlpgsqlStatement() throws Exception {
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(PLPGSQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
}
@Test
public void testEasySqlStatement() throws Exception {
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(EASY_SQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
}
}
testEasySqlStatement
有效,但 testPlpgsqlStatement
抛出异常:
org.postgresql.util.PSQLException: The column index is out of range: 1, number of columns: 0.
at org.postgresql.core.v3.SimpleParameterList.bind(SimpleParameterList.java:65)
at org.postgresql.core.v3.SimpleParameterList.setStringParameter(SimpleParameterList.java:128)
at org.postgresql.jdbc.PgPreparedStatement.bindString(PgPreparedStatement.java:996)
at org.postgresql.jdbc.PgPreparedStatement.setString(PgPreparedStatement.java:326)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:528)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:881)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:185)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:185)
at com.somecompany.someservicetest.database.PreparedStatementDatabaseTest.testPlpgsqlStatement(PreparedStatementDatabaseTest.java:44)
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.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=15=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
问题: 如何将 PLPGSQL_STATEMENT
这样的代码发送到 Postgres 数据库?
我可以这样做,但由于 SQL 注入风险,这是不好的做法:
@Test
public void testSqlInjectionRisk() throws Exception {
String hello = "hello-testSqlInjectionRisk";
String world = "world-testSqlInjectionRisk";
String PLPGSQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" my_thing1 varchar(100) = '" + hello + "';\n" +
" my_thing2 varchar(100) = '" + world + "';\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" thing_1_value\n" +
" , thing_2_value\n" +
" )\n" +
" values\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(PLPGSQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
问题重述:我尝试准备的方式有问题吗PLPGSQL_STATEMENT
? PLPGSQL_STATEMENT
可以准备吗?
更新:@Izruo 指出我应该使用 prepareCall
,这似乎是答案的一部分。但不幸的是,以下代码因相同的异常而失败:
@Test
public void testEasySqlStatement2() throws Exception {
final String SQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" x varchar(100) = ?;\n" +
" y varchar(100) = ?;\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" values\n" +
" (\n" +
" x\n" +
" , y\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
System.out.println(SQL_STATEMENT);
Connection conn = basicDataSource.getConnection();
CallableStatement statement = conn.prepareCall(SQL_STATEMENT);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
如果我将 System.out.println(SQL_STATEMENT);
打印的 sql 语句复制到 DataGrip(JetBrains 的数据库 IDE)并 运行 它,然后 DataGrip 要求我填写在两个参数值中(对于两个问号)并成功 运行s sql 语句。换句话说,plpgsql 代码在语法上是有效的(一旦参数被替换)。
这里好像有三种可能,我也分不清哪个是对的:
- 不支持此功能(创建一个 CallableStatement/PreparedStatement 其中包含 plpgsql 变量)。
- 支持此功能,但我做错了。
- 支持该功能,我使用正确,但有一个错误。
不能直接调用动态过程。
您必须先创建过程(手动或通过语句调用动态创建),然后按名称调用过程。
Statement stmt = conn.createStatement();
stmt.execute("CREATE OR REPLACE FUNCTION myfunc(x text, y text) RETURNS refcursor AS '"
+ "do $$\n"
+ "begin\n"
+ " insert into table_things\n"
+ " (\n"
+ " my_thing1\n"
+ " , my_thing2\n"
+ " )\n"
+ " values\n"
+ " (\n"
+ " x\n"
+ " , y\n"
+ " )\n"
+ " ;\n"
+ "end\n"
+ "$$;"
+ "' language plpgsql");
stmt.close();
// We must be inside a transaction for cursors to work.
conn.setAutoCommit(false);
// Procedure call.
CallableStatement proc = conn.prepareCall("{ call myfunc(?,?)}");
CallableStatement statement = conn.prepareCall(SQL_STATEMENT);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
try (Connection con = DriverManager.getConnection(dbConnectionString, user, password);
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM public.\"Airplanes\" ")) {
while (rs.next()) {
//use result
}
} catch (SQLException ex) {
//handle exception
}
return results;
}
尽管 Mạnh Quyết Nguyễn 的回答是正确的,但没有解释,建议的解决方法在我的情况下不可行(我认为大多数情况下)。
我收到了来自postgresql.org的权威回答。
you attempted to add question marks to a location where they are not interpreted as parameters.
Basically you wrote:
SELECT 'let me say ? ? to you';
Which is a perfectly valid query that has zero input parameters and
will return:
"let me say ? ? to you"
It has no input parameters because the question marks you wrote are
inside a string literal.
The $$...$$ in your DO statement also denote a string literal.
这很不幸,据我所知,如果您需要将参数传递到 PL/pgSQL 代码,则整个 PL/pgSQL 语言都是 off-limits。 (当然,除非您即时或作为模式开发的一部分编译 PL/pgSQL 过程或函数)。看起来我无法将 PL/pgSQL 'script' 连同参数一起发送到数据库。
考虑我可能想从 Java.
发送到我的服务器的两个语句- 简单SQL:这是典型的插入语句。
insert into table_things (thing_1_value, thing_2_value) values(?, ?);
- PlPgSQL:我想通过在数据库中进行一些登录来避免往返数据库。我们不允许在数据库中使用存储过程或函数(原因似乎是有效的)。
do $$
declare
my_thing1 varchar(100) = ?;
my_thing2 varchar(100) = ?;
begin
insert into table_things
(
thing_1_value
, thing_2_value
)
values
(
my_thing1
, my_thing2
)
;
end
$$;
执行这些语句的代码在下面 Java8 个测试用例中表示:
package com.somecompany.someservice.test.database;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.Assert;
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Types;
public class PreparedStatementDatabaseTest {
private static final String CONNECTION_URI = "jdbc:postgresql://localhost:5432/somedb?user=someuser&password=somepass";
private static final String PLPGSQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" my_thing1 varchar(100) = ?;\n" +
" my_thing2 varchar(100) = ?;\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" thing_1_value\n" +
" , thing_2_value\n" +
" )\n" +
" values\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
private static final String EASY_SQL_STATEMENT = "insert into table_things (thing_1_value, thing_2_value) values(?, ?);";
@Test
public void testPlpgsqlStatement() throws Exception {
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(PLPGSQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
}
@Test
public void testEasySqlStatement() throws Exception {
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(EASY_SQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
}
}
testEasySqlStatement
有效,但 testPlpgsqlStatement
抛出异常:
org.postgresql.util.PSQLException: The column index is out of range: 1, number of columns: 0.
at org.postgresql.core.v3.SimpleParameterList.bind(SimpleParameterList.java:65)
at org.postgresql.core.v3.SimpleParameterList.setStringParameter(SimpleParameterList.java:128)
at org.postgresql.jdbc.PgPreparedStatement.bindString(PgPreparedStatement.java:996)
at org.postgresql.jdbc.PgPreparedStatement.setString(PgPreparedStatement.java:326)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:528)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:881)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:185)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:185)
at com.somecompany.someservicetest.database.PreparedStatementDatabaseTest.testPlpgsqlStatement(PreparedStatementDatabaseTest.java:44)
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.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=15=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
问题: 如何将 PLPGSQL_STATEMENT
这样的代码发送到 Postgres 数据库?
我可以这样做,但由于 SQL 注入风险,这是不好的做法:
@Test
public void testSqlInjectionRisk() throws Exception {
String hello = "hello-testSqlInjectionRisk";
String world = "world-testSqlInjectionRisk";
String PLPGSQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" my_thing1 varchar(100) = '" + hello + "';\n" +
" my_thing2 varchar(100) = '" + world + "';\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" thing_1_value\n" +
" , thing_2_value\n" +
" )\n" +
" values\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
Connection conn = basicDataSource.getConnection();
PreparedStatement statement = conn.prepareStatement(PLPGSQL_STATEMENT, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
问题重述:我尝试准备的方式有问题吗PLPGSQL_STATEMENT
? PLPGSQL_STATEMENT
可以准备吗?
更新:@Izruo 指出我应该使用 prepareCall
,这似乎是答案的一部分。但不幸的是,以下代码因相同的异常而失败:
@Test
public void testEasySqlStatement2() throws Exception {
final String SQL_STATEMENT = "" +
"do $$\n" +
"declare\n" +
" x varchar(100) = ?;\n" +
" y varchar(100) = ?;\n" +
"begin\n" +
" insert into table_things\n" +
" (\n" +
" my_thing1\n" +
" , my_thing2\n" +
" )\n" +
" values\n" +
" (\n" +
" x\n" +
" , y\n" +
" )\n" +
" ;\n" +
"end\n" +
"$$;";
Class.forName("org.postgresql.Driver");
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setUrl(CONNECTION_URI);
System.out.println(SQL_STATEMENT);
Connection conn = basicDataSource.getConnection();
CallableStatement statement = conn.prepareCall(SQL_STATEMENT);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
boolean isResultSet = statement.execute();
conn.close();
Assert.assertFalse(isResultSet);
如果我将 System.out.println(SQL_STATEMENT);
打印的 sql 语句复制到 DataGrip(JetBrains 的数据库 IDE)并 运行 它,然后 DataGrip 要求我填写在两个参数值中(对于两个问号)并成功 运行s sql 语句。换句话说,plpgsql 代码在语法上是有效的(一旦参数被替换)。
这里好像有三种可能,我也分不清哪个是对的:
- 不支持此功能(创建一个 CallableStatement/PreparedStatement 其中包含 plpgsql 变量)。
- 支持此功能,但我做错了。
- 支持该功能,我使用正确,但有一个错误。
不能直接调用动态过程。
您必须先创建过程(手动或通过语句调用动态创建),然后按名称调用过程。
Statement stmt = conn.createStatement();
stmt.execute("CREATE OR REPLACE FUNCTION myfunc(x text, y text) RETURNS refcursor AS '"
+ "do $$\n"
+ "begin\n"
+ " insert into table_things\n"
+ " (\n"
+ " my_thing1\n"
+ " , my_thing2\n"
+ " )\n"
+ " values\n"
+ " (\n"
+ " x\n"
+ " , y\n"
+ " )\n"
+ " ;\n"
+ "end\n"
+ "$$;"
+ "' language plpgsql");
stmt.close();
// We must be inside a transaction for cursors to work.
conn.setAutoCommit(false);
// Procedure call.
CallableStatement proc = conn.prepareCall("{ call myfunc(?,?)}");
CallableStatement statement = conn.prepareCall(SQL_STATEMENT);
statement.setObject(1, "hello", Types.VARCHAR);
statement.setObject(2, "world", Types.VARCHAR);
try (Connection con = DriverManager.getConnection(dbConnectionString, user, password);
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM public.\"Airplanes\" ")) {
while (rs.next()) {
//use result
}
} catch (SQLException ex) {
//handle exception
}
return results;
}
尽管 Mạnh Quyết Nguyễn 的回答是正确的,但没有解释,建议的解决方法在我的情况下不可行(我认为大多数情况下)。
我收到了来自postgresql.org的权威回答。
you attempted to add question marks to a location where they are not interpreted as parameters.
Basically you wrote:
SELECT 'let me say ? ? to you';
Which is a perfectly valid query that has zero input parameters and will return:
"let me say ? ? to you"
It has no input parameters because the question marks you wrote are inside a string literal.
The $$...$$ in your DO statement also denote a string literal.
这很不幸,据我所知,如果您需要将参数传递到 PL/pgSQL 代码,则整个 PL/pgSQL 语言都是 off-limits。 (当然,除非您即时或作为模式开发的一部分编译 PL/pgSQL 过程或函数)。看起来我无法将 PL/pgSQL 'script' 连同参数一起发送到数据库。