使用约束复制一组具有共同祖先的记录

时间:2009-04-07 20:00:03

标签: sql sql-server tsql

我有一组实际上是粗短树的表。在顶部,有一个客户,下面是发票和发票明细记录。 (实际上,这些表中大约有二十个都是指客户,但原则只适用于三个表。)

我想要做的是复制客户和属于该客户的所有记录,而不必枚举每条记录中的每个字段。一切都是外键关键的,而且大多数表都有自动增加的身份字段。

以下是用于设置数据库的T-SQL脚本。是的它很乱,但它已经完成了。

CREATE TABLE [dbo].[Customer](
    [custID] [int] IDENTITY(1,1) NOT NULL,
    [name] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED ( [custID] ASC)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,
 ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY] ) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Invoice](
    [invoiceNum] [int] IDENTITY(1,1) NOT NULL,
    [custID] [int] NOT NULL,
    [Description] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Invoice] PRIMARY KEY CLUSTERED ( [invoiceNum] ASC )
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
 ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY] ) ON [PRIMARY]
GO
CREATE TABLE [dbo].[InvoiceDetail](
    [invoiceNum] [int] NOT NULL,
    [sequence] [smallint] NOT NULL,
    [description] [varchar](50) NOT NULL,
    [price] [decimal](10, 2) NOT NULL CONSTRAINT [DF_InvoiceDetail_price]  DEFAULT ((0.0)),
 CONSTRAINT [PK_InvoiceDetail] PRIMARY KEY CLUSTERED ( [invoiceNum] ASC, [sequence] ASC )
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
 ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY] ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Invoice]  WITH CHECK ADD  CONSTRAINT [FK_Invoice_Customer] 
    FOREIGN KEY([custID])
    REFERENCES [dbo].[Customer] ([custID])
GO
ALTER TABLE [dbo].[Invoice] CHECK CONSTRAINT [FK_Invoice_Customer]
GO
ALTER TABLE [dbo].[InvoiceDetail]  WITH CHECK ADD  CONSTRAINT [FK_InvoiceDetail_Invoice] 
    FOREIGN KEY([invoiceNum])
    REFERENCES [dbo].[Invoice] ([invoiceNum])
GO
ALTER TABLE [dbo].[InvoiceDetail] CHECK CONSTRAINT [FK_InvoiceDetail_Invoice]

declare @id int;
declare @custid int;
insert into Customer values ('Bob');
set @custid = @@IDENTITY;
insert into Invoice values ( @custid, 'Little Purchase');
set @id = @@IDENTITY;
insert into InvoiceDetail values (@id, 1, 'Small Stuff', 1.98);
insert into InvoiceDetail values (@id, 2, 'More Small Stuff', 0.25);
insert into Invoice values ( @custid, 'Medium Purchase');
set @id = @@IDENTITY;
insert into InvoiceDetail values (@id, 1, 'Stuff', 11.95);
insert into InvoiceDetail values (@id, 2, 'More Stuff', 10.66);
insert into Customer values ('Sally');
set @custid = @@IDENTITY;
insert into Invoice values ( @custid, 'Big Purchase');
set @id = @@IDENTITY;
insert into InvoiceDetail values (@id, 1, 'BIG Stuff', 100.00);
insert into InvoiceDetail values (@id, 2, 'Larger Stuff', 99.95);

所以我想做的是在这个数据库中复制“Bob”,并将其命名为“Bob2”,而不必为每个表指定每列的麻烦。我可以,但在真实世界中有很多专栏。

另一个问题是我必须编写一个显式循环来获取每张发票。我需要先前的发票插入中的标识才能编写发票明细。

我有一个正在运行的C#“复制”程序,但我想在数据库中完成所有这些工作。天真的实现是一个transact sql存储过程,其中包含循环和游标。

是否有聪明的方式避免这些问题中的一个(如果不是两个)?

2 个答案:

答案 0 :(得分:1)

我遇到了更多涉及更多表格的类似问题。我们确实可以避免为每个要复制的行创建游标。唯一的游标是循环所涉及的表名列表。我们还需要动态SQL。与传统的游标循环解决方案相比,整个操作非常快。

诀窍是将相关行插入到相同的表中;然后将其FK列更新为其父级。我们如何收集质量@@ identity是在插入过程中使用'output'关键字,并将它们保存到临时表#refTrack中。稍后我们将#refTrack与所涉及的表一起加入他们的FK。

我们知道:


create table #refTrack 
(
    tbl sysname,
    id int, 
    refId int
)

insert InvoiceDetail (refId, invoiceNum, sequence, description, price)
output 'InvoiceDetail', inserted.id, inserted.refId into #refTrack 
select invoiceNum, invoiceNum, sequence, description, price from InvoiceDetail 
where custID = 808 -- denormalized original Bob^s custID

将使用新创建的自动运行数字列表填充临时#refTrack表。我们的工作就是将此插入查询设置为动态。

这种方法的唯一缺点是我们需要在每张桌子上都有一致性:

  1. 自己的主键,名称为“id”。在这种情况下,我们需要重命名:Customer.custID成为Customer.id; Invoice.invoiceNum成为Invoice.id;和InvoiceDetail中的新列'id int identity(1,1)主键'。
  2. 非规范化的'custID'列。对于使用'depth'>列出的表格1,该表将要求当前的前端应用程序填充这个新的帮助器列。 “插入触发器”将使我们的工作更复杂。
  3. 一个名为'refId'的列,定义为:int null。此列用于将属于“Bob2”的行关系作为“Bob”的副本。
  4. 采取的步骤:

    一个。将所有表名列入@tList表变量

    
    declare @tList table
    (
         tbl sysname primary key,
         fkTbl sysname,
         fkCol sysname,
         depth int
    )
    insert @tList select 'Customer', null, null, 0
    insert @tList select 'Invoice', 'Customer', 'custID', 1
    insert @tList select 'InvoiceDetail', 'Invoice', 'invoiceNum', 2
    

    我喜欢抽象,只是在上面插入时填充'tbl'列;并通过使用information_schema视图的递归CTE结果更新它们来动态填充其余列。然而,这可能与此无关。假设我们有一个表格,其中包含所涉及的表名列表,按照它应该填充的方式排序。

    B中。将@tList表循环到游标中。

    
    declare 
        @depth int,
        @tbl sysname,
        @fkTbl sysname,
        @fkCol sysname,
        @exec nvarchar(max),
        @insCols nvarchar(max),
        @selCols nvarchar(max),
        @where nvarchar(max),
        @newId int,
        @mainTbl sysname,
        @custId int 
    
    
    select @custId = 808 -- original Bob^s custID to copy from
    
    select @mainTbl = tbl from @tList where fkTbl is null
    
    declare dbCursor cursor local forward_only read_only for  
        select tbl, fkTbl, fkCol, depth from @tlist order by depth
    open dbCursor   
    fetch next from dbCursor into @tbl, @fkTbl, @fkCol, @depth 
    while @@fetch_status = 0   
    begin   
        set @where = case when @depth = 0 then 'Id' else 'custId' end + ' = ' + 
            cast(@custId as nvarchar(20))
        set @insCols = dbo.FnGetColumns(@tbl) 
        set @selCols = replace
        (
            @insCols, 
            'refId', 
            'Id'
        )
        set @exec = 'insert ' + @tbl + ' (' + @insCols + ') ' + 
            'output ''' + @tbl + ''', inserted.id, inserted.refId into #refTrack ' +
            'select ' + @selCols + ' from ' + @tbl + ' where ' + @where
    
        print @exec
        exec(@exec)
    
        -- remap parent
        if isnull(@fkTbl, @mainTbl) != @mainTbl -- third level onwards
        begin
            set @exec = 'update ' + @tbl + ' set ' + @tbl + '.' + @fkCol + ' = rf.Id from ' + 
                @tbl + ' join #refTrack as rf on ' + @tbl + '.' + @fkCol + ' = rf.refId and rf.tbl = ''' + 
                @fkTbl + ''' where ' + @tbl + '.custId = ' + cast(@newId as nvarchar(20))
    
            print @exec
            exec(@exec)
        end
    
        if @depth = 0 select @newId = Id from #refTrack
        fetch next from dbCursor into @tbl, @fkTbl, @fkCol, @depth 
    end   
    
    close dbCursor
    deallocate dbCursor
    
    select * from @tList order by depth
    select * from #refTrack
    
    drop table #refTrack 
    

    ℃。 FnGetColumns()的内容:

    
    create function FnGetColumns(@tableName sysname) 
    returns nvarchar(max)
    as
    begin
        declare @cols nvarchar(max)
        set @cols = ''
        select @cols = @cols + ', ' + column_name 
            from information_schema.columns 
            where table_name = @tableName
                and column_name <> 'id' -- non PK
        return substring(@cols, 3, len(@cols))
    end
    

    我相信我们可以进一步改进这些脚本,使其更加动态。但是为了解决问题,这将是最低要求。

    干杯,

    阿里。

答案 1 :(得分:0)

“大多数表都有自动增加标识字段”

存在部分问题。使用IDENTITY作为PK使这些操作既困难又昂贵(从计算的角度来看)。即使你没有使用IDENTITY,你仍然需要为“新”客户生成新的发票号码,这意味着你需要一次循环一个或者想出来基于集合的分配新发票编号的方法,然后可用于创建发票明细行。

我将假设您从业务角度理解您正在进行的工作,但我仍然需要指出您现在也在创建不完全“真实”的数据。如果您复制其中一个客户,包括他们的所有发票,然后您报告您当年的销售额,那么您将重复计算销售额。

有关您尝试解决的业务问题的更多信息,可能会找到另一种解决方案。