diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JpaInsertNativeHelper.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JpaInsertNativeHelper.java
new file mode 100644
index 0000000000..2a6cd528e8
--- /dev/null
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JpaInsertNativeHelper.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.querydsl.jpa;
+
+import com.querydsl.core.types.Expression;
+import com.querydsl.core.types.ParamExpression;
+import com.querydsl.core.types.ParamNotSetException;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.Param;
+import jakarta.persistence.Column;
+import jakarta.persistence.Table;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Helper for building native SQL INSERT statements from JPA entity metadata. Used by {@link
+ * com.querydsl.jpa.impl.JPAInsertClause} and {@link
+ * com.querydsl.jpa.hibernate.HibernateInsertClause} to support {@code executeWithKey()}.
+ *
+ *
This is an internal API and not intended for direct use by application code.
+ */
+public final class JpaInsertNativeHelper {
+
+ private JpaInsertNativeHelper() {}
+
+ /**
+ * Resolve the SQL table name for an entity class.
+ *
+ * @param entityClass the JPA entity class
+ * @return the SQL table name
+ */
+ public static String resolveTableName(Class> entityClass) {
+ if (entityClass.isAnnotationPresent(Table.class)) {
+ var table = entityClass.getAnnotation(Table.class);
+ if (!table.name().isEmpty()) {
+ var sb = new StringBuilder();
+ if (!table.schema().isEmpty()) {
+ sb.append(table.schema()).append('.');
+ }
+ sb.append(table.name());
+ return sb.toString();
+ }
+ }
+ return entityClass.getSimpleName();
+ }
+
+ /**
+ * Resolve the SQL column name for a path. Reads {@code @Column} annotation if present, otherwise
+ * falls back to the path metadata name.
+ *
+ * @param path the query path
+ * @return the SQL column name
+ */
+ public static String resolveColumnName(Path> path) {
+ if (path.getAnnotatedElement() != null
+ && path.getAnnotatedElement().isAnnotationPresent(Column.class)) {
+ var column = path.getAnnotatedElement().getAnnotation(Column.class);
+ if (!column.name().isEmpty()) {
+ return column.name();
+ }
+ }
+ return path.getMetadata().getName();
+ }
+
+ /**
+ * Build a native SQL INSERT statement from entity metadata and column paths.
+ *
+ * @param entityClass the entity class (for table name resolution)
+ * @param columns the columns to insert
+ * @return the native SQL INSERT string with positional parameters
+ */
+ public static String buildNativeInsertSQL(Class> entityClass, Collection> columns) {
+ var tableName = resolveTableName(entityClass);
+ var sb = new StringBuilder();
+ sb.append("INSERT INTO ").append(tableName).append(" (");
+
+ var first = true;
+ for (Path> col : columns) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(resolveColumnName(col));
+ first = false;
+ }
+
+ sb.append(") VALUES (");
+ first = true;
+ for (int i = 0; i < columns.size(); i++) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append('?');
+ first = false;
+ }
+ sb.append(')');
+
+ return sb.toString();
+ }
+
+ /**
+ * Resolve constant values from the serializer, unwrapping {@link Param} expressions.
+ *
+ * @param constants the constants from the serializer
+ * @param params the parameter bindings
+ * @return resolved values ready for JDBC binding
+ */
+ public static Object[] resolveConstants(
+ List constants, Map, Object> params) {
+ var result = new Object[constants.size()];
+ for (var i = 0; i < constants.size(); i++) {
+ var val = constants.get(i);
+ if (val instanceof Param> param) {
+ val = params.get(val);
+ if (val == null) {
+ throw new ParamNotSetException(param);
+ }
+ }
+ result[i] = val;
+ }
+ return result;
+ }
+
+ /**
+ * Execute a native SQL INSERT with RETURN_GENERATED_KEYS and return the generated key.
+ *
+ * @param the key type
+ * @param conn the JDBC connection (not closed by this method)
+ * @param sql the native SQL INSERT string
+ * @param params the parameter values to bind
+ * @param keyType the expected key type
+ * @return the generated key, or null if no rows were inserted
+ * @throws SQLException if a database error occurs
+ */
+ @Nullable
+ public static T executeAndReturnKey(
+ java.sql.Connection conn, String sql, Object[] params, Class keyType) throws SQLException {
+ try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
+ for (int i = 0; i < params.length; i++) {
+ stmt.setObject(i + 1, params[i]);
+ }
+ stmt.executeUpdate();
+
+ try (ResultSet rs = stmt.getGeneratedKeys()) {
+ if (rs.next()) {
+ return rs.getObject(1, keyType);
+ }
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Collect effective columns and values from either the set-style inserts map or the
+ * columns/values lists.
+ *
+ * @param inserts the set-style inserts (path to expression mapping)
+ * @param columns the columns list (from columns().values() style)
+ * @param values the values list
+ * @param serializer used to extract constant values from expressions
+ * @return the effective column paths
+ */
+ public static Collection> effectiveColumns(
+ Map, Expression>> inserts, List> columns) {
+ if (!inserts.isEmpty()) {
+ return inserts.keySet();
+ }
+ return columns;
+ }
+}
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java
index 4cb48e055a..fde50f51c0 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java
@@ -14,6 +14,7 @@
package com.querydsl.jpa.hibernate;
import org.hibernate.Session;
+import org.hibernate.jdbc.ReturningWork;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -39,4 +40,9 @@ public Query> createQuery(String queryString) {
public NativeQuery> createSQLQuery(String queryString) {
return session.createNativeQuery(queryString);
}
+
+ @Override
+ public T doReturningWork(ReturningWork work) {
+ return session.doReturningWork(work);
+ }
}
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java
index ee2b6801bc..b0e1d90f50 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java
@@ -14,6 +14,7 @@
package com.querydsl.jpa.hibernate;
import com.querydsl.core.JoinType;
+import com.querydsl.core.QueryException;
import com.querydsl.core.dml.InsertClause;
import com.querydsl.core.support.QueryMixin;
import com.querydsl.core.types.EntityPath;
@@ -25,6 +26,8 @@
import com.querydsl.jpa.JPAQueryMixin;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.JPQLTemplates;
+import com.querydsl.jpa.JpaInsertNativeHelper;
+import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -35,6 +38,7 @@
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.hibernate.query.Query;
+import org.jetbrains.annotations.Nullable;
/**
* UpdateClause implementation for Hibernate
@@ -97,6 +101,70 @@ public long execute() {
return query.executeUpdate();
}
+ /**
+ * Execute the clause and return the generated key with the type of the given path. If no rows
+ * were created, null is returned, otherwise the key of the first row is returned.
+ *
+ * This method bypasses JPQL and executes a native SQL INSERT via JDBC to retrieve the
+ * generated key using Hibernate's {@code Session.doReturningWork()}.
+ *
+ *
Note: {@code INSERT ... SELECT} subqueries are not supported by this method.
+ *
+ * @param key type
+ * @param path path for key (used to determine return type)
+ * @return generated key, or null if no rows were created
+ * @throws QueryException if a database error occurs
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public T executeWithKey(Path path) {
+ return executeWithKey((Class) path.getType());
+ }
+
+ /**
+ * Execute the clause and return the generated key cast to the given type. If no rows were
+ * created, null is returned, otherwise the key of the first row is returned.
+ *
+ * @param key type
+ * @param type class of the key type
+ * @return generated key, or null if no rows were created
+ * @throws QueryException if a database error occurs
+ */
+ @Nullable
+ public T executeWithKey(Class type) {
+ if (subQuery != null) {
+ throw new UnsupportedOperationException(
+ "executeWithKey is not supported for INSERT ... SELECT subqueries");
+ }
+
+ var effectiveColumns = JpaInsertNativeHelper.effectiveColumns(inserts, columns);
+ if (effectiveColumns.isEmpty()) {
+ throw new IllegalStateException("No columns specified for insert");
+ }
+
+ var entityClass = queryMixin.getMetadata().getJoins().get(0).getTarget().getType();
+
+ // Serialize to collect constant values
+ var serializer = new JPQLSerializer(templates, null);
+ serializer.serializeForInsert(
+ queryMixin.getMetadata(), effectiveColumns, values, subQuery, inserts);
+
+ var params =
+ JpaInsertNativeHelper.resolveConstants(
+ serializer.getConstants(), queryMixin.getMetadata().getParams());
+
+ var sql = JpaInsertNativeHelper.buildNativeInsertSQL(entityClass, effectiveColumns);
+
+ return session.doReturningWork(
+ connection -> {
+ try {
+ return JpaInsertNativeHelper.executeAndReturnKey(connection, sql, params, type);
+ } catch (SQLException e) {
+ throw new QueryException("Failed to execute insert with generated key", e);
+ }
+ });
+ }
+
@Override
public HibernateInsertClause columns(Path>... columns) {
this.columns.addAll(Arrays.asList(columns));
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/NoSessionHolder.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/NoSessionHolder.java
index 8df92b7e7e..1215694209 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/NoSessionHolder.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/NoSessionHolder.java
@@ -13,6 +13,7 @@
*/
package com.querydsl.jpa.hibernate;
+import org.hibernate.jdbc.ReturningWork;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -36,4 +37,9 @@ public Query> createQuery(String queryString) {
public NativeQuery> createSQLQuery(String queryString) {
throw new UnsupportedOperationException("No session in detached Query available");
}
+
+ @Override
+ public T doReturningWork(ReturningWork work) {
+ throw new UnsupportedOperationException("No session in detached Query available");
+ }
}
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/SessionHolder.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/SessionHolder.java
index ecfd9b5999..df526c1d02 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/SessionHolder.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/SessionHolder.java
@@ -13,6 +13,7 @@
*/
package com.querydsl.jpa.hibernate;
+import org.hibernate.jdbc.ReturningWork;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -38,4 +39,13 @@ public interface SessionHolder {
* @return query
*/
NativeQuery> createSQLQuery(String queryString);
+
+ /**
+ * Execute a {@link ReturningWork} within the session's transaction context.
+ *
+ * @param the return type
+ * @param work the work to execute with a JDBC Connection
+ * @return the result of the work
+ */
+ T doReturningWork(ReturningWork work);
}
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/StatelessSessionHolder.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/StatelessSessionHolder.java
index d94c0c6d29..7e11188d2d 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/StatelessSessionHolder.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/StatelessSessionHolder.java
@@ -14,6 +14,7 @@
package com.querydsl.jpa.hibernate;
import org.hibernate.StatelessSession;
+import org.hibernate.jdbc.ReturningWork;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -41,4 +42,9 @@ public Query> createQuery(String queryString) {
public NativeQuery> createSQLQuery(String queryString) {
return session.createNativeQuery(queryString);
}
+
+ @Override
+ public T doReturningWork(ReturningWork work) {
+ return session.doReturningWork(work);
+ }
}
diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/JPAInsertClause.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/JPAInsertClause.java
index def564d7d9..e2e625959c 100644
--- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/JPAInsertClause.java
+++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/JPAInsertClause.java
@@ -14,6 +14,7 @@
package com.querydsl.jpa.impl;
import com.querydsl.core.JoinType;
+import com.querydsl.core.QueryException;
import com.querydsl.core.dml.InsertClause;
import com.querydsl.core.support.QueryMixin;
import com.querydsl.core.types.EntityPath;
@@ -24,6 +25,7 @@
import com.querydsl.jpa.JPAQueryMixin;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.JPQLTemplates;
+import com.querydsl.jpa.JpaInsertNativeHelper;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import java.util.ArrayList;
@@ -84,6 +86,72 @@ public long execute() {
return query.executeUpdate();
}
+ /**
+ * Execute the clause and return the generated key with the type of the given path. If no rows
+ * were created, null is returned, otherwise the key of the first row is returned.
+ *
+ * This method bypasses JPQL and executes a native SQL INSERT via JDBC to retrieve the
+ * generated key. It requires that the JPA provider supports {@code
+ * EntityManager.unwrap(Connection.class)}.
+ *
+ *
Note: {@code INSERT ... SELECT} subqueries are not supported by this method.
+ *
+ * @param key type
+ * @param path path for key (used to determine return type)
+ * @return generated key, or null if no rows were created
+ * @throws QueryException if a database error occurs or the operation is not supported
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public T executeWithKey(Path path) {
+ return executeWithKey((Class) path.getType());
+ }
+
+ /**
+ * Execute the clause and return the generated key cast to the given type. If no rows were
+ * created, null is returned, otherwise the key of the first row is returned.
+ *
+ * @param key type
+ * @param type class of the key type
+ * @return generated key, or null if no rows were created
+ * @throws QueryException if a database error occurs or the operation is not supported
+ */
+ @Nullable
+ public T executeWithKey(Class type) {
+ if (subQuery != null) {
+ throw new UnsupportedOperationException(
+ "executeWithKey is not supported for INSERT ... SELECT subqueries");
+ }
+
+ var effectiveColumns = JpaInsertNativeHelper.effectiveColumns(inserts, columns);
+ if (effectiveColumns.isEmpty()) {
+ throw new IllegalStateException("No columns specified for insert");
+ }
+
+ var entityClass = queryMixin.getMetadata().getJoins().get(0).getTarget().getType();
+
+ // Serialize to collect constant values
+ var serializer = new JPQLSerializer(templates, entityManager);
+ serializer.serializeForInsert(
+ queryMixin.getMetadata(), effectiveColumns, values, subQuery, inserts);
+
+ var params =
+ JpaInsertNativeHelper.resolveConstants(
+ serializer.getConstants(), queryMixin.getMetadata().getParams());
+
+ var sql = JpaInsertNativeHelper.buildNativeInsertSQL(entityClass, effectiveColumns);
+
+ try {
+ return entityManager
+ .unwrap(org.hibernate.Session.class)
+ .doReturningWork(
+ connection ->
+ JpaInsertNativeHelper.executeAndReturnKey(connection, sql, params, type));
+ } catch (Exception e) {
+ throw new QueryException("Failed to execute insert with generated key", e);
+ }
+ }
+
public JPAInsertClause setLockMode(LockModeType lockMode) {
this.lockMode = lockMode;
return this;
diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/DummySessionHolder.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/DummySessionHolder.java
index f44f4e9ad6..07703e7ef7 100644
--- a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/DummySessionHolder.java
+++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/DummySessionHolder.java
@@ -14,6 +14,7 @@
package com.querydsl.jpa;
import com.querydsl.jpa.hibernate.SessionHolder;
+import org.hibernate.jdbc.ReturningWork;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -28,4 +29,9 @@ public Query> createQuery(String queryString) {
public NativeQuery> createSQLQuery(String queryString) {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public T doReturningWork(ReturningWork work) {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateExecuteWithKeyTest.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateExecuteWithKeyTest.java
new file mode 100644
index 0000000000..5b91de7c46
--- /dev/null
+++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateExecuteWithKeyTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.querydsl.jpa;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.querydsl.core.types.EntityPath;
+import com.querydsl.jpa.domain.GeneratedKeyEntity;
+import com.querydsl.jpa.domain.QGeneratedKeyEntity;
+import com.querydsl.jpa.hibernate.HibernateInsertClause;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.Transaction;
+import org.hibernate.cfg.Configuration;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class HibernateExecuteWithKeyTest {
+
+ private static SessionFactory sessionFactory;
+ private Session session;
+ private Transaction tx;
+
+ @BeforeClass
+ public static void setUpClass() {
+ var cfg = new Configuration();
+ cfg.addAnnotatedClass(GeneratedKeyEntity.class);
+ cfg.setProperty("hibernate.connection.driver_class", "org.h2.Driver");
+ cfg.setProperty("hibernate.connection.url", "jdbc:h2:mem:hib_ewk_test;DB_CLOSE_DELAY=-1");
+ cfg.setProperty("hibernate.connection.username", "sa");
+ cfg.setProperty("hibernate.connection.password", "");
+ cfg.setProperty("hibernate.hbm2ddl.auto", "create-drop");
+ cfg.setProperty("hibernate.show_sql", "false");
+ sessionFactory = cfg.buildSessionFactory();
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ if (sessionFactory != null) {
+ sessionFactory.close();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ session = sessionFactory.openSession();
+ tx = session.beginTransaction();
+ }
+
+ @After
+ public void tearDown() {
+ if (tx != null && tx.isActive()) {
+ tx.rollback();
+ }
+ if (session != null) {
+ session.close();
+ }
+ }
+
+ private HibernateInsertClause insert(EntityPath> entity) {
+ return new HibernateInsertClause(session, entity);
+ }
+
+ @Test
+ public void executeWithKey_set_style() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).set(entity.name, "TestName").executeWithKey(entity.id);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_columns_values_style() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).columns(entity.name).values("TestName2").executeWithKey(entity.id);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_with_class_type() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).set(entity.name, "TestName3").executeWithKey(Long.class);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_multiple_inserts_return_different_keys() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id1 = insert(entity).set(entity.name, "Name1").executeWithKey(entity.id);
+ Long id2 = insert(entity).set(entity.name, "Name2").executeWithKey(entity.id);
+
+ assertThat(id1).isNotNull();
+ assertThat(id2).isNotNull();
+ assertThat(id2).isGreaterThan(id1);
+ }
+
+ @Test
+ public void executeWithKey_with_column_annotation() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).set(entity.name, "ColumnTest").executeWithKey(entity.id);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_rejects_subquery() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ var other = new QGeneratedKeyEntity("other");
+
+ assertThatThrownBy(
+ () ->
+ insert(entity)
+ .columns(entity.name)
+ .select(JPAExpressions.select(other.name).from(other))
+ .executeWithKey(entity.id))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+}
diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPAExecuteWithKeyTest.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPAExecuteWithKeyTest.java
new file mode 100644
index 0000000000..9721a2bdb3
--- /dev/null
+++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPAExecuteWithKeyTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.querydsl.jpa;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.querydsl.core.types.EntityPath;
+import com.querydsl.jpa.domain.QGeneratedKeyEntity;
+import com.querydsl.jpa.impl.JPAInsertClause;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.EntityTransaction;
+import jakarta.persistence.Persistence;
+import java.util.Map;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class JPAExecuteWithKeyTest {
+
+ private static EntityManagerFactory emf;
+ private EntityManager entityManager;
+ private EntityTransaction tx;
+
+ @BeforeClass
+ public static void setUpClass() {
+ emf =
+ Persistence.createEntityManagerFactory(
+ "executeWithKeyTest",
+ Map.of(
+ "jakarta.persistence.jdbc.driver", "org.h2.Driver",
+ "jakarta.persistence.jdbc.url", "jdbc:h2:mem:jpa_ewk_test;DB_CLOSE_DELAY=-1",
+ "jakarta.persistence.jdbc.user", "sa",
+ "jakarta.persistence.jdbc.password", "",
+ "hibernate.hbm2ddl.auto", "create-drop",
+ "hibernate.show_sql", "false"));
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ if (emf != null) {
+ emf.close();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ entityManager = emf.createEntityManager();
+ tx = entityManager.getTransaction();
+ tx.begin();
+ }
+
+ @After
+ public void tearDown() {
+ if (tx != null && tx.isActive()) {
+ tx.rollback();
+ }
+ if (entityManager != null) {
+ entityManager.close();
+ }
+ }
+
+ private JPAInsertClause insert(EntityPath> entity) {
+ return new JPAInsertClause(entityManager, entity);
+ }
+
+ @Test
+ public void executeWithKey_set_style() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).set(entity.name, "TestName").executeWithKey(entity.id);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_columns_values_style() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).columns(entity.name).values("TestName2").executeWithKey(entity.id);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_with_class_type() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id = insert(entity).set(entity.name, "TestName3").executeWithKey(Long.class);
+
+ assertThat(id).isNotNull();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ public void executeWithKey_multiple_inserts_return_different_keys() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ Long id1 = insert(entity).set(entity.name, "Name1").executeWithKey(entity.id);
+ Long id2 = insert(entity).set(entity.name, "Name2").executeWithKey(entity.id);
+
+ assertThat(id1).isNotNull();
+ assertThat(id2).isNotNull();
+ assertThat(id2).isGreaterThan(id1);
+ }
+
+ @Test
+ public void executeWithKey_rejects_subquery() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ var other = new QGeneratedKeyEntity("other");
+
+ assertThatThrownBy(
+ () ->
+ insert(entity)
+ .columns(entity.name)
+ .select(JPAExpressions.select(other.name).from(other))
+ .executeWithKey(entity.id))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+}
diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JpaInsertNativeHelperTest.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JpaInsertNativeHelperTest.java
new file mode 100644
index 0000000000..61157b58fa
--- /dev/null
+++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JpaInsertNativeHelperTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.querydsl.jpa;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.querydsl.jpa.domain.Author;
+import com.querydsl.jpa.domain.GeneratedKeyEntity;
+import com.querydsl.jpa.domain.Numeric;
+import com.querydsl.jpa.domain.QAuthor;
+import com.querydsl.jpa.domain.QGeneratedKeyEntity;
+import com.querydsl.jpa.domain.QNumeric;
+import java.util.List;
+import org.junit.Test;
+
+public class JpaInsertNativeHelperTest {
+
+ @Test
+ public void resolveTableName_with_table_annotation() {
+ // Author has @Table(name = "author_")
+ assertThat(JpaInsertNativeHelper.resolveTableName(Author.class)).isEqualTo("author_");
+ }
+
+ @Test
+ public void resolveTableName_with_generated_key_entity() {
+ // GeneratedKeyEntity has @Table(name = "generated_key_entity")
+ assertThat(JpaInsertNativeHelper.resolveTableName(GeneratedKeyEntity.class))
+ .isEqualTo("generated_key_entity");
+ }
+
+ @Test
+ public void resolveColumnName_with_column_annotation() {
+ // Numeric.value has @Column(name = "value_")
+ var numeric = QNumeric.numeric;
+ assertThat(JpaInsertNativeHelper.resolveColumnName(numeric.value)).isEqualTo("value_");
+ }
+
+ @Test
+ public void resolveColumnName_with_name_column() {
+ // GeneratedKeyEntity.name has @Column(name = "name_")
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ assertThat(JpaInsertNativeHelper.resolveColumnName(entity.name)).isEqualTo("name_");
+ }
+
+ @Test
+ public void resolveColumnName_without_column_annotation() {
+ // Author.name has no @Column annotation
+ var author = QAuthor.author;
+ assertThat(JpaInsertNativeHelper.resolveColumnName(author.name)).isEqualTo("name");
+ }
+
+ @Test
+ public void buildNativeInsertSQL() {
+ var entity = QGeneratedKeyEntity.generatedKeyEntity;
+ var sql =
+ JpaInsertNativeHelper.buildNativeInsertSQL(GeneratedKeyEntity.class, List.of(entity.name));
+
+ assertThat(sql).isEqualTo("INSERT INTO generated_key_entity (name_) VALUES (?)");
+ }
+
+ @Test
+ public void buildNativeInsertSQL_multiple_columns() {
+ var numeric = QNumeric.numeric;
+ var sql = JpaInsertNativeHelper.buildNativeInsertSQL(Numeric.class, List.of(numeric.value));
+
+ assertThat(sql).isEqualTo("INSERT INTO numeric_ (value_) VALUES (?)");
+ }
+}
diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/domain/GeneratedKeyEntity.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/domain/GeneratedKeyEntity.java
new file mode 100644
index 0000000000..a616e374fd
--- /dev/null
+++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/domain/GeneratedKeyEntity.java
@@ -0,0 +1,40 @@
+package com.querydsl.jpa.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.io.Serial;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "generated_key_entity")
+public class GeneratedKeyEntity implements Serializable {
+
+ @Serial private static final long serialVersionUID = 1L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "name_")
+ private String name;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/querydsl-libraries/querydsl-jpa/src/test/resources/META-INF/persistence.xml b/querydsl-libraries/querydsl-jpa/src/test/resources/META-INF/persistence.xml
index 60ca31c002..06c5f59c47 100644
--- a/querydsl-libraries/querydsl-jpa/src/test/resources/META-INF/persistence.xml
+++ b/querydsl-libraries/querydsl-jpa/src/test/resources/META-INF/persistence.xml
@@ -198,6 +198,23 @@
+
+
+
+ org.hibernate.jpa.HibernatePersistenceProvider
+ com.querydsl.jpa.domain.GeneratedKeyEntity
+ true
+
+
+
+
+
+
+
+
+
+
+
org.hibernate.jpa.HibernatePersistenceProvider