Problem
The same @Query
statement works when using Hibernate as the JPA provider, but does not work when using EclipseLink.
Goal
- Understand why there is a different behavior.
- Figure out how to make the thing work for EclipseLink.
Context
I had another SO question that related to EclipseLink, and a kind stranger provided me with a git repository that seemed to solve my problem on his end. However, I could never get his (tested!) solution to work within my project, so I investigated further.
I found out that when you swap Hibernate for EclipseLink, the solution ends up spitting out this error:
Unexpected exception thrown:
org.springframework.dao.InvalidDataAccessApiUsageException:
You have attempted to set a value of type class java.util.ImmutableCollections$List12 for parameter ids with expected type of class com.stackoverflow.questions.entities.MyIdClass from query string DELETE FROM MyEntity me WHERE me.id in (:ids).
nested exception is java.lang.IllegalArgumentException:
You have attempted to set a value of type class java.util.ImmutableCollections$List12 for parameter ids with expected type of class com.stackoverflow.questions.entities.MyIdClass from query string DELETE FROM MyEntity me WHERE me.id in (:ids).
Configuration and dependencies
These are modifications that you can apply to the provided repository to reach the state that my question talks about. It's basically just how you can swap from Hibernate to EclipseLink.
Add this @Configuration
class:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories("com.stackoverflow.questions.repositories")
public class EclipseLinkConfig {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
EclipseLinkJpaVendorAdapter jpaVendorAdapter = new EclipseLinkJpaVendorAdapter();
jpaVendorAdapter.setDatabasePlatform("org.eclipse.persistence.platform.database.H2Platform");
jpaVendorAdapter.setGenerateDdl(Boolean.TRUE);
jpaVendorAdapter.setShowSql(true);
return jpaVendorAdapter;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
entityManagerFactoryBean.setJpaDialect(new EclipseLinkJpaDialect());
// Instead of persistence.xml
entityManagerFactoryBean.setPersistenceUnitName("yourPersistenceUnitName");
entityManagerFactoryBean.setPackagesToScan("com.stackoverflow.questions");
Properties jpaProperties = new Properties();
jpaProperties.put("eclipselink.weaving", "static");
// jpaProperties.put(PersistenceUnitProperties.CACHE_SHARED_DEFAULT, "false");
// jpaProperties.put(PersistenceUnitProperties.BATCH_WRITING, BatchWriting.JDBC);
// jpaProperties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "2");
jpaProperties.put("eclipselink.logging.level", "ALL");
jpaProperties.put("eclipselink.logging.level.cache", "ALL");
jpaProperties.put("eclipselink.logging.level.sql", "ALL");
jpaProperties.put("eclipselink.logging.parameters", "true");
entityManagerFactoryBean.setJpaProperties(jpaProperties);
entityManagerFactoryBean.afterPropertiesSet();
return entityManagerFactoryBean;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
And replace the build.gradle
content with:
plugins {
id 'org.springframework.boot' version '2.5.6'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.stackoverflow'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.eclipse.persistence:eclipselink:2.7.5'
def withoutHibernate = {
exclude group: 'org.hibernate', module: 'hibernate-entitymanager'
exclude group: 'org.hibernate', module: 'hibernate-core'
exclude group: 'org.hibernate.common', module: 'common-annotations'
}
implementation 'org.springframework.boot:spring-boot-starter-data-jpa', withoutHibernate
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
Code that I would expect to work from any JPA Provider
That is essentially the core of the solution that I'm just extracting for the provided git repo to make sure this question is "self-contained".
public interface MyEntityRepository extends JpaRepository<MyEntity, MyIdClass> {
@Modifying
@Query("DELETE FROM MyEntity me WHERE me.id in (:ids)")
void deleteByIdInWithQuery(@Param("ids") Collection<MyIdClass> ids);
}
@Entity
@Table(name = "myentity")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@IdClass(MyIdClass.class)
public class MyEntity {
@Id
@Column(updatable = false)
private String foo;
@Id
@Column(updatable = false)
private String bar;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "my_foreign_key", referencedColumnName = "external_pk")
private AnotherEntity anotherEntity;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "foo",
column = @Column(name = "foo", insertable = false, updatable = false)),
@AttributeOverride(name = "bar",
column = @Column(name = "bar", insertable = false, updatable = false))})
private MyIdClass id;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class MyIdClass implements Serializable {
private String foo;
private String bar;
}
@Entity
@Table(name = "anotherentity")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class AnotherEntity {
@Id
@Column(name = "external_pk", nullable = false, updatable = false)
private String externalPk;
}
I would expect a call to deleteByIdInWithQuery
to work from any JPA Provider. Any ideas why it doesn't work with EclipseLink?
In conclusion
Is this expected?
Or is this a bug? If so, does it come from EclipseLink, Spring-Data, or Hibernate?