@SoftDelete vs @SQLDelete+@SQLRestriction: bulk deletes, double deletes, converters ######## H2 ######## database: H2Dialect / H2 2.4.240 (2025-09-22) == 1. HQL bulk delete: delete from ArticleDeleted / delete from ArticleManual -- delete from ArticleDeleted sql> update article_deleted ad1_0 set deleted=true where ad1_0.deleted=false article_deleted rows: [1 | a | true, 2 | b | true] -- delete from ArticleManual sql> delete from article_manual am1_0 where (am1_0.deleted = false) article_manual rows : [] == 2. Two sessions delete the same row (second one is stale) -- @SQLDelete entity, s1.flush() sql> update article_manual set deleted = true where id = ? flush succeeded silently (the hand-written UPDATE has no 'and deleted = false' guard) (the @SoftDelete entity fails the same scenario with StaleStateException: see 02-ddl-and-sql.txt) == 3. converter attribute: char(1) 'Y'/'N' column named is_gone create table article_yes_no (is_gone char(1) not null check ((is_gone in ('N','Y'))), id bigint generated by default as identity, title varchar(255), primary key (id)); comment on column article_yes_no.is_gone is 'Y = deleted'; alive : [1 | N] -- remove sql> select ayn1_0.id,ayn1_0.title from article_yes_no ayn1_0 where ayn1_0.is_gone='N' and ayn1_0.id=? sql> update article_yes_no set is_gone='Y' where id=? and is_gone='N' deleted : [1 | Y] == 4. @SoftDelete on a @MappedSuperclass create table note (deleted_at timestamp(6) with time zone, id bigint generated by default as identity, text varchar(255), primary key (id)); -- remove(note) sql> select n1_0.id,n1_0.text from note n1_0 where n1_0.deleted_at is null and n1_0.id=? sql> update note set deleted_at=localtimestamp where id=? and deleted_at is null raw: [1 | true] ######## POSTGRES ######## database: PostgreSQLDialect / PostgreSQL 17.11 == 1. HQL bulk delete: delete from ArticleDeleted / delete from ArticleManual -- delete from ArticleDeleted sql> update article_deleted ad1_0 set deleted=true where ad1_0.deleted=false article_deleted rows: [1 | a | true, 2 | b | true] -- delete from ArticleManual sql> delete from article_manual am1_0 where (am1_0.deleted = false) article_manual rows : [] == 2. Two sessions delete the same row (second one is stale) -- @SQLDelete entity, s1.flush() sql> update article_manual set deleted = true where id = ? flush succeeded silently (the hand-written UPDATE has no 'and deleted = false' guard) (the @SoftDelete entity fails the same scenario with StaleStateException: see 02-ddl-and-sql.txt) == 3. converter attribute: char(1) 'Y'/'N' column named is_gone create table article_yes_no (is_gone char(1) not null check ((is_gone in ('N','Y'))), id bigint generated by default as identity, title varchar(255), primary key (id)); comment on column article_yes_no.is_gone is 'Y = deleted'; alive : [1 | N] -- remove sql> select ayn1_0.id,ayn1_0.title from article_yes_no ayn1_0 where ayn1_0.is_gone='N' and ayn1_0.id=? sql> update article_yes_no set is_gone='Y' where id=? and is_gone='N' deleted : [1 | Y] == 4. @SoftDelete on a @MappedSuperclass create table note (deleted_at timestamp(6) with time zone, id bigint generated by default as identity, text varchar(255), primary key (id)); -- remove(note) sql> select n1_0.id,n1_0.text from note n1_0 where n1_0.deleted_at is null and n1_0.id=? sql> update note set deleted_at=localtimestamp where id=? and deleted_at is null raw: [1 | true]