Why does H2 alias duplicate inserted rows? - java

I have created the following H2 alias:
CREATE ALIAS INSERT_CHANGE_RECORD AS '
java.sql.ResultSet insertChangeRecord(final java.sql.Connection conn) throws java.sql.SQLException
{
String sql = "insert into `change_records` (`made_when`, `made_by`) values (CURRENT_TIMESTAMP(9), ''admin'');";
java.sql.PreparedStatement ps = conn.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
ps.executeUpdate();
java.sql.ResultSet results = ps.getGeneratedKeys() ;
return results;
}
';
Weirdly enough, when I call the alias (once) using call INSERT_CHANGE_RECORD(); 3 identical records are created in the table, instead of one.
When I use the following definition of the alias instead (the difference is that I actually retrieve the generated ID from the resultset), only 1 row is inserted.
CREATE ALIAS IF NOT EXISTS INSERT_CHANGE_RECORD AS '
Long insertChangeRecord(final java.sql.Connection conn) throws java.sql.SQLException
{
String sql = "insert into `change_records` (`made_when`, `made_by`) values (CURRENT_TIMESTAMP(9), ''admin'');";
java.sql.PreparedStatement ps = conn.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
ps.executeUpdate();
java.sql.ResultSet results = ps.getGeneratedKeys();
results.next();
return results.getLong(1);
}
';
Is this a bug in H2, or is there any reasonable explanation for this behavior? I'm using H2 2.1.210.
Here is the DDL of the table that I insert into.
CREATE TABLE IF NOT EXISTS `change_records` (
`id` BIGINT NOT NULL AUTO_INCREMENT CONSTRAINT `change_records_id_pk` PRIMARY KEY,
`made_when` TIMESTAMP (9) WITH TIME ZONE NOT NULL,
`made_by` VARCHAR NOT NULL
);

Function aliases returning a ResultSet are called multiple times. You need to check URL of passed connection. If it is equal to jdbc:columnlist:connection, you need to return an empty ResultSet with properly configured columns, you can use org.h2.tools.SimpleResultSet or some other implementation. These invocations are performed during compilation or recompilation of the query before its actual execution.
String url = conn.getMetaData().getURL();
if (url.equals("jdbc:columnlist:connection")) {
SimpleResultSet rs = new SimpleResultSet();
// With some connection options "id" should be used instead
rs.addColumn("ID", Types.BIGINT, 19, 0);
return rs;
}
// main code
This result set must have columns with exactly the same names and data types as your function normally returns. Be careful with column names, `id` usually means "ID", but if ;DATABASE_TO_LOWER=TRUE or ;DATABASE_TO_UPPER=FALSE were specified in JDBC URL used by your application, it means "id". Your function shouldn't modify any data during this execution, it is only asked about its metadata.
If URL is different, it is a real function call during execution of a query or other command and you need to execute your code.
See also an example:
https://github.com/h2database/h2database/blob/45b609dec0e45125e6a93f85c9018d34551332a1/h2/src/test/org/h2/samples/Function.java#L140

Related

How do PostgreeSQL functions return multiple columns and rows to a Java application?

I am trying to get 3 fields from 1 table using a function and I am getting errors:
CREATE OR REPLACE FUNCTION sp_search_test_ui_test_prog_revision(dev TEXT)
RETURNS table (dev_op_test_id BIGINT, test_program_name TEXT, test_program_revision TEXT)
AS $$
BEGIN
RETURN QUERY
SELECT dev_op_test_id, test_program_name, test_program_revision
FROM dev_op_test
WHERE device = dev
ORDER BY dev_op_test_id;
END;
$$ LANGUAGE plpgsql;
The JAVA code is below: (device is a String passed to this method) Is this where the error is?
// Get unique devices from dev_op_test
String sql = " SELECT sp_search_test_ui_test_prog_revision(" + device + ") ";
PreparedStatement statement = pgConn.prepareStatement(sql);
ResultSet rs = statement.executeQuery();
// Clear from previous run
cboTestProgDev.getItems().clear();
while (rs.next()) {
TestProgRev tpr = new TestProgRev();
tpr.setDevOpTestId(rs.getLong(1));
tpr.setTestProgramName(rs.getString(2));
tpr.setTestProgramRevision(rs.getString(3));
testProgs.add(tpr);
cboTestProgDev.getItems().add(tpr.toString());
}
And this is the error I keep getting. Notice it's telling me the column doesn't exist, which is true because that is a value not a column name. Any ideas??? I know it may be something simple, I just can't seem to ding the anwser.
The query you are sending is
SELECT sp_search_test_ui_test_prog_revision(mnf0306aa)
Do you notice the missing single quotes around the string? That is why PostgreSQL interprets it as a column name and complains that the column does not exist.
Composing queries with string concatenation is dangerous, it exposes you to the dangers of SQL injection. If device contains a single quote, your statement would cause an error or worse – a skilled attacker could do anything with your database.
Use the power of prepared statements to avoid that danger:
java.sql.PreparedStatement statement =
pgConn.prepareStatement("SELECT * FROM sp_search_test_ui_test_prog_revision(?)";
statement.setString(1, device);
java.sql.ResultSet rs = statement.executeQuery();

select scope_identity() using createSQLQuery in Hibernate

I am forced to use createSQLQuery to insert values into tables with an Identity column (the first column and the primary key) using hibernate. Using hibernate classes are not an option since the tables are created on the fly for each customer that is added to the system. I have run the query and it successfully inserts into the table. I then execute a "select scope_identity()" and it always returns null. "select ##Identity" works but that is not guaranteed to be the correct one. I have also tried to append "select scope_identity()" to the insert query. Then I tried query.list() and query.uniqueResult() both of which throw the hibernate exception of "No Results ..."
Session session = DatabaseEngine.getSessionFactory().openSession();
String queryString = "insert into table1 (dataid) values (1)"
SQLQuery query = session.createSQLQuery(insertQueryString);
query.executeUpdate();
query = session.createSQLQuery("select scope_identity()");
BigDecimal entryID = (BigDecimal)query.uniqueResult();
The simple example table is defined as follows:
"CREATE TABLE table1 (EntryID int identity(1,1) NOT NULL," +
"DataID int default 0 NOT NULL, " +
"PRIMARY KEY (EntryID))";
Is there a way I am missing to use scope_identity() with createSQLQuery?
Actually the SQLServerDialect class used by Hibernate uses the same "scope_identity()" too.
The reason why it's not working is because you need to execute those in the same statement or stored procedure.
If you execute the scope_identity() call in a separate statement, SQL Server will not be able to give you last inserted identity value.
You cannot do it with the SQLQuery, even Hibernate uses JDBC to accomplish this task. I wrote a test on GitHub to emulate this and it works like this:
Session session = entityManager.unwrap(Session.class);
final AtomicLong resultHolder = new AtomicLong();
session.doWork(connection -> {
try(PreparedStatement statement = connection.prepareStatement("INSERT INTO post VALUES (?) select scope_identity() ") ) {
statement.setString(1, "abc");
if ( !statement.execute() ) {
while ( !statement.getMoreResults() && statement.getUpdateCount() != -1 ) {
// do nothing until we hit the resultset
}
}
try (ResultSet rs = statement.getResultSet()) {
if(rs.next()) {
resultHolder.set(rs.getLong(1));
}
}
}
});
assertNotNull(resultHolder.get());
The code uses Java 8 lambdas instead of anonymous classes, but you can easily port it to Java 1.7 too.

JDBC query RETURNING value

I have a problem trying to figure out how to get the id of the last inserted row using PostgreSQL and JDBC.
CREATE TABLE vet_care (
id serial PRIMARY KEY,
deworming int,
castration int
);
My query is
String insertVetCare = "INSERT INTO vet_care(castration,deworming) VALUES("+castrated+","+dewormed+") RETURNING id";
I want the id value (which is serial) for later use. I tried to execute the query like so:
int id = statement.executeUpdate(insertVetCare);
But this says after compilation, that "A result was returned when none was expected." and it does not insert the other values into table.
How can I get this to work?
If "id" means "generated identity key", then you've got it wrong.
The executeUpdate() method returns the number of affected rows, according to the javadocs.
You want it to return auto generated keys, like this.
More advice: Use PreparedStatement and bind values. Building up a SQL query that way is asking for a SQL injection attack.
// Why make the gc work? This query never changes.
private static final String INSERT_VET_CARE = "INSERT INTO vet_care(castration,deworming) VALUES(?, ?)";
PreparedStatement ps = connection.prepareStatement(INSERT_VET_CARE, Statement.RETURN_GENERATED_KEYS);
ps.setInt(1, castration);
ps.setInt(2, deworming);

Getting column metadata from jdbc/postgresql for newly created table

I'm trying to get the column list from newly created table(it is created in the java code).
The thing is that I do not get the columns.
The code works for tables that are already in the database, but if i create a new one and try to get the column info immediately it does not find any...
Update:
Here is full code that I used for testing:
#Test
public void testtest() throws Exception {
try (Connection conn = dataSource.getConnection()) {
String tableName = "Table_" + UUID.randomUUID().toString().replace("-", "");
try (Statement statement = conn.createStatement()) {
statement.executeUpdate(String.format("create table %s (id int primary key,name varchar(30));", tableName));
}
DatabaseMetaData metaData = conn.getMetaData();
try (ResultSet rs = metaData.getColumns(null, null, tableName, null)) {
int colsFound = 0;
while (rs.next()) {
colsFound++;
}
System.out.println(String.format("Found %s cols.", colsFound));
}
System.out.println(String.format("Autocommit is set to %s.", conn.getAutoCommit()));
}
}
The and the output:
Found 0 cols.
Autocommit is set to true.
The problem is with the case of your tablename:
String tableName = "Table_"
As that is an unquoted identifier (a good thing) the name is converted to lowercase when Postgres stores its name in the system catalog.
The DatabaseMetaData API calls are case sensitive ( "Table_" != "table_"), so you need to pass the lowercase tablename:
ResultSet rs = metaData.getColumns(null, null, tableName.toLowerCase(), null))
More details on how identifiers are using are in the manual: http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
I have made simple test and it seems to work. I can create new table and show its columns using PostgreSQL JDBC (I use Jython):
conn = db.createStatement()
conn.execute("CREATE TABLE new_table (id SERIAL, txt VARCHAR(200))")
db_meta_data = db.getMetaData()
for tbl_name in ('date_test', 'new_table'):
print('\n-- %s --' % (tbl_name))
rs = db_meta_data.getColumns(None, None, tbl_name, None)
while (rs.next()):
print('%s:%s' % (rs.getString(3), rs.getString(4)))
conn.close()
This code shows columns for both already existing table: date_test and for just created new_table. I also added some code to close connection after CREATE TABLE but my results are always the same and correct.
Maybe it is problem with your JDBC driver. I use driver from postgresql-9.3-1100.jdbc41.jar.
It may be also problem with user permissions. Do you use the same user for both creating table and getting metadata? Is new table visible in psql, pgAdmin or other tool?
Other reason is that PostgreSQL uses transactions also for schema changes. So if you disabled default autocommit and closed connection your schema changes will be lost. Do you use db.setAutoCommit(false)?
You can also query PostgreSQL schema directly:
SELECT DISTINCT table_name, column_name
FROM information_schema.columns
WHERE table_schema='public'
AND table_name = 'new_table'
ORDER BY 1, 2
Strangely giving passing table name in lower case to getColumns method does work...thanks for the query Michał Niklas it got me on the right track.

SQLException when using a PreparedStatement for Derby DB

I have an SQL query that i am going to run using a PreparedStatement, and it is
UPDATE tbl_HitsCounter SET count = ? WHERE keyid = (SELECT id FROM tbl_HitsMaster WHERE sitename = '?')
Now when i set the 2nd paramater, which is a string value, i am getting a strange SQLException.
preparedStatement.setInt(1, 99);
preparedStatement.setString(2, masterKey);
As the setString() method is executed, i am getting an SQLException
The column position '2' is out of range. The number of columns for this ResultSet is '1'.
I have no idea what this is about, i havent even executed the executeUpdate() method.
There is only one placeholder in your SQL but you are trying to assign a value for the second. Your problem is that you have quoted the second placeholder, your SQL should look more like this:
UPDATE tbl_HitsCounter
SET count = ?
WHERE keyid = (
SELECT id
FROM tbl_HitsMaster
WHERE sitename = ?
)
Note the lack of quotes in sitename = ?. This is a placeholder: ?. This is an SQL question mark string literal: '?'.

Categories