更新具有一对多关系的实体

时间:2017-01-28 10:58:36

标签: c# sql asp.net sql-server one-to-many

我有一个供应商表,其中包含以下架构:

| SupplierId | SupplierName |
-----------------------------
| 1          | Good Company |

我还有一对多的表 SupplierActivityHours ,其中包含以下架构:

| Id | SupplierId | Day (enum) | OpenHour         | CloseHour        |
----------------------------------------------------------------------
| 1  | 1          | 0          | 05:00:00.0000000 | 15:00:00.0000000 |
| 2  | 1          | 1          | 05:00:00.0000000 | 15:00:00.0000000 |
| 3  | 1          | 2          | 05:00:00.0000000 | 16:00:00.0000000 |

我创建了一个设置页面,供应商可以更新其数据。

问题是,更新活动时间表的正确方法是什么?
供应商可能会删除天数,添加天数,更新现有天数。

我想到了以下几个选项:

  1. 当用户提交更新的数据时,我将删除" SupplierActivityHours"中的所有行。表(特定供应商的所有行) 然后添加新值。 这种方法的问题在于效率不高。如果用户只在一天中更新一小时怎么办?
  2. 我将在客户端持有3个列表
    • 要删除的ID列表
    • 要更新的项目列表(行ID和更新数据)
    • 要添加的新项目列表

      这种方法的问题是我认为对于这种常见的操作来说有点过于复杂。
  3. BTW,我正在使用ASP.NET MVC Core,MS SQL和Dapper.NET(如果它以某种方式重要)。

    有没有更好的选择我不知道?谢谢大家!

    更新

    我目前正在阅读" MERGE",试着看看它是否可以解决我的问题。

4 个答案:

答案 0 :(得分:2)

是的,您可以这样使用MERGE:

create type SupplierActivityHoursType as table 
(
    [Id] [int] NOT NULL,
    [SupplierId] [int] NULL,
    [Day] [int] NULL,
    [OpenHour] [datetime] NULL,
    [CloseHour] [datetime] NULL
)
go

CREATE PROCEDURE UpdateSupplierActivityHours
    @SupplierActivityHours dbo.SupplierActivityHoursType readonly
AS
BEGIN
    merge SupplierActivityHours as t
    using (select Id, SupplierId, [Day], OpenHour, CloseHour  
           from @SupplierActivityHours) as s
    on t.Id = s.Id, t.SupplierId = s.SupplierId
    when matched then
        update set t.[Day] = s.[Day], t.OpenHour = s.OpenHour, t.CloseHour = s.CloseHour
    when not matched by  target then
        insert (SupplierId, [Day], OpenHour, CloseHour) 
            values (s.SupplierId, s.[Day], s.OpenHour, s.CloseHour)
    when not matched by source then
        delete;
END

因此,要更新所有供应商数据,您需要将您的小时表传递给UpdateSupplierActivityHours SP。

当在SupplierActivityHours中找到记录时,它会被更新,当它在@SupplierActivityHours中找到但在SupplierActivityHours表中找不到时,它将被插入,如果在@SupplierActivityHours中找不到但在SupplierActivityHours中找到它将被删除。

MERGE语句允许在一个事务中有效地执行所有这些插入,更新和删除。

答案 1 :(得分:0)

虽然你说“擦除/更新”方法效率不高,但最容易实现。

当您有多个用户编辑相同的数据时,您必须阻止其他人编辑表格或告诉他们“上次编辑获胜”;即编辑将被覆盖。

这对您的用户来说是否有问题取决于您,并且取决于同时编辑数据的潜在用户数量 - 您可能会发现这绝不是问题。

如果您担心丢失编辑,可以实施数据的审核日志并记录所有更改。

答案 2 :(得分:0)

此表架构和模式是我考虑将代理Id作为主键删除并考虑使用SupplierId, Day作为主键的少数几个实例之一。此版本的merge过程无论如何都会忽略Id。除非您还要通过SupplierId, Day之外的其他内容获取或按行设置查询,否则我认为不需要Id

最后,我建议你做一些你对Id感到满意的事情。

表格设置:

create table dbo.Suppliers (
    SupplierId int primary key
  , SupplierName nvarchar(64)
);

insert into dbo.Suppliers values
(1, 'Good Company');

create table dbo.SupplierActivityHours (
    Id int not null
  , SupplierId int not null
  , [Day] tinyint not null
  , OpenHour time(0) not null
  , CloseHour time(0) not null
  , constraint pk_SupplierActivityHours primary key clustered (Id)
  , constraint fk_SupplierActivityHours_Suppliers foreign key (SupplierId)
      references Suppliers(SupplierId)
  );

create unique nonclustered index
  uix_SupplierActivityHours_SupplierId_Day
    on dbo.SupplierActivityHours (SupplierId, [Day])
      include (OpenHour, CloseHour);
/* If the Supplier can have multiple open/close per day,
 move OpenHour from the include() to the index key. */

insert into dbo.SupplierActivityHours values
    (1,1,0,'05:00:00.0000000','15:00:00.0000000')
  , (2,1,1,'05:00:00.0000000','15:00:00.0000000')
  , (3,1,2,'05:00:00.0000000','16:00:00.0000000');

表格类型:

create type dbo.udt_SupplierActivityHours as table (
  /* You will not have an ID for new rows, so no need for it here */
    SupplierId int not null
  , [Day] tinyint not null
  , OpenHour time(0) not null
  , CloseHour time(0) not null
  , unique (SupplierId,[Day]) /* unnamed unique constraint */
  --, primary key clustered (SupplierId, [Day]) /* instead of unique constraint */
  /* If the Supplier can have multiple open/close per day,
     add OpenHour to the unique constraint or primary key. */
);

合并程序:

go
create procedure dbo.SupplierActivityHours_Merge (
    @SupplierActivityHours dbo.udt_SupplierActivityHours readonly
) as
begin;
  set nocount, xact_abort on; /* you should always include this. */
  begin try
    begin tran
      merge SupplierActivityHours with (holdlock) as t
        /* with (holdlock) prevents race conditions in merge */
      using (select SupplierId, [Day], OpenHour, CloseHour
             from @SupplierActivityHours) as s
        on t.SupplierId = s.SupplierId
          and t.[Day] = s.[Day]
      when matched and (
             t.OpenHour  != s.OpenHour
          or t.CloseHour != s.CloseHour
      )
        then
          update set
              t.OpenHour  = s.OpenHour
            , t.CloseHour = s.CloseHour
      when not matched by target
        then insert (SupplierId, [Day], OpenHour, CloseHour)
          values (s.SupplierId, s.[Day], s.OpenHour, s.CloseHour)
      when not matched by source then
          delete
          /* check output for testing: */
          --output $action, deleted.*, inserted.*, s.*
          ;
    commit tran
  end try
  begin catch;
    if @@trancount > 0
      begin;
        rollback transaction;
        throw; /* or other error handling */
      end;
  end catch;
end;
go

在上面的表类型中,我包含了一个唯一的约束来防止重复。唯一约束将产生一些开销。如果这个开销是一个问题,并且您有其他安全措施来防止传入数据出现问题,请将其删除。

唯一约束的替代方法是使用群集在表类型上的主键,这也会产生开销。有一些方法可以通过保证已经从应用程序中订购的数据添加到数据中来减轻一些开销,但它非常复杂。

为什么你应该总是包括set nocount, xact_abort on; - Erland Sommarskog

使用merge时应注意的事项:

表值参数参考:

以下是如何执行三部分操作而不是merge的示例。希望这看起来不像你想要的那么复杂:

create type dbo.udt_ActivityHours as table (
    [Day] tinyint not null
  , OpenHour time(0) not null
  , CloseHour time(0) not null
  , unique ([Day])
  -- If the Supplier can have multiple open/close per day,
  -- add OpenHour to the unique constraint or primary key
);

go
create procedure dbo.SupplierActivityHours_Set_BySupplierId (
    @SupplierId int not null
  , @ActivityHours dbo.udt_ActivityHours readonly
) as
begin;
  set nocount, xact_abort on; -- you should always use these.
  begin try
    begin tran

    /* delete */
      delete sah 
        from dbo.SupplierActivityHours sah
          where sah.SupplierId = @SupplierId
            and not exists (
              select 1 
                from @ActivityHours ah
                  where ah.[Day] = sah.[Day]
                );

    /* update */
      update sah set 
          sah.OpenHour  = ah.OpenHour
        , sah.CloseHour = ah.CloseHour
        from SupplierActivityHours sah
          inner join @ActivityHours ah on sah.[Day] = ah.[Day]
           and sah.SupplierId = @SupplierId
        where sah.OpenHour  != ah.OpenHour
           or sah.CloseHour != ah.CloseHour;

    /* insert */
      insert into dbo.SupplierActivityHours 
                (SupplierId, [Day], OpenHour, CloseHour)
         select @SupplierId, [Day], OpenHour, CloseHour
          from @ActivityHours ah
          where not exists (
              select 1 
                from dbo.SupplierActivityHours sah 
                  where sah.SupplierId = @SupplierId
                    and ah.[Day] = sah.[Day]
                );

    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw; 
      end;
  end catch;
end;
go
TVP似乎会在rextester上造成死锁,因此rextester使用临时表替身。 (tvp deadlock example

rextester:http://rextester.com/DCUPNF63408

答案 3 :(得分:0)

首先,让我们回想一下SQL是一个relational set-based theory,因此它代表数据库,其解决方案对于一切的关系价值是有意义的。 这也使我们从整体上思考,包括涉及的网络流量以及对数据库的影响,碎片等。我们的设计没有浪费任何东西。

此外,Schemas中的SQL Server相当于tablespace中的Oracle,或者像房屋城市中的住房协会。模式具有自己的安全性并包含多个表。表包含多个列。

你的桌子设计

/*---------------------------------
|   DIMENSION - dbo.Suppliers
*--------------------------------*/
CREATE TABLE dbo.Suppliers (Supplier_ID INT NOT NULL
                          , Supplier_Name NVARCHAR(100) NOT NULL)
/*---------------------------------
|   FACT - dbo.ActivityHours_Suppliers
*--------------------------------*/
CREATE TABLE dbo.ActivityHours_Suppliers
                         (  Row_ID INT IDENTITY(1,1) NOT NULL
/* At best a database key, but your design makes it useless.*/
                         ,  Supplier_ID INT NOT NULL
/* Value, whether you have a constraint or not comes from dbo.Suppliers*/
                         ,  Day_enum INT
/* Is this a Durable Key?*/
                         ,  OpenHour  TIME NOT NULL
                         ,  CloseHour TIME NOT NULL) 

因此,您希望在以下情况下反映此FACT表中的更改:

  1. UPDATE ActivityHours_Suppliers表值OpenHour,CloseHour
  2. INSERT ActivityHours_Suppliers表Day_enum,OpenHour,CloseHour?
  3. 来自ActivityHours_Suppliers的
  4. DELETE,其中Day_enum是非法的。 - 虽然后一个问题应该在它到达数据库之前处理。
  5. 观察和问题:

    • 您的Day_enum列是否会永久前进?

    如果是,您刚刚创建了durable key。如果不是,请考虑使用其他列以允许将来进行区分。虽然,我希望这与客户控制分开

    • 这些更改何时发生?

    在一天的开始?间隔?在一天结束?住(可能不是)?如果不是在当天结束时或开始之前,您是否允许在CloseHour中使用NULL值?这可能会使更新更容易。

    • 您需要的是临时表。

    临时表是更好的长期解决方案,可以在系统上更轻松地管理插入/更新,因为您使用的是基于集合的解决方案。

    特别是如果您不打算实时计划,在登陆数据库之前,临时表将充当另一个缓冲区。

    • 应自动输入Supplier_ID。

    也许通过GUI使用的GUI引用了DIM表。从中创建一个新的供应商SEPARATE。你希望这永远不会有错。

    你可以....可以使用两个表,一个用于更新,一个用于插入,但由于这是一个临时表,这有关系吗?你是否正在为这些表做任何事情,需要两个单独的表?

    您的StagingTable会保留以下值:     Supplier_ID, Day_enum, OpenHour, CloseHour

    • MERGE是一个昂贵的过程。

    如果您认为每次都插入和删除了大量行,那么请考虑其优势在于对表行进行大量修改。

    如果您的金额不同,那么合并MERGE和CTE可能是合适的(这是一个简单的检查)IF <someNumber> < (SELECT COUNT(1) FROM TempTable)

    /*---------------------------------
    |       UPDATING FIELDS
    *--------------------------------*/
    WITH CTE AS (SELECT Supplier_ID, Day_enum, OpenHour, CloseHour
                 FROM TempTable)
    
    UPDATE A
        SET OpenHour = B.OpenHour
          , CloseHour = C.CloseHour
        FROM MYTABLE A
        INNER JOIN CTE B ON B.Supplier_ID = A.Supplier_ID
                        AND B.Day_enum = B.Day_enum
    
    /*---------------------------------
    |       INSERTING FIELDS
    *--------------------------------*/
    ;WITH CTE AS (SELECT Supplier_ID, Day_enum
                 FROM dbo.Suppliers
                  --use this as an opportunity to filter the rows or not at all)
    
    INSERT INTO MYTABLE (Supplier_ID, Day_enum, OpenHour, CloseHour)
    SELECT Supplier_ID, Day_enum, OpenHour, CloseHour
    FROM TempTable
    WHERE NOT EXISTS (SELECT 1 
                      FROM CTE
                      WHERE ROWS = TempTable.ROWS)
    

    (NOT)EXISTS返回true或false值,因此我们不需要在SELECT语句中指定列。 或者你可以限制对部分的影响......也许是区域,州,城市,如下所示

    ;WITH CTE AS (SELECT A.Supplier_ID, A.Day_enum, A.OpenHour, A.CloseHour
                 FROM TempTable A
                 INNER JOIN dbo.Suppliers B ON B.Supplier_ID = A.Supplier_ID
                                           AND B.Day_enum = A.Day_Enum
                 WHERE A.Day_enum > (somenumber)
                 -- A.Day_enum IS NOT NULL
                     )
    

    更好的是,考虑使用子查询替换SupplierTable,该子查询将DIM表限制为这些部分,区域,如下所示

    INNER JOIN (SELECT Supplier_ID, Day_enum
                FROM dbo.Suppliers
                WHERE Region_ID = 15
                    AND Region_ID = 20) AS B ON B.Supplier_ID = A.Supplier_ID
                                           AND B.Day_enum = A.Day_Enum
                 WHERE A.Day_enum IS NOT NULL
    

    / *您甚至可能会发现这种类型的JOIN对您的数据库* /

    有效
    SELECT A.Supplier_ID, A.Day_enum, A.OpenHour, A.CloseHour
    FROM MYINSERT A
    LEFT OUTER JOIN dbo.ActivityHours_Suppliers B ON A.Supplier_ID = B.Supplier_ID 
                             AND A.Day_enum = B.Day_enum
    WHERE A.Day_enum IS NOT NULL
    

    这将返回左侧匹配的每一行,并过滤掉每一行为NULL,因此只提供需要插入的行。

    最后,请始终进行测试,因为表格的大小和范围可以决定什么是真正的解决方案。

    我希望我能给你很多考虑并帮助你提供长期解决方案!

    来源: KimballGroup(07/2012)。 Durable Super-Natural Keys