Skip to content

Commit 32afca1

Browse files
committed
Reinstate parameter per entity for batch deletes using EclipseLink.
EclipseLink doesn't support WHERE e IN (:entities) and requires e = ?1 OR e = ?2 OR … style. Closes #3983
1 parent 2bf3bb4 commit 32afca1

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.springframework.data.domain.Sort;
5151
import org.springframework.data.domain.Sort.Order;
5252
import org.springframework.data.jpa.domain.JpaSort.JpaOrder;
53+
import org.springframework.data.jpa.provider.PersistenceProvider;
5354
import org.springframework.data.mapping.PropertyPath;
5455
import org.springframework.data.util.Streamable;
5556
import org.springframework.lang.Nullable;
@@ -528,6 +529,21 @@ private static Integer findClose(final Integer open, final List<Integer> closes,
528529
* @return Guaranteed to be not {@literal null}.
529530
*/
530531
public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {
532+
return applyAndBind(queryString, entities, entityManager, PersistenceProvider.fromEntityManager(entityManager));
533+
}
534+
535+
/**
536+
* Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
537+
* entities to the query.
538+
*
539+
* @param <T> type of the entities.
540+
* @param queryString must not be {@literal null}.
541+
* @param entities must not be {@literal null}.
542+
* @param entityManager must not be {@literal null}.
543+
* @return Guaranteed to be not {@literal null}.
544+
*/
545+
static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager,
546+
PersistenceProvider persistenceProvider) {
531547

532548
Assert.notNull(queryString, "Querystring must not be null");
533549
Assert.notNull(entities, "Iterable of entities must not be null");
@@ -539,9 +555,46 @@ public static <T> Query applyAndBind(String queryString, Iterable<T> entities, E
539555
return entityManager.createQuery(queryString);
540556
}
541557

558+
if (persistenceProvider == PersistenceProvider.HIBERNATE) {
559+
560+
String alias = detectAlias(queryString);
561+
Query query = entityManager.createQuery("%s where %s IN (?1)".formatted(queryString, alias));
562+
query.setParameter(1, entities instanceof Collection<T> ? entities : Streamable.of(entities).toList());
563+
564+
return query;
565+
}
566+
567+
return applyWhereEqualsAndBind(queryString, entities, entityManager, iterator);
568+
}
569+
570+
private static Query applyWhereEqualsAndBind(String queryString, Iterable<?> entities, EntityManager entityManager,
571+
Iterator<?> iterator) {
572+
542573
String alias = detectAlias(queryString);
543-
Query query = entityManager.createQuery("%s where %s IN (?1)".formatted(queryString, alias));
544-
query.setParameter(1, entities instanceof Collection<T> ? entities : Streamable.of(entities).toList());
574+
StringBuilder builder = new StringBuilder(queryString);
575+
builder.append(" where");
576+
577+
int i = 0;
578+
579+
while (iterator.hasNext()) {
580+
581+
iterator.next();
582+
583+
builder.append(String.format(" %s = ?%d", alias, ++i));
584+
585+
if (iterator.hasNext()) {
586+
builder.append(" or");
587+
}
588+
}
589+
590+
Query query = entityManager.createQuery(builder.toString());
591+
592+
iterator = entities.iterator();
593+
i = 0;
594+
595+
while (iterator.hasNext()) {
596+
query.setParameter(++i, iterator.next());
597+
}
545598

546599
return query;
547600
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3583,7 +3583,7 @@ private interface UserProjectionInterfaceBased {
35833583
String getLastname();
35843584
}
35853585

3586-
record UserDto(Integer id, String firstname, String lastname, String emailAddress) {
3586+
public record UserDto(Integer id, String firstname, String lastname, String emailAddress) {
35873587

35883588
}
35893589

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@
2222
import jakarta.persistence.criteria.Path;
2323
import jakarta.persistence.criteria.Root;
2424

25+
import java.util.List;
26+
27+
import org.eclipse.persistence.internal.jpa.EJBQueryImpl;
28+
import org.junit.jupiter.api.Disabled;
2529
import org.junit.jupiter.api.Test;
2630

2731
import org.springframework.data.jpa.domain.sample.User;
2832
import org.springframework.data.mapping.PropertyPath;
2933
import org.springframework.test.context.ContextConfiguration;
34+
import org.springframework.transaction.annotation.Transactional;
3035

3136
/**
3237
* EclipseLink variant of {@link QueryUtilsIntegrationTests}.
@@ -63,4 +68,22 @@ void prefersFetchOverJoin() {
6368
assertThat(from.getJoins()).hasSize(1);
6469
}
6570

71+
@Test // GH-3983, GH-2870
72+
@Disabled("Not supported by EclipseLink")
73+
@Transactional
74+
@Override
75+
void applyAndBindOptimizesIn() {}
76+
77+
@Test // GH-3983, GH-2870
78+
@Transactional
79+
@Override
80+
void applyAndBindExpandsToPositionalPlaceholders() {
81+
82+
em.getCriteriaBuilder();
83+
EJBQueryImpl<?> query = (EJBQueryImpl) QueryUtils.applyAndBind("DELETE FROM User u",
84+
List.of(new User(), new User()), em.unwrap(null));
85+
86+
assertThat(query.getDatabaseQuery().getJPQLString()).isEqualTo("DELETE FROM User u where u = ?1 or u = ?2");
87+
}
88+
6689
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.function.Consumer;
4545
import java.util.stream.Collectors;
4646

47+
import org.hibernate.query.hql.spi.SqmQueryImplementor;
4748
import org.junit.jupiter.api.Test;
4849
import org.junit.jupiter.api.extension.ExtendWith;
4950
import org.mockito.Mockito;
@@ -59,6 +60,7 @@
5960
import org.springframework.data.mapping.PropertyPath;
6061
import org.springframework.test.context.ContextConfiguration;
6162
import org.springframework.test.context.junit.jupiter.SpringExtension;
63+
import org.springframework.transaction.annotation.Transactional;
6264

6365
/**
6466
* Integration tests for {@link QueryUtils}.
@@ -384,6 +386,29 @@ void demonstrateDifferentBehaviorOfGetJoin() {
384386
assertThat(root.getJoins()).hasSize(getNumberOfJoinsAfterCreatingAPath());
385387
}
386388

389+
@Test // GH-3983, GH-2870
390+
@Transactional
391+
void applyAndBindOptimizesIn() {
392+
393+
em.getCriteriaBuilder();
394+
SqmQueryImplementor<?> query = (SqmQueryImplementor) QueryUtils
395+
.applyAndBind("DELETE FROM User u", List.of(new User(), new User()), em.unwrap(null));
396+
397+
assertThat(query.getQueryString()).isEqualTo("DELETE FROM User u where u IN (?1)");
398+
}
399+
400+
@Test // GH-3983, GH-2870
401+
@Transactional
402+
void applyAndBindExpandsToPositionalPlaceholders() {
403+
404+
em.getCriteriaBuilder();
405+
SqmQueryImplementor<?> query = (SqmQueryImplementor) QueryUtils
406+
.applyAndBind("DELETE FROM User u", List.of(new User(), new User()), em.unwrap(null),
407+
org.springframework.data.jpa.provider.PersistenceProvider.ECLIPSELINK);
408+
409+
assertThat(query.getQueryString()).isEqualTo("DELETE FROM User u where u = ?1 or u = ?2");
410+
}
411+
387412
int getNumberOfJoinsAfterCreatingAPath() {
388413
return 0;
389414
}

0 commit comments

Comments
 (0)