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