2009-04-07 7 views
0

J'ai un ensemble de tables qui est en fait un arbre tronqué. En haut, il y a un client, et en dessous de ces factures, et des enregistrements de détails de facture. (En réalité, il y a environ deux douzaines de ces tables faisant toutes référence au Client mais le principe ne devrait s'appliquer qu'à trois tables.)Copie d'un ensemble d'enregistrements avec des contraintes, avec un ancêtre commun

Ce que j'aimerais faire, c'est copier le Client et tous les enregistrements appartenant à ce client sans avoir à énumérer chaque champ dans chaque enregistrement. Tout est étranger-clé contraint à la chose au-dessus de lui, et la plupart des tables ont des champs d'identité d'auto-incrémentation.

Vous trouverez ci-dessous un script T-SQL pour configurer une base de données. Oui, c'est en désordre, mais c'est complet.

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); 

Donc ce que je veux faire est de faire une copie de « Bob » dans cette base de données, et l'appeler « BOB2 » sans tous les tracas de spécifier chaque colonne pour chaque table. Je pourrais, mais dans le monde réel c'est beaucoup de colonnes. L'autre problème étant que je devrais écrire une boucle explicite pour obtenir chacune des factures. J'ai besoin de l'identité de l'insertion de facture précédente pour écrire le détail de facture.

J'ai un programme de copie en C#, mais j'aimerais le faire dans la base de données. L'implémentation naïve est une procédure stockée sql transact avec des boucles et des curseurs partout.

Y a-t-il un astucieux pour éviter l'un de ces problèmes (si ce n'est les deux)?

Répondre

1

J'ai eu un problème similaire avec beaucoup plus de tables impliquées. Nous pouvons en effet éviter de faire du curseur pour chaque ligne à copier. Le seul curseur est pour boucler la liste des noms de tables impliqués. Nous allons également avoir besoin de SQL dynamique pour cela. L'ensemble de l'opération est extrêmement rapide, par rapport à la solution de boucle de curseur traditionnelle.

L'astuce consiste à insérer les lignes pertinentes dans les mêmes tables; puis mettez à jour sa colonne FK à son parent. La façon dont nous pouvons collecter l'identité mass @ @@ est en utilisant le mot clé 'output' lors de l'insertion, et les sauvegarder dans une table temporaire #refTrack. Plus tard, nous rejoignons #refTrack avec les tables impliquées pour mettre à jour leurs FK.

Nous savons que:


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 

renseignera table #refTrack temporaire une liste des numéros nouvellement créés de fonctionnement automatique. Notre travail consiste simplement à rendre cette requête d'insertion dynamique.

Le seul inconvénient de cette méthode est que nous avons besoin consistances, sur chaque table, nous devons avoir:

  1. clé primaire de son propre sous le nom de « id ». Dans ce cas, nous devons renommer: Customer.custID pour devenir Customer.id; Invoice.invoiceNum devient Invoice.id; et une nouvelle colonne 'id int identity (1, 1) clé primaire' dans InvoiceDetail.
  2. Colonne dénommée 'custID'. Pour les tables répertoriées avec 'depth'> 1, la table demandera à l'application frontale actuelle de remplir cette nouvelle colonne d'assistance. Un 'insert trigger' rendra notre travail un peu plus compliqué.
  3. Une colonne appelée 'refId', définie comme: int null. Cette colonne est pour faire la relation de lignes appartiennent à 'Bob2' comme une copie de 'Bob'.

Mesures prises:

A. Liste tous les noms de table dans la variable table @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 

J'aimerais aller abstraite à juste peuplant la colonne 'TBL' lors de l'insertion ci-dessus; et remplissez dynamiquement le reste des colonnes en les mettant à jour avec le résultat du CTE récursif des vues d'information_schema. Cependant, cela pourrait être à côté du point. Supposons que nous ayons une table avec la liste des noms de tables impliqués, ordonnée par la façon dont elle devrait être peuplée.

B. Bouclez la table @tList dans un curseur.


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 

C. Le contenu de 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 

Je suis sûr que nous pouvons encore améliorer ces scripts pour être beaucoup plus dynamique. Mais pour résoudre le problème, ce serait l'exigence minimale.

Cheers,

Ari.

0

« la plupart des tables ont des champs d'identité auto-incrément »

Il y a une partie du problème. L'utilisation d'IDENTITY en tant que PK rend ce type d'opérations à la fois difficile et coûteux (du point de vue informatique). Même si vous n'avez pas utilisé IDENTITY, vous devrez toujours générer de nouveaux numéros de facture pour le «nouveau» client, ce qui signifie que vous devrez soit faire un cycle à la fois, soit arriver avec une méthode d'attribution de nouveaux numéros de facture qui peut ensuite être utilisée pour créer les lignes de détail de facture.

Je vais supposer que vous comprenez ce que vous entreprenez d'un point de vue commercial, mais je dois quand même souligner que vous créez également des données qui ne sont pas vraiment «réelles». Si vous copiez l'un de ces clients, y compris toutes ses factures, puis que vous déclarez vos ventes pour l'année, vous allez comptabiliser deux fois les ventes.

Pour plus d'informations sur le problème commercial que vous essayez de résoudre, peut-être qu'une autre solution pourrait être trouvée.

+0

Il n'y a pas vraiment de "Clients" et de "Factures". Il serait inapproprié d'afficher un schéma réel dans un forum public. Ils imitent une vraie base de données cependant. –

Questions connexes