9

For some reason my delete is not cascading when I try to delete the parent element which has an elementcollection in it, the two classes are as follows:

@Entity
@Table(name="Timestamps")
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductList {
    private boolean success;
    @Id
    private Date lastUpdated;

    private String time = "minute";

    @ElementCollection
    @MapKeyColumn(name="product_id")
    @CollectionTable(name="Products")
    private Map<String,Product> products;

And:

@Embeddable
@JsonIgnoreProperties(ignoreUnknown = true)
public class Product{
    @Embedded
    private Status quick_status;

Currently this is the only field that I have in the class because I have removed all the others trying to figure out why when I try to delete a parent object the delete does not cascade to the Products table. Below is the query I am running:

DELETE FROM Timestamps list WHERE list.last_updated !=0;

The last_updated value will always be non-zero, so I am just using this query to test deleting, but even when I run the query in the mysql shell I get "Cannot delete or update a parent row: a foreign key constraint fails" I thought that the elementcollection annotation was suppose to automatically cascade, is there something that I am missing?

EDIT, when Below are the sql commands that Hibernate is sending, as you will notice on the third one it is missing the cascade.

Hibernate: create table products (product_list_last_updated datetime(6) not null, buy_price float not null, sell_price float not null, product_id varchar(255) not null, primary key (product_list_last_updated, product_id)) engine=InnoDB
Hibernate: create table timestamps (last_updated datetime(6) not null, success bit not null, time varchar(255), primary key (last_updated)) engine=InnoDB
Hibernate: alter table products add constraint FKo31ur4gpvx1d5720rgs3qaawi foreign key (product_list_last_updated) references timestamps (last_updated)

EDIT 2: Below is the @Query that I have for the ProductListRepository class, I included on the query that is relevant for deleting.

@Repository
public interface ProductListRepository extends CrudRepository<ProductList, Integer>{
    @Modifying
    @Query(value = "DELETE FROM Timestamps list WHERE list.last_updated !=0", nativeQuery = true)
    void deleteOld();
}
IDKWhatImDoing
  • 137
  • 1
  • 13
  • How are you issuing your delete statement: `DELETE FROM Timesta...`? Directly in your SQL editor? Or, in a `@Query`? Or, something else? What happens if you try deleting it using the correspondent product repository: e.g: `productRepository.delete(productsToBeDeleted)`? – Rafa May 02 '20 at 14:39
  • @Rafa Well I have tried both and the end goal will be to get it to run with the Query annotation, but I have been testing it using the MySQL shell, because if I can get it working in there then it should be fine using it in the repository class. Also I did try productRepository.deleteAll(); and it did indeed cascade the delete, however why does that work but specifying using the Query annotation and entering it directly into the MySQL shell not work? – IDKWhatImDoing May 02 '20 at 15:26
  • @IDKWhatImDoing , it seems crazy the way you want to achieve. why do you want to delete products on deletion of logs??? I makes more sense if logs are deleted when products is deleted. there is no sense of still storing logs of deleted product. – Akhil Surapuram May 03 '20 at 13:36

3 Answers3

1

There are several variables in play. @ElementCollection has some limitations. See: https://en.wikibooks.org/wiki/Java_Persistence/ElementCollection

The limitations of using an ElementCollection instead of a OneToMany is that the target objects cannot be queried, persisted, merged independently of their parent object. They are strictly privately-owned (dependent) objects, the same as an Embedded mapping. There is no cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent. ElementCollection still can use a fetch type and defaults to LAZY the same as other collection mappings.

It works as intended because productRepository.deleteAll() works.

Why doesn't work with a native query? Because native queries are executed "as is", meaning that it won't account the annotations in the entities.

Additionnaly, because it is annotated with @ElementCollection it didn't include any ON DELETE CASCADE in your constraint foreign key

Aka, this alter table... does not have ON DELETE CASCADE

alter table products add constraint FKo31ur4gpvx1d5720rgs3qaawi foreign key (product_list_last_updated) references timestamps (last_updated)

Suggested Solition 1

Change from @ElementCollection to @OneToMany with the intended cascade options.

Suggested Solition 2

Remove the nativeQuery = true and use a JPA query instead. It should look something like:

@Modifying
@Query("DELETE FROM ProductList list WHERE list.lastUpdated != 0")
void deleteOld();

Suggested Solition 3

Use Spring Data named queries in your ProductListRepository. Something like:

deleteByLastUpdatedNot(Date date);
or 
deleteByLastUpdatedLessThan(Date date);
Rafa
  • 1,997
  • 3
  • 21
  • 33
  • 1
    As you have stated in your answer the `alter table` does not have the `ON CASCADE DELETE` however that is the crux of matter. Why does it not automatically apply the cascade? As was stated in the quote "target objects are always persisted, merged, removed with their parent" however if it doesn't cascade the delete how does it go about removing the child with the parent? It seems to unnecessarily limit how you can interact with the database. – IDKWhatImDoing May 02 '20 at 23:11
  • As for the solutions, solution 1 is what I would probably have to end up going with, although that is a workaround and I would prefer to keep using the `@ElementCollection` annotation. Solution 2 does not work, it hits the same error as the native query, which makes sense because using a non-native query is just a nicer way to write the query but it ultimately gets processed into the native query I am using because that is the only way it can run. The last one might work as well, but it has its own limitations and i would prefer to specify my own queries using `@Query` – IDKWhatImDoing May 02 '20 at 23:18
  • Hi @IDKWhatImDoing, the `ON CASCADE DELETE` is createc along with cascade options e.g: `@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)`. Hum , I did not know that Solution2 won't work on `@ElementCollection`. I personally find JPA very opinionated and sometimes rigid. For high flexibility `JDBC Template` is a good option for you to write your own queries. – Rafa May 02 '20 at 23:50
  • Option2 is a bulk-delete. HIbernate does not generate delete-sql-statements for the embeddable collection entities. And that is the problem. The `productRepository.deleteAll()` generates 1 delete-sql statement per element. Which is highly inefficient. With all these limitations I don't see any benefits of the @ElementCollection over @ManyToOne. – egelev Aug 26 '21 at 09:05
0

(Caveat: I do not know what was in the designers' minds.)

To me, automatic deletion of data is scary. A slight misunderstanding on my part could lead to data being deleted that I did not expect.

Sure, in a well-designed, textbook-style, schema, the data will be perfectly structured and it should be obvious what to delete and what not to delete.

But textbook schemas are limited to textbooks.

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • While the prospect of shooting yourself in the foot with cascading deletions may be a problem with the other relations such as `@OneToMany` here cascading deletion when the parent is deleted is the only logical course, because the collection of embedded elements have no meaning without the parent, because they are a part of the parent. Similar to how normally embedding a single class into an entity using `@Embedded` will embed the columns of the child's fields into the parents table and delete works, here the idea is the same, it just need a different table for the one to many like relation – IDKWhatImDoing May 02 '20 at 22:05
0

In mysql shell you aren't able to execute the query because you didn't set your foreign key ON DELETE CASCADE in your mysql. Adding ON DELETE CASCADE to your foreign key will allow mysql shell to cascade on delete.

It seems illogical to delete product once it's logs been deleted.

I think the structure must look something like this:

  • CREATE TABLE products(id INT NOT NULL AUTO_INCREMENT, buy_price FLOAT, sell_price FLOAT, lastUpdateDate DATETIME, PRIMARY KEY (id));
  • CREATE TABLE timestamps(id INT NOT NULL AUTO_INCREMENT, product INT NOT NULL, success BIT NOT NULL,updateDate DATETIME NOT NULL,PRIMARY KEY (id), FOREIGN KEY (product) REFERENCES products(id) ON DELETE CASCADE);

So now when ever you delete a product it's logs gonna be deleted.

Hussein Akar
  • 425
  • 3
  • 12