PostgreSQL:仅针对源列的双向引用进行级联删除

huangapple go评论50阅读模式
英文:

PostgreSQL: cascade delete on bidirectional references only for source column

问题

我有表variablevariable_variable。后者有两列:

  1. variable_id_from
  2. variable_id_to

查看下面的架构:

PostgreSQL:仅针对源列的双向引用进行级联删除

如果variable_variable中有值为{variable_id_from = 1, variable_id_to = 2}的行,意味着id = 1variable引用了id = 2variable

现在我有一个新的业务要求:

假设我们有variable A。如果有任何其他variables引用了A(即variable_variable中有任何值为{variable_id_to = A.id}的行),则必须禁止删除A

假设我们有variable B。如果没有引用B的变量,但与此同时B引用了其他变量,那么可以成功删除B,并且必须删除所有{variable_id_from = B.id}的引用。

我考虑创建一个简单的约束并使用级联删除。看看下面的SQL片段:

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_to_fkey
        FOREIGN KEY (variable_id_to) REFERENCES variable (id);

我认为这可以满足我的需求。但奇怪的是,这些测试用例未通过:

  1. 创建变量A
  2. 创建变量B
  3. A引用B
  4. 尝试删除B
  5. 预期结果:删除变量时出现错误。实际结果:成功删除B以及variable_variable中的所有相关链接。

这很奇怪。似乎variable_variable_variable_id_from_fkey在某种程度上被触发了。有没有解决这个问题的想法?

此外,还有一个重要情况。变量可能引用自身。因此,variable_variable表可以包含行{variable_id_from = 1, variable_id_to = 1}。在这种情况下,删除也应该成功,并且必须按级联方式删除所有链接。

附注:我知道可以在应用程序端执行删除,但我将其视为最后的选择。整个数据库结构比我展示的要复杂得多,级联约束对于保持代码更清晰非常有帮助。

英文:

I have tables variable and variable_variable. The last one holds 2 columns:

  1. variable_id_from
  2. variable_id_from

Look at the schema below:

PostgreSQL:仅针对源列的双向引用进行级联删除

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 other variables that reference to A (i.e. variable_variable has any row with value of {variable_id_to = A.id}), then deletion of A must be prohibited.

> Suppose we have variable B. If there are no variables that references B but in the same time B references any other variables, then deletion of B 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:

  1. Create variable A
  2. Create variable B
  3. Make A reference B
  4. Try to delete B
  5. Expected: error during variable deletion. Actual: 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.

答案1

得分: 1

A forign key would with the addition on deleet cascade, delete also n the bridge table all records

A BEFORE DELETE TRIGGER will do the job, as it will check every deleted row, if there are records in variable_variable

see example

CREATE TABLE variable (id int, name varchar(10))
英文:

A forign key would with the addition on deleet cascade, delete also n the bridge table all records

A BEFORE DELETE TRIGGER will do the job, as it will check every deleted row, if there are records in variable_variable

see example

CREATE TABLE variable (id int, name varchar(10))

> status
> CREATE TABLE
>

INSERT INTO variable VALUES(1,'test'),(2,'test2')

> status
> INSERT 0 2
>

CREATE tABLe variable_variable (variable_id_from int,variable_id_to int)

> status
> CREATE TABLE
>

INSERT INTO variable_variable VALUEs (3,1)

> status
> INSERT 0 1
>

CREATE OR REPLACE FUNCTION DELETE_ONLY_()
  RETURNS TRIGGER 
  LANGUAGE PLPGSQL
  AS
$$
declare
  l_count integer;
begin 
  select count(*)
    into l_count
  from variable_variable WHERE variable_id_to = OLd.id;

  if l_count > 0 then 
    raise exception 'There are still records';
  end if; 
  return old;
end; 

$$

> status
> CREATE FUNCTION
>

CREATE TRIGGER trigger_name 
    BEFORE DELETE
   ON variable
   FOR EACH ROW
       EXECUTE PROCEDURE DELETE_ONLY_()

> status
> CREATE TRIGGER
>

DELETE FROM variable WHERE id = 1

> error
> ERROR: There are still records
> CONTEXT: PL/pgSQL function delete_only_() line 10 at RAISE
>

DELETE FROM variable WHERE id = 2

> status
> DELETE 1
>

SELECT * FROM variable
id name
1 test
> ``` status
> SELECT 1
> ```

fiddle

答案2

得分: 0

感谢大家提供的解决方案。我成功解决了我的问题。以下是定义的约束:

-- 如果当前变量被另一个变量引用,请限制删除
ALTER TABLE variable_variable
    ADD CONSTRAINT variable_variable_variable_id_to_fkey
        FOREIGN KEY (variable_id_to) REFERENCES variable (id)
            ON DELETE RESTRICT;

-- 如果当前变量引用其他变量,请按级联方式删除其链接
ALTER TABLE variable_variable
    ADD CONSTRAINT variable_variable_variable_id_from_fkey
        FOREIGN KEY (variable_id_from) REFERENCES variable (id)
            ON DELETE CASCADE;

据我了解,PostgreSQL按照它们创建的顺序检查约束。所以,以下是发生的情况:

  1. 如果存在像 其他变量 -> 当前变量 的引用,请限制删除。否则,进行下一步。
  2. 如果存在像 当前变量 -> 其他变量 的引用,请按级联方式删除 当前变量 及其在 variable_variable 表中的链接。

然而,我还遇到了与约束无关的问题,而是与Hibernate有关。以下是Variable实体的定义:

@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)
    )
    // 这个集合是只读的,不会更新
    private Set<Variable> inverseVariables = new HashSet<>();
    
    ...
}

我曾经使用简单的Spring Data JPA存储库方法 delete(Variable variable) 来删除Variable。实际上,生成的查询如下:

delete from variable_variable where variable_id_from = ?
delete from variable_variable where variable_id_to = ?
delete from variable where id = ?

据我了解,拥有方的ManyToMany集合始终是 orphanRemoval = true。因此,Hibernate将始终在删除实体本身之前删除ManyToMany链接(如果我理解错误,请纠正我)。因此,数据库约束没有意义,因为Hibernate会过早地删除所有链接。

目前,我使用了原生SQL查询,并将 deletedeleteById 方法标记为抛出 UnsupportedOperationException,以防止意外调用它们。无论如何,我认为这不是一个清晰的解决方案。你有没有想法如何告诉Hibernate不要在拥有方删除ManyToMany链接?

英文:

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:

  1. If there are any references like Other Variable -&gt; Current Variable, restrict deletion. Otherwise, go the next step.
  2. If there are any references like Current Variable -&gt; 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 = &quot;variable&quot;)
@Getter
@Setter(PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@DynamicUpdate
public class Variable {
    @EmbeddedId
    private VariableId id;

    @ManyToMany(fetch = LAZY)
    @JoinTable(
        name = &quot;variable_variable&quot;,
        joinColumns = @JoinColumn(name = &quot;variable_id_from&quot;),
        inverseJoinColumns = @JoinColumn(name = &quot;variable_id_to&quot;)
    )
    private Set&lt;Variable&gt; variables = new HashSet&lt;&gt;();

    @ManyToMany(fetch = LAZY)
    @JoinTable(
        name = &quot;variable_variable&quot;,
        joinColumns = @JoinColumn(name = &quot;variable_id_to&quot;, updatable = false, insertable = false),
        inverseJoinColumns = @JoinColumn(name = &quot;variable_id_from&quot;, updatable = false, insertable = false)
    )
    // this collection is readonly and never updates
    private Set&lt;Variable&gt; inverseVariables = new HashSet&lt;&gt;();
    
    ...
}

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?

答案3

得分: -1

您遇到的问题是,当您删除变量B时,variable_variable_variable_id_from_fkey上的外键约束不会触发,因为它没有定义为ON DELETE CASCADE。这意味着引用B的variable_variable中的行不会被删除,因此B的删除将成功进行。

为了解决这个问题,您可以修改variable_id_to上的外键约束,使其也具有ON DELETE CASCADE:

ALTER TABLE variable_variable
    ADD CONSTRAINT variable_variable_variable_id_to_fkey
        FOREIGN KEY (variable_id_to) REFERENCES variable (id)
            ON DELETE CASCADE;

通过这种修改,当您删除变量B时,还将删除引用B的variable_variable中的所有行(即variable_id_to等于B的id的行)。这将防止删除B,如果其他变量引用它。

为了处理变量引用自身的情况,您可以在variable表中添加一个触发器,检查变量是否引用自身,并删除variable_variable中的相应行:

CREATE TRIGGER delete_self_reference
    BEFORE DELETE ON variable
    FOR EACH ROW
    BEGIN
        DELETE FROM variable_variable WHERE variable_id_from = OLD.id AND variable_id_to = OLD.id;
    END;

这个触发器将在从variable表中删除一行之前触发。如果要删除的行的id同时出现在variable_variable的一行的两列中,那么variable_variable中的相应行将被删除。这将允许删除变量,即使它引用自身。

通过这些修改,外键约束和触发器,您应该能够满足业务要求并保持数据库结构的清洁。

英文:

The issue you are facing is that when you delete variable B, the foreign key constraint variable_variable_variable_id_from_fkey on variable_id_to is not triggered because it is not defined with ON DELETE CASCADE. This means that the rows in variable_variable that reference B are not deleted, and therefore, the deletion of B proceeds successfully.

To solve this problem, you can modify the foreign key constraint on variable_id_to to also have ON DELETE CASCADE:

ALTER TABLE variable_variable
    ADD CONSTRAINT variable_variable_variable_id_to_fkey
        FOREIGN KEY (variable_id_to) REFERENCES variable (id)
            ON DELETE CASCADE;

With this modification, when you delete variable B, all rows in variable_variable that reference B (i.e., where variable_id_to is equal to B's id) will also be deleted. This will prevent the deletion of B if any other variables reference it.

To handle the case where a variable references itself, you can add a trigger to the variable table that checks if a variable is referencing itself and deletes the corresponding rows in variable_variable:

CREATE TRIGGER delete_self_reference
    BEFORE DELETE ON variable
    FOR EACH ROW
    BEGIN
        DELETE FROM variable_variable WHERE variable_id_from = OLD.id AND variable_id_to = OLD.id;
    END;

This trigger will be triggered before a row is deleted from the variable table. If the row being deleted has an id that appears in both columns of a row in variable_variable, then the corresponding row in variable_variable will be deleted. This will allow a variable to be deleted even if it references itself.

With these modifications, the foreign key constraints and the trigger, you should be able to satisfy your business requirements and keep your database structure clean.

think about it

huangapple
  • 本文由 发表于 2023年3月15日 21:36:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/75745485.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定