Эта запись продолжает вопрос, поднятый в теме Извлекаем схему таблицы – сложный вариант.
Зачем?
У вас никогда не возникало желание сказать 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
Инструкции
Если вы желаете повторить описанную функциональность, необходимо выполнить четыре шага:
- Создать 'системную' базу данных. Я назвал её
SYSDB
; - Подключить функции CLR отсюда;
- Запустить скрипты:
- Сохранить информацию о типах данных;
- Создать и выполнить процедуру обновления схем таблиц.
- Создать процедуру для копирования строки.
- Пользоваться.