I have tables variable
and variable_variable
. The last one holds 2 columns:
variable_id_from
variable_id_from
Look at the schema below:
If variable_variable
has row with values {variable_id_from = 1, variable_id_to = 2}
, it means that variable
with id = 1
references to variable
with id = 2
.
Now I have a new business requirement:
Suppose we have
variable A
. If there any othervariables
that reference toA
(i.e.variable_variable
has any row with value of{variable_id_to = A.id}
), then deletion ofA
must be prohibited.
Suppose we have
variable B
. If there are no variables that referencesB
but in the same timeB
references any other variables, then deletion ofB
should proceed successfully and all references where{variable_id_from = B.id}
has to be deleted as well.
I was thinking to create one simple constraint and with the cascade delete. Look at the SQL snippet below:
ALTER TABLE variable_variable
ADD CONSTRAINT variable_variable_variable_id_from_fkey
FOREIGN KEY (variable_id_from) REFERENCES variable (id)
ON DELETE CASCADE;
ALTER TABLE variable_variable
ADD CONSTRAINT variable_variable_variable_id_from_fkey
FOREIGN KEY (variable_id_to) REFERENCES variable (id);
I thought it's to going to serve my needs exactly. But strangely, this test cases doesn't pass:
A
B
A
reference B
B
B
and all corresponding links in variable_variable
are successfully deleted.That's strange. Seems like the variable_variable_variable_id_from_fkey
is somehow triggered. Any ideas how to solve this problem?
Besides, there is also an important case. Variable might reference itself. So, the variable_variable
table can contain row {variable_id_from = 1, variable_id_to = 1}
. In this case, the deletion should also pass successfully and all the links has to be deleted by cascade.
P.S. I know I can perform the deletion on the application side but I'm considering this decision as the last resort. The entire database structure is much more complicated than I've shown. And cascade constraints are really helpful to keep code cleaner.
Thanks everybody for proposed solutions. I managed to solve my problem. Here are the defined constraints:
-- If current variable is being referenced by another variable, restrict deletion
ALTER TABLE variable_variable
ADD CONSTRAINT variable_variable_variable_id_to_fkey
FOREIGN KEY (variable_id_to) REFERENCES variable (id)
ON DELETE RESTRICT;
-- If current variable reference other variable, delete its link by cascade
ALTER TABLE variable_variable
ADD CONSTRAINT variable_variable_variable_id_from_fkey
FOREIGN KEY (variable_id_from) REFERENCES variable (id)
ON DELETE CASCADE;
As I understood, PostgreSQL checks constraints in the order they were created. So, here is what happens:
Other Variable -> Current Variable
, restrict deletion. Otherwise, go the next step.Current Variable -> Other Variable
, delete Current Variable
and its links in the variable_variable
table on cascade.However, I had another problem not with constraints but with Hibernate. Here is the Variable
entity definition:
@Entity
@Table(name = "variable")
@Getter
@Setter(PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@DynamicUpdate
public class Variable {
@EmbeddedId
private VariableId id;
@ManyToMany(fetch = LAZY)
@JoinTable(
name = "variable_variable",
joinColumns = @JoinColumn(name = "variable_id_from"),
inverseJoinColumns = @JoinColumn(name = "variable_id_to")
)
private Set<Variable> variables = new HashSet<>();
@ManyToMany(fetch = LAZY)
@JoinTable(
name = "variable_variable",
joinColumns = @JoinColumn(name = "variable_id_to", updatable = false, insertable = false),
inverseJoinColumns = @JoinColumn(name = "variable_id_from", updatable = false, insertable = false)
)
// this collection is readonly and never updates
private Set<Variable> inverseVariables = new HashSet<>();
...
}
I used to delete Variable
with simple Spring Data JPA repository method delete(Variable variable)
. Actually, that are the queries that were generated:
delete from variable_variable where variable_id_from = ?
delete from variable_variable where variable_id_to = ?
delete from variable where id = ?
As far as I understood, the owning-side ManyToMany
collections are always orphanRemoval = true
. So, Hibernate will always delete ManyToMany
links before removing the entity itself (correct me if I'm wrong). Therefore, the DB constraint meant nothing because Hibernate deleted all links prematurely.
For now I put native SQL query and marked delete
and deleteById
methods to throw UnsupportedOperationException
so nobody call them accidentally. Anyway, I don't think that's a clear solution. Do you have any ideas how I can tell Hibernate not to delete ManyToMany
links on the owner side?