Skip to content

Commit 2308d3e

Browse files
author
이승영
committed
Add executeWithKey() to JPAInsertClause and HibernateInsertClause
Closes #1692
1 parent c6e8fe8 commit 2308d3e

File tree

13 files changed

+763
-0
lines changed

13 files changed

+763
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
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+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package com.querydsl.jpa;
15+
16+
import com.querydsl.core.types.Expression;
17+
import com.querydsl.core.types.ParamExpression;
18+
import com.querydsl.core.types.ParamNotSetException;
19+
import com.querydsl.core.types.Path;
20+
import com.querydsl.core.types.dsl.Param;
21+
import jakarta.persistence.Column;
22+
import jakarta.persistence.Table;
23+
import java.sql.PreparedStatement;
24+
import java.sql.ResultSet;
25+
import java.sql.SQLException;
26+
import java.sql.Statement;
27+
import java.util.Collection;
28+
import java.util.List;
29+
import java.util.Map;
30+
import org.jetbrains.annotations.Nullable;
31+
32+
/**
33+
* Helper for building native SQL INSERT statements from JPA entity metadata. Used by {@link
34+
* com.querydsl.jpa.impl.JPAInsertClause} and {@link
35+
* com.querydsl.jpa.hibernate.HibernateInsertClause} to support {@code executeWithKey()}.
36+
*
37+
* <p>This is an internal API and not intended for direct use by application code.
38+
*/
39+
public final class JpaInsertNativeHelper {
40+
41+
private JpaInsertNativeHelper() {}
42+
43+
/**
44+
* Resolve the SQL table name for an entity class.
45+
*
46+
* @param entityClass the JPA entity class
47+
* @return the SQL table name
48+
*/
49+
public static String resolveTableName(Class<?> entityClass) {
50+
if (entityClass.isAnnotationPresent(Table.class)) {
51+
var table = entityClass.getAnnotation(Table.class);
52+
if (!table.name().isEmpty()) {
53+
var sb = new StringBuilder();
54+
if (!table.schema().isEmpty()) {
55+
sb.append(table.schema()).append('.');
56+
}
57+
sb.append(table.name());
58+
return sb.toString();
59+
}
60+
}
61+
return entityClass.getSimpleName();
62+
}
63+
64+
/**
65+
* Resolve the SQL column name for a path. Reads {@code @Column} annotation if present, otherwise
66+
* falls back to the path metadata name.
67+
*
68+
* @param path the query path
69+
* @return the SQL column name
70+
*/
71+
public static String resolveColumnName(Path<?> path) {
72+
if (path.getAnnotatedElement() != null
73+
&& path.getAnnotatedElement().isAnnotationPresent(Column.class)) {
74+
var column = path.getAnnotatedElement().getAnnotation(Column.class);
75+
if (!column.name().isEmpty()) {
76+
return column.name();
77+
}
78+
}
79+
return path.getMetadata().getName();
80+
}
81+
82+
/**
83+
* Build a native SQL INSERT statement from entity metadata and column paths.
84+
*
85+
* @param entityClass the entity class (for table name resolution)
86+
* @param columns the columns to insert
87+
* @return the native SQL INSERT string with positional parameters
88+
*/
89+
public static String buildNativeInsertSQL(Class<?> entityClass, Collection<Path<?>> columns) {
90+
var tableName = resolveTableName(entityClass);
91+
var sb = new StringBuilder();
92+
sb.append("INSERT INTO ").append(tableName).append(" (");
93+
94+
var first = true;
95+
for (Path<?> col : columns) {
96+
if (!first) {
97+
sb.append(", ");
98+
}
99+
sb.append(resolveColumnName(col));
100+
first = false;
101+
}
102+
103+
sb.append(") VALUES (");
104+
first = true;
105+
for (int i = 0; i < columns.size(); i++) {
106+
if (!first) {
107+
sb.append(", ");
108+
}
109+
sb.append('?');
110+
first = false;
111+
}
112+
sb.append(')');
113+
114+
return sb.toString();
115+
}
116+
117+
/**
118+
* Resolve constant values from the serializer, unwrapping {@link Param} expressions.
119+
*
120+
* @param constants the constants from the serializer
121+
* @param params the parameter bindings
122+
* @return resolved values ready for JDBC binding
123+
*/
124+
public static Object[] resolveConstants(
125+
List<Object> constants, Map<ParamExpression<?>, Object> params) {
126+
var result = new Object[constants.size()];
127+
for (var i = 0; i < constants.size(); i++) {
128+
var val = constants.get(i);
129+
if (val instanceof Param<?> param) {
130+
val = params.get(val);
131+
if (val == null) {
132+
throw new ParamNotSetException(param);
133+
}
134+
}
135+
result[i] = val;
136+
}
137+
return result;
138+
}
139+
140+
/**
141+
* Execute a native SQL INSERT with RETURN_GENERATED_KEYS and return the generated key.
142+
*
143+
* @param <T> the key type
144+
* @param conn the JDBC connection (not closed by this method)
145+
* @param sql the native SQL INSERT string
146+
* @param params the parameter values to bind
147+
* @param keyType the expected key type
148+
* @return the generated key, or null if no rows were inserted
149+
* @throws SQLException if a database error occurs
150+
*/
151+
@Nullable
152+
public static <T> T executeAndReturnKey(
153+
java.sql.Connection conn, String sql, Object[] params, Class<T> keyType) throws SQLException {
154+
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
155+
for (int i = 0; i < params.length; i++) {
156+
stmt.setObject(i + 1, params[i]);
157+
}
158+
stmt.executeUpdate();
159+
160+
try (ResultSet rs = stmt.getGeneratedKeys()) {
161+
if (rs.next()) {
162+
return rs.getObject(1, keyType);
163+
}
164+
return null;
165+
}
166+
}
167+
}
168+
169+
/**
170+
* Collect effective columns and values from either the set-style inserts map or the
171+
* columns/values lists.
172+
*
173+
* @param inserts the set-style inserts (path to expression mapping)
174+
* @param columns the columns list (from columns().values() style)
175+
* @param values the values list
176+
* @param serializer used to extract constant values from expressions
177+
* @return the effective column paths
178+
*/
179+
public static Collection<Path<?>> effectiveColumns(
180+
Map<Path<?>, Expression<?>> inserts, List<Path<?>> columns) {
181+
if (!inserts.isEmpty()) {
182+
return inserts.keySet();
183+
}
184+
return columns;
185+
}
186+
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package com.querydsl.jpa.hibernate;
1515

1616
import org.hibernate.Session;
17+
import org.hibernate.jdbc.ReturningWork;
1718
import org.hibernate.query.NativeQuery;
1819
import org.hibernate.query.Query;
1920

@@ -39,4 +40,9 @@ public Query<?> createQuery(String queryString) {
3940
public NativeQuery<?> createSQLQuery(String queryString) {
4041
return session.createNativeQuery(queryString);
4142
}
43+
44+
@Override
45+
public <T> T doReturningWork(ReturningWork<T> work) {
46+
return session.doReturningWork(work);
47+
}
4248
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package com.querydsl.jpa.hibernate;
1515

1616
import com.querydsl.core.JoinType;
17+
import com.querydsl.core.QueryException;
1718
import com.querydsl.core.dml.InsertClause;
1819
import com.querydsl.core.support.QueryMixin;
1920
import com.querydsl.core.types.EntityPath;
@@ -25,6 +26,8 @@
2526
import com.querydsl.jpa.JPAQueryMixin;
2627
import com.querydsl.jpa.JPQLSerializer;
2728
import com.querydsl.jpa.JPQLTemplates;
29+
import com.querydsl.jpa.JpaInsertNativeHelper;
30+
import java.sql.SQLException;
2831
import java.util.ArrayList;
2932
import java.util.Arrays;
3033
import java.util.HashMap;
@@ -35,6 +38,7 @@
3538
import org.hibernate.Session;
3639
import org.hibernate.StatelessSession;
3740
import org.hibernate.query.Query;
41+
import org.jetbrains.annotations.Nullable;
3842

3943
/**
4044
* UpdateClause implementation for Hibernate
@@ -97,6 +101,70 @@ public long execute() {
97101
return query.executeUpdate();
98102
}
99103

104+
/**
105+
* Execute the clause and return the generated key with the type of the given path. If no rows
106+
* were created, null is returned, otherwise the key of the first row is returned.
107+
*
108+
* <p>This method bypasses JPQL and executes a native SQL INSERT via JDBC to retrieve the
109+
* generated key using Hibernate's {@code Session.doReturningWork()}.
110+
*
111+
* <p>Note: {@code INSERT ... SELECT} subqueries are not supported by this method.
112+
*
113+
* @param <T> key type
114+
* @param path path for key (used to determine return type)
115+
* @return generated key, or null if no rows were created
116+
* @throws QueryException if a database error occurs
117+
*/
118+
@SuppressWarnings("unchecked")
119+
@Nullable
120+
public <T> T executeWithKey(Path<T> path) {
121+
return executeWithKey((Class<T>) path.getType());
122+
}
123+
124+
/**
125+
* Execute the clause and return the generated key cast to the given type. If no rows were
126+
* created, null is returned, otherwise the key of the first row is returned.
127+
*
128+
* @param <T> key type
129+
* @param type class of the key type
130+
* @return generated key, or null if no rows were created
131+
* @throws QueryException if a database error occurs
132+
*/
133+
@Nullable
134+
public <T> T executeWithKey(Class<T> type) {
135+
if (subQuery != null) {
136+
throw new UnsupportedOperationException(
137+
"executeWithKey is not supported for INSERT ... SELECT subqueries");
138+
}
139+
140+
var effectiveColumns = JpaInsertNativeHelper.effectiveColumns(inserts, columns);
141+
if (effectiveColumns.isEmpty()) {
142+
throw new IllegalStateException("No columns specified for insert");
143+
}
144+
145+
var entityClass = queryMixin.getMetadata().getJoins().get(0).getTarget().getType();
146+
147+
// Serialize to collect constant values
148+
var serializer = new JPQLSerializer(templates, null);
149+
serializer.serializeForInsert(
150+
queryMixin.getMetadata(), effectiveColumns, values, subQuery, inserts);
151+
152+
var params =
153+
JpaInsertNativeHelper.resolveConstants(
154+
serializer.getConstants(), queryMixin.getMetadata().getParams());
155+
156+
var sql = JpaInsertNativeHelper.buildNativeInsertSQL(entityClass, effectiveColumns);
157+
158+
return session.doReturningWork(
159+
connection -> {
160+
try {
161+
return JpaInsertNativeHelper.executeAndReturnKey(connection, sql, params, type);
162+
} catch (SQLException e) {
163+
throw new QueryException("Failed to execute insert with generated key", e);
164+
}
165+
});
166+
}
167+
100168
@Override
101169
public HibernateInsertClause columns(Path<?>... columns) {
102170
this.columns.addAll(Arrays.asList(columns));

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/NoSessionHolder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package com.querydsl.jpa.hibernate;
1515

16+
import org.hibernate.jdbc.ReturningWork;
1617
import org.hibernate.query.NativeQuery;
1718
import org.hibernate.query.Query;
1819

@@ -36,4 +37,9 @@ public Query<?> createQuery(String queryString) {
3637
public NativeQuery<?> createSQLQuery(String queryString) {
3738
throw new UnsupportedOperationException("No session in detached Query available");
3839
}
40+
41+
@Override
42+
public <T> T doReturningWork(ReturningWork<T> work) {
43+
throw new UnsupportedOperationException("No session in detached Query available");
44+
}
3945
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/SessionHolder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package com.querydsl.jpa.hibernate;
1515

16+
import org.hibernate.jdbc.ReturningWork;
1617
import org.hibernate.query.NativeQuery;
1718
import org.hibernate.query.Query;
1819

@@ -38,4 +39,13 @@ public interface SessionHolder {
3839
* @return query
3940
*/
4041
NativeQuery<?> createSQLQuery(String queryString);
42+
43+
/**
44+
* Execute a {@link ReturningWork} within the session's transaction context.
45+
*
46+
* @param <T> the return type
47+
* @param work the work to execute with a JDBC Connection
48+
* @return the result of the work
49+
*/
50+
<T> T doReturningWork(ReturningWork<T> work);
4151
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/StatelessSessionHolder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package com.querydsl.jpa.hibernate;
1515

1616
import org.hibernate.StatelessSession;
17+
import org.hibernate.jdbc.ReturningWork;
1718
import org.hibernate.query.NativeQuery;
1819
import org.hibernate.query.Query;
1920

@@ -41,4 +42,9 @@ public Query<?> createQuery(String queryString) {
4142
public NativeQuery<?> createSQLQuery(String queryString) {
4243
return session.createNativeQuery(queryString);
4344
}
45+
46+
@Override
47+
public <T> T doReturningWork(ReturningWork<T> work) {
48+
return session.doReturningWork(work);
49+
}
4450
}

0 commit comments

Comments
 (0)