web-dev-qa-db-fra.com

L'instruction DELETE est en conflit avec la contrainte REFERENCE

Ma situation ressemble à ceci:

Table STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

EMPLACEMENT de la table:

ID *[PK]*
LOCATION_NAME

Table WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Table INVENTORY_ITEMS:

ID *[PK]*
ITEM_NAME
STOCK_ARTICLE *[FK]*
LOCATION *[FK]*
WORK_PLACE *[FK]*

Les 3 FK dans INVENTORY_ITEMS référencent évidemment les colonnes "ID" dans les autres tables respectives.

Les tableaux pertinents ici sont STOCK_ARTICLE et INVENTORY_ITEMS.

Il existe maintenant un travail SQL composé de plusieurs étapes (scripts SQL) qui "synchronise" la base de données mentionnée ci-dessus avec la base de données ne autre (OTHER_DB). L'une des étapes de ce travail consiste à effectuer un "nettoyage". Il supprime tous les enregistrements de STOCK_ITEMS où il n'y a pas d'enregistrement correspondant dans l'autre base de données avec le même ID. Cela ressemble à ceci:

DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)

Mais cette étape échoue toujours avec:

L'instruction DELETE est en conflit avec la contrainte REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Le conflit s'est produit dans la base de données "FIRST_DB", table "dbo.INVENTORY_ITEMS", colonne "STOCK_ARTICLES". [SQLSTATE 23000] (Erreur 547) L'instruction a été terminée. [SQLSTATE 01000] (erreur 3621). L'étape a échoué.

Le problème est donc qu'il ne peut pas supprimer les enregistrements de STOCK_ARTICLES lorsqu'ils sont référencés par INVENTORY_ITEMS. Mais ce nettoyage doit fonctionner. Ce qui signifie que je dois probablement étendre le script de nettoyage afin qu'il identifie d'abord les enregistrements qui doivent être supprimés de STOCK_ITEMS, mais ne le peut pas car l'ID correspondant est référencé depuis INVENTORY_ITEMS. Ensuite, il doit d'abord supprimer ces enregistrements dans INVENTORY_ITEMS, puis supprimer les enregistrements dans STOCK_ARTICLES. Ai-je raison? À quoi ressemblerait alors le code SQL?

Je vous remercie.

10
derwodaso

C'est tout l'intérêt des contraintes de clé étrangère: elles vous empêchent de supprimer des données qui sont référencées ailleurs afin de maintenir l'intégrité référentielle.

Il y a deux options:

  1. Supprimez les lignes de INVENTORY_ITEMS d'abord, puis les lignes de STOCK_ARTICLES.
  2. Utilisation ON DELETE CASCADE pour dans la définition de clé.

1: suppression dans le bon ordre

La manière la plus efficace de procéder varie en fonction de la complexité de la requête qui décide des lignes à supprimer. Un schéma général pourrait être:

BEGIN TRANSACTION
SET XACT_ABORT ON
DELETE INVENTORY_ITEMS WHERE STOCK_ARTICLE IN (<select statement that returns stock_article.id for the rows you are about to delete>)
DELETE STOCK_ARTICLES WHERE <the rest of your current delete statement>
COMMIT TRANSACTION

C'est très bien pour les requêtes simples ou pour supprimer un seul article en stock, mais étant donné que votre instruction de suppression contient un WHERE NOT EXISTS clause imbriquée dans WHERE IN peut produire un plan très inefficace, alors testez avec une taille de jeu de données réaliste et réorganisez la requête si nécessaire.

Notez également les instructions de transaction: vous voulez vous assurer que les deux suppressions sont terminées ou que ni l'une ni l'autre ne le font. Si l'opération se déroule déjà dans une transaction, vous devrez évidemment la modifier pour qu'elle corresponde à votre transaction actuelle et à votre processus de gestion des erreurs.

2: utilisez ON DELETE CASCADE

Si vous ajoutez l'option en cascade à votre clé étrangère, SQL Server le fera automatiquement pour vous, en supprimant les lignes de INVENTORY_ITEMS pour satisfaire la contrainte que rien ne doit faire référence aux lignes que vous supprimez. Ajoutez simplement ON DELETE CASCADE à la définition FK comme ceci:

ALTER TABLE <child_table> WITH CHECK 
ADD CONSTRAINT <fk_name> FOREIGN KEY(<column(s)>)
REFERENCES <parent_table> (<column(s)>)
ON DELETE CASCADE

Un avantage ici est que la suppression est une instruction atomique réduisant (bien que, comme d'habitude, ne supprimant pas à 100%) la nécessité de se soucier des paramètres de transaction et de verrouillage. La cascade peut même fonctionner sur plusieurs niveaux parent/enfant/petit-enfant/... si il n'y a qu'un seul chemin entre le parent et tous les descendants ( recherchez "plusieurs chemins en cascade" pour des exemples de cas où cela pourrait ne pas fonctionner).

REMARQUE: Moi et beaucoup d'autres, je considère que les suppressions en cascade sont dangereuses, donc si vous utilisez cette option, faites très attention à bien la documenter dans la conception de votre base de données afin que vous et les autres développeurs ne trébuchiez pas le danger plus tard . J'évite les suppressions en cascade dans la mesure du possible pour cette raison.

Un problème courant causé par les suppressions en cascade est lorsque quelqu'un met à jour les données en supprimant et en recréant des lignes au lieu d'utiliser UPDATE ou MERGE. Cela se produit souvent là où "mettre à jour les lignes qui existent déjà, insérer celles qui n'en ont pas" (parfois appelé opération UPSERT) est nécessaire et les personnes qui ne connaissent pas l'instruction MERGE trouvent plus facile à faire:

DELETE <all rows that match IDs in the new data>
INSERT <all rows from the new data>

que

-- updates
UPDATE target 
SET    <col1> = source.<col1>
  ,    <col2> = source.<col2>
       ...
  ,    <colN> = source.<colN>
FROM   <target_table> AS target JOIN <source_table_or_view_or_statement> AS source ON source.ID = target.ID
-- inserts
INSERT  <target_table>
SELECT  *
FROM    <source_table_or_other> AS source
LEFT OUTER JOIN
        <target_table> AS target
        ON target.ID = source.ID
WHERE   target.ID IS NULL

Le problème ici est que l'instruction delete se répercutera en cascade sur les lignes enfants et que l'instruction insert ne les recréera pas. Ainsi, lors de la mise à jour de la table parent, vous perdez accidentellement des données de la ou des tables enfant.

Résumé

Oui, vous devez d'abord supprimer les lignes enfants.

Il existe une autre option: ON DELETE CASCADE.

Mais ON DELETE CASCADE peut être dangereux , utilisez-le donc avec précaution.

Remarque: utilisez MERGE (ou UPDATE- et -INSERTMERGE n'est pas disponible) lorsque vous avez besoin d'une opération UPSERT, not DELETE- then-replace-with -INSERT pour éviter de tomber dans des pièges posés par d'autres personnes utilisant ON DELETE CASCADE.

13
David Spillett

Vous pouvez obtenir des ID à supprimer une seule fois, les stocker dans une table temporaire et les utiliser pour supprimer des opérations. Ensuite, vous contrôlez mieux ce que vous supprimez.

Cette opération ne doit pas échouer:

SELECT sa.ID INTO #StockToDelete
FROM STOCK_ARTICLES sa
LEFT JOIN [OTHER_DB].[dbo].[OtherTable] other ON other.ObjectID = sa.OTHER_DB_ID
WHERE other.ObjectID IS NULL

DELETE ii
FROM INVENTORY_ITEMS ii
JOIN #StockToDelete std ON ii.STOCK_ARTICLE = std.ID

DELETE sa
FROM STOCK_ARTICLES sa
JOIN #StockToDelete std ON sa.ID = std.ID
2
Paweł Tajs

J'ai également rencontré ce problème et j'ai pu le résoudre. Voici ma situation:

Dans mon cas, j'ai une base de données utilisée pour signaler une analyse (MYTARGET_DB), qui tire d'un système source (MYSOURCE_DB). Certaines des tables "MYTARGET_DB" sont uniques à ce système, et les données y sont créées et gérées; La plupart des tableaux proviennent de "MYSOURCE_DB" et il existe un travail qui supprime/insère les données dans "MYTARGET_DB" de "MYSOURCE_DB".

L'une des tables de recherche [PRODUCT] provient de la SOURCE et une table de données [InventoryOutsourced] est stockée dans la TARGET. L'intégrité référentielle est conçue dans les tableaux. Ainsi, lorsque j'essaie d'exécuter la suppression/insertion, j'obtiens ce message.

Msg 50000, Level 16, State 1, Procedure uspJobInsertAllTables_AM, Line 249
The DELETE statement conflicted with the REFERENCE constraint "FK_InventoryOutsourced_Product". The conflict occurred in database "ProductionPlanning", table "dbo.InventoryOutsourced", column 'ProdCode'.

La solution de contournement que j'ai créée consiste à insérer des données dans la variable de table [@tempTable] de [InventoryOutsourced], à supprimer des données dans [InventoryOutsourced], à exécuter les travaux de synchronisation, à insérer dans [InventoryOutsourced] à partir de [@tempTable]. Cela maintient l'intégrité en place et la collecte de données unique est également conservée. Quel est le meilleur des deux mondes. J'espère que cela t'aides.

BEGIN TRY
    BEGIN TRANSACTION InsertAllTables_AM

        DECLARE
        @BatchRunTime datetime = getdate(),
        @InsertBatchId bigint
            select @InsertBatchId = max(IsNull(batchid,0)) + 1 from JobRunStatistic 

        --<DataCaptureTmp/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            DECLARE @tmpInventoryOutsourced as table (
                [ProdCode]      VARCHAR (12)    NOT NULL,
                [WhseCode]      VARCHAR (4)     NOT NULL,
                [Cases]          NUMERIC (8)     NOT NULL,
                [Weight]         NUMERIC (10, 2) NOT NULL,
                [Date] DATE NOT NULL, 
                [SourcedFrom] NVARCHAR(50) NOT NULL, 
                [User] NCHAR(50) NOT NULL, 
                [ModifiedDatetime] DATETIME NOT NULL
                )

            INSERT INTO @tmpInventoryOutsourced (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM [dbo].[InventoryOutsourced]

            DELETE FROM [InventoryOutsourced]
        --</DataCaptureTmp> 

... Delete Processes
... Delete Processes    

        --<DataCaptureInsert/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            INSERT INTO [dbo].[InventoryOutsourced] (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM @tmpInventoryOutsourced
            --</DataCaptureInsert> 

    COMMIT TRANSACTION InsertAllTables_AM
END TRY
1

Je n'ai pas entièrement testé, mais quelque chose comme ça devrait fonctionner.

--cte of Stock Articles to be deleted
WITH StockArticlesToBeDeleted AS
(
SELECT ID FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)
)
--delete from INVENTORY_ITEMS where we have a match on deleted STOCK_ARTICLE
DELETE a FROM INVENTORY_ITEMS a join
StockArticlesToBeDeleted b on
    b.ID = a.STOCK_ARTICLE;

--now, delete from STOCK_ARTICLES
DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID);
0
Scott Hodgin