触发器在另一个表中创建或更新记录

Posted

技术标签:

【中文标题】触发器在另一个表中创建或更新记录【英文标题】:Trigger to create or update records in another table 【发布时间】:2021-10-23 13:19:16 【问题描述】:

编辑:这个问题基本解决了。我仍在尝试做的事情可能需要不同的方法。

我正在向旧数据库表添加触发器,以便为相关表自动创建新记录。这不是家庭作业。这是我在 SQL Server 2017 上的 DB Fiddle。https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=ed2ed585606da9d69cb63402ea5c0807

例如,父表如下所示。

新记录将从kids 以逗号分隔的列创建(不幸的是,它是旧数据库,无法更改)。我正在使用 SQL Server 2016 中的 string_split 函数将其分成行。例如。当我在上面插入这 5 条记录时,由于我的触发器,这是我在 kids 表中得到的结果,正如预期的那样。

kids 表的唯一标识符kidCodeparentID 和每个孩子的名字的哈希值。我们假设每个姓氏都是唯一的,每个孩子的名字都是唯一的,并且姓氏可以改变但孩子的名字不能改变。 地址、运动和团队字段是可选的,将在创建/更新期间从父母复制到孩子。 孩子的加入日期是父记录首次存在的日期。

编辑:我修复了前两个问题。此外,代码现在包含在下面。

CREATE OR ALTER TRIGGER TR_Update_Kids ON [dbo].[parents] AFTER INSERT, UPDATE AS
BEGIN
   SET NOCOUNT ON;
   IF TRIGGER_NESTLEVEL(OBJECT_ID('TR_Update_Kids','TR')) > 1 RETURN
   
   MERGE INTO [dbo].[kids] AS KidsTable USING
      (SELECT CONVERT(VARCHAR(32), HashBytes('SHA2_256', CONCAT(INSERTED.id,'-',TRIM([value]))), 2) AS kidCode,
               INSERTED.surname AS surname,
               TRIM([value]) AS name,
               INSERTED.address AS address,
               INSERTED.sports AS sports,
               INSERTED.team AS team,
               INSERTED.id AS parentID
        FROM INSERTED
        CROSS APPLY string_split(INSERTED.kids, ',')
      ) AS KidsInfo
   ON KidsTable.kidCode=KidsInfo.kidCode
   WHEN MATCHED THEN UPDATE SET
        KidsTable.surname = KidsInfo.surname,
        KidsTable.address = KidsInfo.address,
        KidsTable.sports = KidsInfo.sports,
        KidsTable.team = KidsInfo.team
   WHEN NOT MATCHED THEN INSERT
        (kidCode,surname,name,address,sports,team,age,class,teacher,parentID,join_date)
        VALUES
        (KidsInfo.kidCode,KidsInfo.surname,KidsInfo.name,KidsInfo.address,KidsInfo.sports,KidsInfo.team,NULL,NULL,NULL,KidsInfo.parentID,GETDATE());
   print ('Added trigger to insert/update kids when parents data changes')
END

触发器似乎可以满足我的一些需求,即当您创建新记录时就可以了。当您更新地址、运动或团队时,它也很好。但是,我还有 3 2 个问题。

    没有形成新的儿童行。当您将新子项添加到父项的逗号分隔字段时,不会为该子项形成新行。 请看小提琴。现在解决了。 不更新姓氏 当您更新父母的姓氏时,会为新姓氏添加新记录,但旧记录也会保留在那里。 请看小提琴。现在解决了。 需要双向记录更新 需要双向更新,所以我需要为 kids 表创建第二个(类似)触发器,这样当nameaddress , sportsteam 字段被更新,然后传播回父表。 surname 永远不会从 kids 表中更新,parentsID 永远不会因任何原因而更新。 ageclassteacher 的更新将独立于 parents 我不知道如何开始第二个触发器,我们将不胜感激。 这个是结合我在很多 *** 帖子 starting with my previous post 中阅读的内容的结果,我什至不确定我是否正确地执行了这个新触发器。

如果这第三个问题值得单独提问,请告诉我。然后我将接受前两个的答案,然后为第三个提出一个新问题。谢谢!

【问题讨论】:

根据问题指南,请不要发布代码、数据、错误消息等的图像 - 将文本复制或输入到问题中。请保留将图像用于图表或演示渲染错误,无法通过文本准确描述的事情。 而且你的问题需要是独立的,所以需要在问题中添加触发代码。小提琴很好,但不应该依赖。 我个人建议先修复你的设计。您正在存储分隔数据。解决这个问题,然后担心触发器。 @Larnu 好吧,那很好,但我无权访问它。该数据库由已运行数十年的专有可执行文件写入。我的任务是为现有数据和由此可执行文件创建的新传入数据生成报告(使用触发器直接在 SQL 中)。 另外,您的触发器不起作用的原因是IF。在您的示例中,拆分后有两条记录,Samuel 和 Taylor,由于 Samuel 已经存在,因此您在 IF 中输入真实条件,并且永远不会到达 ELSE 中的 INSERTIF 在整个集合上执行,而不是每行。您可以完全删除IF,只需在插入中添加NOT EXISTS 检查,或在MERGE 中添加WHEN NOT MATCHED 条件(不是100% 确定为什么在这里使用合并,当您所做的只是@987654353 @?) 【参考方案1】:

即使您想创建kids 表的规范化版本,也不需要重复其他数据,例如在kidsparents 中存储相同的address 是多余的,并且可能会导致问题。举个简单的例子,如果我运行:

UPDATE dbo.Kids
SET Address = 'A new Address'
WHERE Name = 'John'
AND Surname = 'Adams';

这应该如何更新父记录?这间房子里有3个孩子,如果一个可以改变地址而其他两个不能改变,那么地址只属于kids而不属于父母。或者如果父母和孩子都需要一个地址,但没有要求他们住在同一个地址,那么您可能每个都有一个地址列,但不可能提供任何自动同步。或者,如果(我怀疑)父地址是相关的,则无需针对每个单独的孩子存储它,如果您需要孩子的地址(或任何其他公共字段),只需加入回父母,例如

SELECT k.*, p.address, p.surname, p.sports, p.team 
FROM Kids AS k 
JOIN Parents AS p ON p.Id = k.ParentId;

这应该有望涵盖您的第三个问题。您的双向触发器是多余的(如果您不重复数据),或者由于将多行合并为一行而不可能/不明智,这可能会导致冲突。

要回答您的第一个问题,即为什么没有为 Taylor domer 创建记录,这是因为您实际上拥有以下伪代码:

If any record already exists in kids
    -  then update the existing record
else
    - add a new record.

由于 Samuel Domer 已经存在,那么您只能进行更新,而永远不会进行插入。你根本不需要IF 语句,如果你接受我关于不复制数据的建议,那么你也不需要更新(因为你只更新两个表共有的字段),所以你需要插入

INSERT INTO dbo.kids(KidCode, Name, ParentId, join_date)
SELECT  kidCode = CONVERT(VARCHAR(32), HASHBYTES('SHA2_256', CONCAT(INSERTED.id, '-', TRIM(s.value))), 2),
        TRIMname = (value),
        parentID = INSERTED.id,
        join_date = GETDATE()
FROM    INSERTED
        CROSS APPLY string_split(INSERTED.kids, ',') AS s
WHERE   NOT EXISTS (SELECT 1  FROM dbo.Kids AS k WHERE k.ParentId = INSERTED.id AND k.name = TRIM(s.value));

您可能还想考虑如果将孩子从父母身边移除会发生什么,可以按如下方式处理:

DELETE  k
FROM    dbo.Kids AS k
WHERE   EXISTS
(
    SELECT  1
    FROM    DELETED AS d
            CROSS APPLY string_split(d.kids, ',') AS s
    WHERE   d.id = k.ParentID
    AND     TRIM(s.value) = k.name
    AND     NOT EXISTS 
            (   SELECT 1 
                FROM    INSERTED AS i 
                        CROSS APPLY string_split(i.kids, ',') AS s2 
                WHERE   i.id = d.id 
                AND     TRIM(s2.value) = TRIM(s.value)
            )
);

Complete demo on db<>fiddle

【讨论】:

我的主管坚持要求父母和孩子的额外数据字段。如果数据库提供商停止提供,这似乎是一项业务决策?或者,如果这个旧软件发生了什么事——我们不会丢失所有数据。数据库是远程的,并不完全在我们的控制之下。这是一个备份,同时对数据进行规范化。无论如何,您之前对#1 问题的评论帮助我解决了这个问题!我正在发布更新的代码和小提琴。我现在只剩下#3(反向触发器)要处理了。由于您的评论帮助了我,我接受了这个! 如果您担心数据库提供者停止提供,那么创建另一个表(例如 parent2)在父级存储数据并使用相同的触发器来维护它。除非同一个家庭的孩子可以有不同的地址,否则没有充分的理由在孩子级别存储地址,但如果他们可以有不同的地址,那么就没有办法将这些更改传播回单个父记录。但是,无论哪种方式,您的主管要求将其存储在子级别,但仍将更改传播给父级是没有意义的。 所有“孩子”总是有共同的数据(比如我所说的“地址”)作为父级,这就是它可以传播回那个级别的方式。复制整个“父母”数据库可能会起作用,但它非常大。我们只对从现在开始在主数据库上更新或创建的记录感兴趣。不过好点!我会和她一起抚养他们。正如您所说,也许我可以修改触发器以仅将新/更新的父级记录的副本存储在本地数据库中。谢谢

以上是关于触发器在另一个表中创建或更新记录的主要内容,如果未能解决你的问题,请参考以下文章

如何编写一个触发器,根据在另一个表中采取的操作来更新一个表?

Rails after_save 回调未触发

SQLServer之创建AFETER DELETE触发器

Laravel 雄辩:更新父记录并使用一种形式在另一个表中创建新记录

Sql 触发器在表更新时添加新行

oracle 触发器插入和更新时,怎么操作