Devdrama

О сайте

Копирование строки любой таблицы in english

03 Apr 2014

Эта запись продолжает вопрос, поднятый в теме Извлекаем схему таблицы – сложный вариант.

Зачем?

У вас никогда не возникало желание сказать MS SQL

Скопируй строку [AdventureWorks2008].[SalesLT].[Product] с ключом ProductID=986!

и он бы просто её скопировал? Не так, что вы перечислили все столбцы в блоках INSERT и SELECT. Наоборот, вы написали: пожалуйста, скопируй. Было бы прекрасно? И если вдруг изменится схема таблицы, вам не пришлось перечислять ещё один столбец.

Позвольте продемонстрирую.

Не смог скопировать строку

Пардон, он сказал, что мешается уникальный индекс. Хорошо. На этот раз попросим вежливее:

Будь добр, скопируй строку [AdventureWorks2008].[SalesLT].[Product] с ключом ProductID=986, но установи новый GUID для столбца rowguid, новое Name и новый ProductNumber?

Просим вежливо:

Успешно скопировали строку

Вот и всё. MSSQL только что скопировал строку таблицы, и нам не пришлось указывать столбцы в INSERT или SELECT. Порой это бывает очень полезно.

Как работает

Если вам не терпится повторить, ищите инструкции в конце страницы.

Мы храним схемы таблиц в особой таблице в нашей 'системной' базе данных. Когда наступает нужный момент, мы можем извлечь набор столбцов в виде текстового перечисленя, т.е. col1, col2, col3 и создать динамический запрос, слепив строки вроде INSERT INTO и НАША_ТАБЛИЦА и (col1, col2, col3) и SELECT и col1, col2, col3 и FROM и НАША_ТАБЛИЦА и WHERE $IDENTITY= и OUR_PRODUCT_ID_VALUE. Более того, если нужно изменить некоторые данные для новой строи, мы просто заменяем названия некоторых столбцов colN явными значениями, например SELECT col1, 'new value', col3 FROM НАША_ТАБЛИЦА.

Как только мы получили запрос, мы его выполняем, и происходит магия.

Если мы когда-либо добавим в нашу таблицу новые столбцы, мы просим особую процедуру 'обновить' схемы таблиц, и новый столбец уже 'посчитали'. Таким образом, нам не нужно следить за схемой таблиц — копирование учтёт новый столбец автоматически.

Теперь о внутренностях

Если вас интересует, как оно устроено — прошу пожаловать.

Требования

Скрипт подразумевает работу с регулярными выражениями, агрегационной функцией конкатенации и массивами. Нужная для этого CLR DLL размещена здесь.

Храним схемы таблиц в одном месте

Если вы знакомы с предыдущей записью, вы заметите, что вместо отображения столбцов отдельно взятой таблицы, мы сохраняем схемы всех таблиц всех баз данных без исключения.

if OBJECT_ID('SYSDB.._table_schemas') IS NULL
    CREATE TABLE SYSDB.._table_schemas(
        [db] [varchar](32) NOT NULL,
        [object_id] [int] NOT NULL,
        [name] [sysname] NULL,
        [type] [sysname] NOT NULL,
        [column_id] [int] NOT NULL,
        [is_nullable] [bit] NULL,
        [is_identity] [bit] NOT NULL,
        [length] [varchar](10) NULL,
        [precision] [varchar](10) NULL,
        [scale] [varchar](10) NULL,
        [StrLen] [smallint] NOT NULL,
        [LITERAL_PREFIX] [varchar](32) NULL,
        [LITERAL_SUFFIX] [varchar](32) NULL,
        [primary_key] [varchar](1) NULL
    )
if (ISNULL(@dbname,'')='') set @objname=NULL
if ISNULL(@objname,'')<>'' set @obj_id=OBJECT_ID(@objname)
DECLARE @tmp_db TABLE (num int identity, dbname varchar(64))

INSERT INTO @tmp_db(dbname) SELECT [name] FROM master..[sysdatabases] WHERE[name]=ISNULL(@dbname,[name])

if (ISNULL(@dbname,'')='') truncate table SYSDB.._table_schemas
else delete from SYSDB.._table_schemas where db=@dbname and object_id=ISNULL(@obj_id,object_id)

select @i=0, @cnt=isnull((select MAX(num) from @tmp_db),0),@dbname=null
while @i<@cnt begin set @i=@i+1 select @dbname=dbname from @tmp_db where num=@i

    set @sql="INSERT INTO SYSDB.._table_schemas SELECT '" +@dbname+"' db, C.object_id, C.name, Tp.name AS type, C.column_id, C.is_nullable, C.is_identity,
            (CASE when ST.CREATE_PARAMS is null then '' WHEN CHARINDEX(',',ST.CREATE_PARAMS)>0 then CAST(Tp.precision AS varchar(10)) ELSE CAST(C.max_length AS varchar(10)) END) AS length,
            (CASE WHEN CHARINDEX(',',ST.CREATE_PARAMS)>0 then CAST(C.scale AS varchar(10)) ELSE '' END) AS scale,
            (CASE WHEN CHARINDEX(',',ST.CREATE_PARAMS)>0 then CAST(C.precision AS varchar(10)) ELSE '' END) AS precision,
            (CASE WHEN C.precision > 0 THEN C.precision WHEN C.precision = 0 AND C.max_length > 0 THEN C.max_length ELSE Tp.max_length END) AS StrLen,
            ST.LITERAL_PREFIX COLLATE Latin1_General_CI_AS AS LITERAL_PREFIX, ST.LITERAL_SUFFIX COLLATE Latin1_General_CI_AS AS LITERAL_SUFFIX,
            (CASE WHEN t2.is_primary_key=1 THEN 'Y' END) AS primary_key
        FROM "+@dbname+".sys.columns AS C JOIN "+@dbname+".sys.types AS Tp ON(C.system_type_id = Tp.system_type_id)
                JOIN SYSDB.._datatype_info AS ST ON(Tp.name COLLATE Cyrillic_General_CI_AS = ST.TYPE_NAME)
                left join "+@dbname+".sys.index_columns t1 on(t1.column_id = C.column_id and t1.object_id = C.object_id)
                left JOIN "+@dbname+".sys.indexes t2 on(t1.object_id = t2.object_id AND t1.index_id = t2.index_id)"
        +(case when ISNULL(@obj_id,0)>0 then " WHERE C.object_id="+CAST(@obj_id as varchar) else "" end)
    exec(@sql)
end

Так мы сохраним схемы таблиц.

Копирование строки

Это ключевая процедура, которая и производит копирование:

CREATE PROCEDURE [dbo].[_CopyRow]
    @tableFullName varchar(1000)        /**@param Full table name (AdventureWorks2008.SalesLT.Product)*/
    ,@keyID         bigint              /**@param Source row ID (identity value)*/
    ,@keyIDout      bigint=null out     /**@param Returned value of inserted identity*/
    ,@todo          varchar(max)=null   /**@param String of values to change (col1=2;col2=;col3=newval)*/
AS
BEGIN
set nocount on;
    DECLARE @dbname     varchar(30)
    DECLARE @query      nvarchar(max)
    DECLARE @columns    varchar(max)
    DECLARE @key_column varchar(100)
    DECLARE @is_identity    int
    DECLARE @tmp        varchar(100)
    DECLARE @errDesc    varchar(max)

    SET @dbname = SYSDB.dbo.arrayAt(@tableFullName,0,'.')

    IF NOT EXISTS(select 1 from sys.databases AS db WHERE db.name=isnull(@dbname,''))BEGIN
        SELECT 'Database not found'
        RETURN END
    /*********** full description of the table columns **************************************/
    CREATE TABLE #tmp_bfColumn([db] [varchar](32) NOT NULL,[object_id] [int] NOT NULL,[name] [sysname] NULL,[type] [sysname] NOT NULL,
        [column_id] [int] NOT NULL,[is_nullable] [bit] NULL,[is_identity] [bit] NOT NULL,[length] [varchar](10) NULL,[precision] [varchar](10) NULL,
        [StrLen] [smallint] NOT NULL,[LITERAL_PREFIX] [varchar](32) NULL,[LITERAL_SUFFIX] [varchar](32) NULL,[primary_key] [varchar](1) NULL)
    INSERT INTO #tmp_bfColumn([db],[object_id],[name],[type],[column_id],[is_nullable],[is_identity],[length],[precision],[StrLen],[LITERAL_PREFIX],[LITERAL_SUFFIX],[primary_key])
        SELECT [db],[object_id],[name],[type],[column_id],[is_nullable],[is_identity],[length],[precision],[StrLen],[LITERAL_PREFIX],[LITERAL_SUFFIX],[primary_key]
            FROM SYSDB.._table_schemas
        WHERE [OBJECT_ID]=OBJECT_ID(@tableFullName) AND db=@dbname

    SELECT @columns = SYSDB.dbo.list(distinct v.name,',') FROM #tmp_bfColumn as v WHERE [name] not in('InsTime','UpdTime','other unwanted columns') and v.is_identity<>1 and v.type<>'sysname'

    SELECT @key_column = v.name FROM #tmp_bfColumn as v WHERE v.primary_key='Y' OR v.is_identity=1

    BEGIN TRY
        BEGIN TRAN linecopy
        IF LEN(@columns)=0 OR ISNULL(@key_column,'')='' BEGIN
            ROLLBACK TRAN
            SELECT 'The table does not exist or it does not have identity/primary key'
            RETURN END

        /*********** column names and suffixes/prefixes **************************************/
        CREATE TABLE #tmp_col (_name varchar(max), LITERAL_PREFIX varchar(10), LITERAL_SUFFIX varchar(10))
        INSERT INTO #tmp_col (_name, LITERAL_PREFIX, LITERAL_SUFFIX) SELECT v.name as _name, d.LITERAL_PREFIX , d.LITERAL_SUFFIX
            FROM #tmp_bfColumn as v JOIN SYSDB.._datatype_info AS d ON (v.type=d.TYPE_NAME)

        /*********** split @todo from 'a=1;b=text;c=3' into table (col,colvalue) *************/

        CREATE TABLE #new_values(value varchar(4000), col varchar(100), colval varchar(4000))
        INSERT INTO #new_values(value) SELECT Match FROM SYSDB.dbo.RegExSplit(@todo,';',0)
        IF @@TRANCOUNT=0 RETURN
        /*********** split pairs 'a=1' into two columns: name and value **********************/
        UPDATE #new_values SET col=left(value,CHARINDEX('=',value)-1), colval=STUFF(value,1,CHARINDEX('=',value),'')
        UPDATE #new_values SET colval = (case WHEN colval='NULL' THEN 'NULL' ELSE ISNULL(#tmp_col.LITERAL_PREFIX,'')+colval+ ISNULL(#tmp_col.LITERAL_SUFFIX,'') END)
            FROM #tmp_col WHERE #tmp_col._name=#new_values.col
        /*********** all columns with new values *********************************************/
        CREATE TABLE #t_all(value varchar(4000), col varchar(100), colval varchar(4000))
        INSERT INTO #t_all(value) SELECT Match FROM SYSDB.dbo.RegExSplit(@columns,',',0)
        IF @@TRANCOUNT=0 RETURN
        update #t_all set colval=(select max(tx.colval) from #new_values tx where tx.col=#t_all.value)
        update #t_all set colval=CAST(@keyIDout AS varchar(max)) where value=@key_column

        /****** assemble the query, replacing column names with new values if necessary ******/
        SELECT @query = 'INSERT INTO '+@tableFullName+'('+APSYS.dbo.list(t.value,',')+')'
            +' SELECT '+APSYS.dbo.list(case when t.colval is null then t.value else t.colval+' '+t.value end,',')
            +' FROM '+ @tableFullName + ' WHERE ' + @key_column + '=' + CAST(@keyID as varchar(10))
        FROM #t_all as t

        EXEC(@query) IF @@ROWCOUNT>0 SET @keyIDout=@@IDENTITY ELSE RAISERROR('Record not created', 16, 1)

        COMMIT TRAN linecopy
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT=0 RETURN
        ROLLBACK TRAN
        SELECT ERROR_MESSAGE() as ERROR_MESSAGE,@query as query
        SET @keyIDout = -1
    END CATCH
set nocount off;
END
GO

Мы создаём временную таблицу и сохраняем в ней информацию о нашей рабочей таблице (той самой, где копируем). Мы также определяем столбец IDENTITY. Затем мы подготавливаем суффикс и префикс (LITERAL_PREFIX и LITERAL_SUFFIX) на случай новых явных значений. Например, INSERT INTO table(int_col, varchar_col, varbinary_col) SELECT 1, 'text', 0xCA010. Символы '' и 0x как раз ими и являются. Мы разделяем массив @todo (например, a=10;b=new text;c=value) в таблицу, чтобы создать пары ключ => значение. В последней таблице мы подменяем названия колонок на переданные новые значения. Остальные столбцы остаются перечисилены без изменений.

Заключительный шаг — собрать запрос, использую функцию агрегации SYSDB.dbo.list.

SELECT @query = 'INSERT INTO '+@tableFullName+'('+SYSDB.dbo.list(t.value,',')+')'
    +' SELECT '+SYSDB.dbo.list(case when t.colval is null then t.value else t.colval+' '+t.value end,',')
    +' FROM '+ @tableFullName + ' WHERE ' + @key_column + '=' + CAST(@keyID as varchar(10))
FROM #t_all as t

Инструкции

Если вы желаете повторить описанную функциональность, необходимо выполнить четыре шага:

  1. Создать 'системную' базу данных. Я назвал её SYSDB;
  2. Подключить функции CLR отсюда;
  3. Запустить скрипты:
  4. Пользоваться.
sql