简体   繁体   中英

JPA Query to deleteById for a Composite Key declared using IdClass

Question

If I have declared my (composite) primary key using @IdClass , how do I write my @Query to be able to issue a DELETE query using a Collection<MyIdClass> ?

Secondary question

Will the CASCADE actually trigger the deletion of the associated AnotherEntity despite using @Query ?

Current model

@Entity
@Table(name = "myentity")
@JsonIgnoreProperties(ignoreUnknown = true)
@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;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MyIdClass implements Serializable {

    private String foo;
    private String bar;
}
@Entity
@Table(name = "anotherentity")
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class AnotherEntity {

    @Id
    @Column(name = "external_pk", nullable = false, updatable = false)
    private String externalPk;
}

What I've read

A few resources:

  1. https://www.baeldung.com/spring-data-jpa-query
  2. https://www.baeldung.com/spring-data-jpa-delete
  3. https://stackoverflow.com/a/36765129/9768291

And I also found this SO question which seemed very close to what I'm looking for, but unfortunately there are no answers.

Goal

Something similar to:

@Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {

    @Modifying
    @Query("DELETE FROM myentity m WHERE m IN ?1") // how do I write this?
    void deleteAllWithIds(Collection<MyIdClass> ids);
}

Ultimately, I want to do this to batch my DELETE requests to increase the performance.

Pitfalls I'm trying to avoid

I know there is a deleteAll(Iterable<? extends MyEntity>) but then I need to actually have those entities to begin with, which would require extra calls to the DB.

There is also deleteById(MyIdClass) , but that actually always issues a findById before sending a single DELETE statement as a transaction: not good for the performance!

Potentially irrelevant precision

I'm not sure if that can help, but my JPA provider is EclipseLink . My understanding is that there are properties for batching requests, and that's ultimately what I'm aiming to use.

However, I'm not entirely sure what are the internal requirements for that batching to happen. For example, if I did a deleteById in a for-loop , would the alternating SELECT and DELETE statements prevent the batching from happening? The documentation is quite scarce about that.

I think you are looking for something that will generate a query like this

delete from myentity where MyIdClass in (? , ? , ?)

You can try from this post , it may help you.

If you're positive IdClass is a better choice than EmbeddedId in your situation , you could add an extra mapping to MyEntity :

@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;

and use it in you repository:

@Modifying
@Query("DELETE FROM MyEntity me WHERE me.id in (:ids)")
void deleteByIdIn(@Param("ids") Collection<MyIdClass> ids);

This will generate a single query: delete from myentity where bar=? and foo=? [or bar=? and foo=?]... delete from myentity where bar=? and foo=? [or bar=? and foo=?]... delete from myentity where bar=? and foo=? [or bar=? and foo=?]... , resulting in this test to pass (with following table records insert into myentity(foo,bar) values ('foo1', 'bar1'),('foo2', 'bar2'),('foo3', 'bar3'),('foo4', 'bar4'); ):

@Test
@Transactional
void deleteByInWithQuery_multipleIds_allDeleted() {
  assertEquals(4, ((Collection<MyEntity>) myEntityRepository.findAll()).size());

  MyIdClass id1 = new MyIdClass("foo1", "bar1");
  MyIdClass id2 = new MyIdClass("foo2", "bar2");

  assertDoesNotThrow(() -> myEntityRepository.deleteByIdIn(List.of(id1, id2)));
  assertEquals(2, ((Collection<MyEntity>) myEntityRepository.findAll()).size());
}

This answer provided great insight, but it seems like the approach only works for Hibernate.

EclipseLink, which is the JPA Provider that I use, would keep throwing an error at me, for the same code.

The only working solution I found is the following hack:

JPA Query for Spring @Repository

@Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {

    @Modifying
    @Query("DELETE FROM myentity m WHERE CONCAT(m.foo, '~', m.bar) IN :ids")
    void deleteAllWithConcatenatedIds(@Param("ids") Collection<String> ids);
}

Associated index for the DB

DROP INDEX IF EXISTS concatenated_pk_index;
CREATE UNIQUE INDEX concatenated_pk_index ON myentity USING btree (( foo || '~' || bar ));

Explanation

Since EclipseLink refuses to properly treat my @IdClass , I had to adapt the service to concatenate the composite key into a single String. Then, in Postgres, you can actually create an index on that concatenation of different composite key columns.

Labeling the index as UNIQUE will greatly improve the performance of that query, but should only be done if you are sure that the concatenation will be unique (in my case it is since I'm using all the columns of the composite key).

The calling service then only has to do something like String.join("~", dto.getFoo(), dto.getBar()) and to collect all of those into the list that will be passed to the repository.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM