0

Problem

The same @Query statement works when using Hibernate as the JPA provider, but does not work when using EclipseLink.

Goal

  1. Understand why there is a different behavior.
  2. 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?

payne
  • 4,691
  • 8
  • 37
  • 85
  • This is not supported by the JPA specification. The 'in_expression' defines the left side as "The state_valued_path_expression must have a string, numeric, date, time, timestamp, or enum value." The me.id is an embeddable, so doesn't fit that requirement. Providers are free to do more - you can request EclipseLink add support for this, but I know it requires detailed query reformatting in ways that aren't always straight forward and can impact performance – Chris Oct 29 '21 at 18:29
  • Interesting. Do you have any idea what is the suggested way to write a `bulk delete` query with EclipseLink, then? – payne Oct 29 '21 at 18:31
  • I haven't crossed this bridge: all my projects get by with validating the entity first, and the ones that haven't, used simple keys. I wouldn't delete it on a complex type using an IN statement, and instead do what those other providers that support this must be doing under the covers: break up that collection into multiple (x=:x1 and y=:y1) clauses when building the query dynamically. Or in a pinch, pass the list as a string to a stored proc to do the deletion, which would allow statement reuse. – Chris Oct 29 '21 at 18:53

0 Answers0