0

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.

payne
  • 4,691
  • 8
  • 37
  • 85
  • There are some hacks you might try, like a native query where you use a string parameter to represent the data in the list https://stackoverflow.com/a/14375413/496099 - but it won't work with parameter binding. Others are to use internal EclipseLink delete queries- EclipseLink is able to batch its DeleteObject query, and it doesn't require fetching the object. JPA doesn't have IN support for constructs though - I didn't think all databases did either. – Chris Oct 28 '21 at 15:51
  • @Chris : how would I use "internal EclipseLink delete queries" ? I've been struggling to understand how to get EclipseLink to batch my requests (there doesn't seem to be much documentation on the preconditions for batching). – payne Oct 28 '21 at 16:16
  • EclipseLink batch writing depends on driver support, but its configured through a persistence unit flag: https://www.eclipse.org/eclipselink/documentation/2.4/jpa/extensions/p_jdbc_batchwriting.htm . This allows it to batch up all statements it encounters within a transaction when writing out SQL (on flush or commit). EclipseLink queries are documented, but it is non-JPA api, so a bit more challenging to find. Simplest starting point might be to get the "UnitOfWork uow = entityManager.unwrap(UnitOfWork.class);" then try using deleteObject on instances with only a pk. – Chris Oct 29 '21 at 19:07

3 Answers3

2

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=?]..., 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());
}
Lookslikeitsnot
  • 366
  • 3
  • 7
  • This seems very close. I'm getting `Internal Exception: org.postgresql.util.PSQLException: Can't infer the SQL type to use for an instance of com.path.to.MyIdClass. Use setObject() with an explicit Types value to specify the type to use.` after seeing the actual query generated for a batch of 5 entities to delete: `Call: DELETE FROM myentity WHERE ((?, ?) IN (?,?,?,?,?))` – payne Oct 28 '21 at 17:26
  • And would the proposed `@Query` propagate the deletion as per the defined `@CascadeType` ? – payne Oct 28 '21 at 17:28
  • Check [this implementation of the code](https://github.com/lookslikeitsnot/stackoverflow-69747958). All tests are passing. So there's either a breaking change in the dependencies versions we're using or a mapping issue. – Lookslikeitsnot Oct 28 '21 at 17:47
  • And to answer your second question, [take a look at this](https://stackoverflow.com/questions/23443188/why-does-a-manually-defined-spring-data-jpa-delete-query-not-trigger-cascades). tl;dr the query annotation disables the smart part of JPA If you check the tests from my github link, you'll see that the last one acutally cascades since there's no annotation but it generates a query for every single action. – Lookslikeitsnot Oct 28 '21 at 17:50
  • Thanks for sharing the repo! Very interesting that your repo works and not mine. For example, I had to add `@Embeddable` on `MyIdClass` else I'd get a "`The Entity class [class com.path.to.MyEntity] has an embedded attribute [id] of type [class com.path.to.MyIdClass] which is NOT an Embeddable class.`". – payne Oct 28 '21 at 18:10
  • I'm also wondering : 1) why you had to add `insertable=false`. 2) what it would look like if it was a simple `@EmbeddedId` (because having this "repeated structure" of the PK kind of bothers me). – payne Oct 28 '21 at 18:15
  • The fact that I'm not getting the missing Embeddable annotation error seems to be a bug. [The doc](https://javaee.github.io/javaee-spec/javadocs/javax/persistence/Embedded.html) is very clear that one should add it for embedded classes. (Either that or the doc is not up to date). 1. Since the column is mapped twice, adding "insertable" and "updatable" is required to prevent repeated mapping errors. 2. I'll make another commit with embeddedId when I have some time. I added a "deleteByMyIdClassIn" in the repo. It's a fun one, it kinda does what you want except for the number of queries – Lookslikeitsnot Oct 28 '21 at 19:07
  • If you could figure out how to get the cascade to work with the JPA Query, it'd be great! Having a single statement to send to the DB is a big performance improvement. The other option I'm trying to figure out is if I can use `deleteByIdIn` but while having properly activated `EclipseLink`'s `batch` capability (I'm thinking it should then do the whole thing pretty efficiently?). – payne Oct 28 '21 at 20:39
  • We do have a few differences between our repos: yours uses H2, Hibernate, and `spring-data-jpa:2.5.6`. Mine uses Postgres, EclipseLink, and `spring-data-jpa:2.4.2`. In my case, the first error I reported (`"... Can't infer the SQL type ...`") was because I tried to use both "`?1`" and "`:ids`" instead of "`(:ids:)`". When I write my query exactly like yours, I get this other error: "You have attempted to set a value of type class java.util.HashSet for parameter ids with expected type of class com.path.to.MyIdClass from query string DELETE FROM MyEntity me WHERE me.id IN (:ids).". – payne Oct 28 '21 at 21:51
  • I've looked deeper into this. It seems like somehow when I use your code and just swap out Hibernate for EclipseLink, it starts behaving as I've described. See [this other SO question I created because of it](https://stackoverflow.com/questions/69763814/jpa-query-eclipselink-not-working-when-hibernate-does): I describe how you can modify your repo to observe that behavior. – payne Oct 29 '21 at 04:58
  • This query is not supported by the JPA specification, so isn't required by all JPA providers (and isn't by EclipseLink). The left side is the state_valued_path_expression, and has strictly been limited to supporting only basic types; not the embedded object being used here. – Chris Oct 29 '21 at 18:38
0

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.

  • Yes, I actually linked that in my question. I'm not well-versed enough in JPA Queries to know how to write the string that goes in the `@Query("here")` properly. The answers in the link only describe how to work with a list of primitives; I'm dealing with a list of composite keys. – payne Oct 28 '21 at 04:13
  • My bad, I forgot to provide this link :- https://www.baeldung.com/jpa-composite-primary-keys. I think this explains what you need. – Nirmal Kumar Oct 28 '21 at 04:28
0

This answer provided great insight, but it seems like the approach only works for Hibernate. EclipseLink, which is the JPA Provider that I'm forced to 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 (Postgres)

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.

payne
  • 4,691
  • 8
  • 37
  • 85