Копирование набора записей с ограничениями с общим предком - PullRequest
0 голосов
/ 08 апреля 2009

У меня есть набор таблиц, на самом деле это короткое дерево. Вверху находится Клиент, а под ним - Счета-фактуры и Записи о счете-фактуре. (На самом деле существует около двух десятков этих таблиц, все из которых относятся к Заказчику, но принцип должен применяться только с тремя таблицами.)

Что я хотел бы сделать, так это скопировать Клиента и все записи, принадлежащие этому Клиенту, без необходимости перечислять каждое отдельное поле в каждой записи. Все внешние ключи ограничены тем, что находится над ним, и большинство таблиц имеют поля идентификаторов с автоприращением.

Ниже приведен скрипт 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 #, но я хотел бы сделать все это в базе данных. Наивная реализация - это транзакционная хранимая процедура sql с повсеместными циклами и курсорами.

Есть ли умный способ избежать одной (если не обеих) этих проблем?

Ответы [ 2 ]

1 голос
/ 08 апреля 2009

У меня была похожая проблема с гораздо большим количеством таблиц. Мы действительно можем избежать создания курсора для каждой строки, которая будет скопирована. Единственный курсор предназначен для зацикливания списка задействованных имен таблиц. Нам также понадобится динамический SQL для этого. Вся операция выполняется очень быстро по сравнению с традиционным решением с циклом курсора.

Хитрость заключается в том, чтобы вставить соответствующие строки в те же таблицы; и затем обновите его столбец FK до его родителя. Как мы можем получить массовую @@ идентичность, используя ключевое слово 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; и новый столбец «id int identity (1, 1) первичный ключ» в InvoiceDetail.
  2. Денормализованный столбец custID. Для таблиц, перечисленных с 'глубиной'> 1, для таблицы потребуется текущее приложение внешнего интерфейса для заполнения этого нового вспомогательного столбца. «Триггер вставки» сделает нашу работу немного сложнее.
  3. Столбец с именем 'refId', определяемый как: int null. Этот столбец предназначен для того, чтобы связать строки, принадлежащие «Bob2», как копию «Bob».

Предпринятые шаги:

а. Вывести все имена таблиц в табличную переменную @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' во время вставки выше; и динамически заполнять остальные столбцы, обновляя их результатом рекурсивного CTE представлений information_schema. Однако это может быть не относящимся к делу. Предположим, у нас есть таблица со списком имен таблиц, упорядоченная по способу ее заполнения.

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 

C. Содержимое 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

Я уверен, что мы сможем улучшить эти сценарии, чтобы они стали более динамичными. Но для решения проблемы это было бы минимальным требованием.

Приветствия

Ари.

0 голосов
/ 08 апреля 2009

"большинство таблиц имеют поля идентификаторов с автоинкрементом"

Есть часть проблемы. Использование IDENTITY в качестве PK делает такие операции сложными и дорогостоящими (с вычислительной точки зрения). Даже если вы не использовали IDENTITY, вам все равно нужно будет сгенерировать новые номера счетов для «нового» клиента, что означает, что вам нужно будет либо циклически переходить по одному за раз, либо придумывать основанный на множестве метод назначения новых номеров счетов, которые затем можно использовать для создания строк сведений о счете.

Я собираюсь предположить, что вы понимаете, во что вы ввязываетесь с точки зрения бизнеса, но я все же должен отметить, что вы также сейчас создаете данные, которые не совсем "реальны". Если вы копируете одного из этих клиентов, включая все их счета-фактуры, а затем сообщаете о своих продажах за год, в котором будет учитываться двойной объем продаж.

Имея больше информации о бизнес-проблеме, которую вы пытаетесь решить, возможно, будет найдено другое решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...