Skip to content

Commit e2dd58a

Browse files
author
Tomáš Kraus
committed
Issue helidon-io#2279 - Return generated IDs from INSERT statement.
Signed-off-by: Tomáš Kraus <tomas.kraus@oracle.com>
1 parent 9ee251c commit e2dd58a

36 files changed

Lines changed: 1034 additions & 96 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.dbclient;
17+
18+
import java.util.stream.Stream;
19+
20+
/**
21+
* Result of DML statement execution.
22+
*/
23+
public interface DbResultDml extends AutoCloseable {
24+
25+
/**
26+
* Retrieves any auto-generated keys created as a result of executing this DML statement.
27+
*
28+
* @return the auto-generated keys
29+
*/
30+
Stream<DbRow> generatedKeys();
31+
32+
/**
33+
* Retrieve statement execution result.
34+
*
35+
* @return row count for Data Manipulation Language (DML) statements or {@code 0}
36+
* for statements that return nothing.
37+
*/
38+
long result();
39+
40+
/**
41+
* Create new instance of DML statement execution result.
42+
*
43+
* @param generatedKeys the auto-generated keys
44+
* @param result the statement execution result
45+
* @return new instance of DML statement execution result
46+
*/
47+
static DbResultDml create(Stream<DbRow> generatedKeys, long result) {
48+
return new DbResultDmlImpl(generatedKeys, result);
49+
}
50+
51+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.dbclient;
17+
18+
import java.util.Objects;
19+
import java.util.stream.Stream;
20+
21+
// DbResultDml implementation
22+
record DbResultDmlImpl(Stream<DbRow> generatedKeys, long result) implements DbResultDml {
23+
24+
DbResultDmlImpl {
25+
Objects.requireNonNull(generatedKeys, "List of auto-generated keys value is null");
26+
if (result < 0) {
27+
throw new IllegalArgumentException("Statement execution result value is less than 0");
28+
}
29+
}
30+
31+
@Override
32+
public void close() throws Exception {
33+
generatedKeys.close();
34+
}
35+
36+
}

dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2019, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
1515
*/
1616
package io.helidon.dbclient;
1717

18+
import java.util.List;
19+
1820
/**
1921
* Data Manipulation Language (DML) database statement.
2022
* A DML statement modifies records in the database and returns the number of modified records.
@@ -24,7 +26,35 @@ public interface DbStatementDml extends DbStatement<DbStatementDml> {
2426
/**
2527
* Execute this statement using the parameters configured with {@code params} and {@code addParams} methods.
2628
*
27-
* @return The result of this statement.
29+
* @return the result of this statement
2830
*/
2931
long execute();
32+
33+
/**
34+
* Execute {@code INSERT} statement using the parameters configured with {@code params} and {@code addParams} methods
35+
* and return compound result with generated keys.
36+
*
37+
* @return the result of this statement with generated keys
38+
*/
39+
DbResultDml insert();
40+
41+
/**
42+
* Set auto-generated keys to be returned from the statement execution using {@link #insert()}.
43+
* Only one method from {@link #returnGeneratedKeys()} and {@link #returnColumns(List)} may be used.
44+
* This feature is database provider specific and some databases require specific columns to be set.
45+
*
46+
* @return updated db statement
47+
*/
48+
DbStatementDml returnGeneratedKeys();
49+
50+
/**
51+
* Set column names to be returned from the inserted row or rows from the statement execution using {@link #insert()}.
52+
* Only one method from {@link #returnGeneratedKeys()} and {@link #returnColumns(List)} may be used.
53+
* This feature is database provider specific.
54+
*
55+
* @param columnNames an array of column names indicating the columns that should be returned from the inserted row or rows
56+
* @return updated db statement
57+
*/
58+
DbStatementDml returnColumns(List<String> columnNames);
59+
3060
}

dbclient/jdbc/etc/spotbugs/exclude.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
3-
Copyright (c) 2019, 2023 Oracle and/or its affiliates.
3+
Copyright (c) 2019, 2024 Oracle and/or its affiliates.
44
55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -26,4 +26,16 @@
2626
<Method name="prepareStatement"/>
2727
<Bug pattern="SQL_INJECTION_JDBC"/>
2828
</Match>
29+
<Match>
30+
<!-- Doesn't construct SQL string. It converts string to statement -->
31+
<Class name="io.helidon.dbclient.jdbc.JdbcStatementDml"/>
32+
<Method name="prepareStatement"/>
33+
<Bug pattern="SQL_INJECTION_JDBC"/>
34+
</Match>
35+
<Match>
36+
<!-- Doesn't construct SQL string. It converts string to statement -->
37+
<Class name="io.helidon.dbclient.jdbc.JdbcTransactionStatementDml"/>
38+
<Method name="prepareStatement"/>
39+
<Bug pattern="SQL_INJECTION_JDBC"/>
40+
</Match>
2941
</FindBugsFilter>

dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,20 @@ private JdbcExecuteContext jdbcContext() {
7979
return context(JdbcExecuteContext.class);
8080
}
8181

82+
/**
83+
* Set the connection.
84+
*
85+
* @param connection the database connection
86+
*/
87+
protected void connection(Connection connection) {
88+
this.connection = connection;
89+
}
90+
8291
/**
8392
* Create the {@link PreparedStatement}.
8493
*
8594
* @param serviceContext client service context
86-
* @return PreparedStatement
95+
* @return new instance of {@link PreparedStatement}
8796
*/
8897
protected PreparedStatement prepareStatement(DbClientServiceContext serviceContext) {
8998
String stmtName = serviceContext.statementName();
@@ -105,7 +114,7 @@ protected PreparedStatement prepareStatement(DbClientServiceContext serviceConte
105114
*
106115
* @param stmtName statement name
107116
* @param stmt statement text
108-
* @return statement
117+
* @return new instance of {@link PreparedStatement}
109118
*/
110119
protected PreparedStatement prepareStatement(String stmtName, String stmt) {
111120
Connection connection = connectionPool.connection();
@@ -120,10 +129,10 @@ protected PreparedStatement prepareStatement(String stmtName, String stmt) {
120129
/**
121130
* Create the {@link PreparedStatement}.
122131
*
123-
* @param connection connection
132+
* @param connection the database connection
124133
* @param stmtName statement name
125134
* @param stmt statement text
126-
* @return statement
135+
* @return new instance of {@link PreparedStatement}
127136
*/
128137
protected PreparedStatement prepareStatement(Connection connection, String stmtName, String stmt) {
129138
try {

dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2019, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,11 +15,22 @@
1515
*/
1616
package io.helidon.dbclient.jdbc;
1717

18+
import java.sql.Connection;
1819
import java.sql.PreparedStatement;
20+
import java.sql.ResultSet;
1921
import java.sql.SQLException;
22+
import java.sql.Statement;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Objects;
2026
import java.util.concurrent.CompletableFuture;
27+
import java.util.stream.Stream;
28+
import java.util.stream.StreamSupport;
2129

30+
import io.helidon.dbclient.DbClientException;
2231
import io.helidon.dbclient.DbClientServiceContext;
32+
import io.helidon.dbclient.DbResultDml;
33+
import io.helidon.dbclient.DbRow;
2334
import io.helidon.dbclient.DbStatementDml;
2435
import io.helidon.dbclient.DbStatementException;
2536
import io.helidon.dbclient.DbStatementType;
@@ -29,7 +40,17 @@
2940
*/
3041
class JdbcStatementDml extends JdbcStatement<DbStatementDml> implements DbStatementDml {
3142

43+
static final String[] EMPTY_STRING_ARRAY = new String[0];
44+
3245
private final DbStatementType type;
46+
// Column names to be returned from the inserted row or rows from the statement execution.
47+
// Value of null (default) indicates no columns are set.
48+
private List<String> columnNames = List.of();
49+
// Whether PreparedStatement shall be created with Statement.RETURN_GENERATED_KEYS:
50+
// - value of false (default) indicates that autoGeneratedKeys won't be passed to PreparedStatement creation
51+
// - value of true indicates that Statement.RETURN_GENERATED_KEYS as autoGeneratedKeys will be passed
52+
// to PreparedStatement creation
53+
private boolean returnGeneratedKeys;
3354

3455
/**
3556
* Create a new instance.
@@ -58,6 +79,45 @@ public long execute() {
5879
});
5980
}
6081

82+
@Override
83+
public DbResultDml insert() {
84+
return doExecute((future, context) -> doInsert(this, future, context, this::closeConnection));
85+
}
86+
87+
@Override
88+
public DbStatementDml returnGeneratedKeys() {
89+
if (!columnNames.isEmpty()) {
90+
throw new IllegalStateException("Method returnColumns(String[]) was already called to set specific column names.");
91+
}
92+
returnGeneratedKeys = true;
93+
return this;
94+
}
95+
96+
@Override
97+
public DbStatementDml returnColumns(List<String> columnNames) {
98+
if (returnGeneratedKeys) {
99+
throw new IllegalStateException("Method returnGeneratedKeys() was already called.");
100+
}
101+
Objects.requireNonNull(columnNames, "List of column names value is null");
102+
this.columnNames = Collections.unmodifiableList(columnNames);
103+
return this;
104+
}
105+
106+
@Override
107+
protected PreparedStatement prepareStatement(Connection connection, String stmtName, String stmt) {
108+
try {
109+
connection(connection);
110+
if (returnGeneratedKeys) {
111+
return connection.prepareStatement(stmt, Statement.RETURN_GENERATED_KEYS);
112+
} else if (!columnNames.isEmpty()) {
113+
return connection.prepareStatement(stmt, columnNames.toArray(EMPTY_STRING_ARRAY));
114+
}
115+
return connection.prepareStatement(stmt);
116+
} catch (SQLException e) {
117+
throw new DbClientException(String.format("Failed to prepare statement: %s", stmtName), e);
118+
}
119+
}
120+
61121
/**
62122
* Execute the given statement.
63123
*
@@ -75,7 +135,41 @@ static long doExecute(JdbcStatement<? extends DbStatementDml> dbStmt,
75135
future.complete(result);
76136
return result;
77137
} catch (SQLException ex) {
138+
dbStmt.closeConnection();
78139
throw new DbStatementException("Failed to execute statement", dbStmt.context().statement(), ex);
79140
}
80141
}
142+
143+
/**
144+
* Execute the given insert statement.
145+
*
146+
* @param dbStmt db statement
147+
* @param future query future
148+
* @param context service context
149+
* @return query result
150+
*/
151+
static DbResultDml doInsert(JdbcStatement<? extends DbStatementDml> dbStmt,
152+
CompletableFuture<Long> future,
153+
DbClientServiceContext context,
154+
Runnable onClose) {
155+
PreparedStatement statement;
156+
try {
157+
statement = dbStmt.prepareStatement(context);
158+
long result = statement.executeUpdate();
159+
ResultSet rs = statement.getGeneratedKeys();
160+
JdbcRow.Spliterator spliterator = new JdbcRow.Spliterator(rs, statement, dbStmt.context(), future);
161+
Stream<DbRow> generatedKeys = autoClose(StreamSupport.stream(spliterator, false)
162+
.onClose(() -> {
163+
spliterator.close();
164+
if (onClose != null) {
165+
onClose.run();
166+
}
167+
}));
168+
return DbResultDml.create(generatedKeys, result);
169+
} catch (SQLException ex) {
170+
dbStmt.closeConnection();
171+
throw new DbStatementException("Failed to execute statement", dbStmt.context().statement(), ex);
172+
}
173+
}
174+
81175
}

dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2019, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -82,7 +82,7 @@ static Stream<DbRow> doExecute(JdbcStatement<? extends DbStatementQuery> dbStmt,
8282
}));
8383
} catch (SQLException ex) {
8484
dbStmt.closeConnection();
85-
throw new DbStatementException("Failed to create Statement", dbStmt.context().statement(), ex);
85+
throw new DbStatementException("Failed to execute Statement", dbStmt.context().statement(), ex);
8686
}
8787
}
8888
}

0 commit comments

Comments
 (0)