Во время разработки модульного теста SQL Server (ssut - см. Связанный пост в блоге) я хотел стандартизировать набор xml, полученный из тестируемого объекта. Поскольку я буду вызывать тестируемый объект несколько раз, каждый раз имена наборов и записей будут одинаковыми. Для удобства чтения я хочу, чтобы набор записей из исходных записей был назван аналогично <original_record_set><original_record /></original_record_set>
, а набор записей для
записи испытаний должны быть названы аналогично <test_record_set><test_record /></ test_record_set >
.
Очевидно, что это тривиально, если вы можете сначала изменить вызов в тестируемом объекте:
SET @output = (SELECT col1, col2
FROM @test_object_result
FOR xml path ( test_record '), root( test_record_set '));
и затем:
SET @output = (SELECT col1, col2
FROM @test_object_result
FOR xml path ( original_record'), root( original_record_set '));
Однако, поскольку я вызываю один и тот же объект несколько раз, а «для пути xml» НЕ допускает использование переменных в методах path('...')
и root('...')
, мне пришлось придумать другой метод.
Эта функция принимает дерево XML и создает новое дерево, заменяя корневой узел значением @relation_name
и именем каждой записи @tuple_name
. Новое дерево строится со всеми атрибутами оригинала, даже если в записи разные номера.
ИСКЛЮЧЕНИЯ
Очевидно, что это НЕ работает с несколькими уровнями элементов! Я построил его специально для обработки одноуровневого дерева на основе атрибутов, как показано в примере ниже. Я могу построить его для многоуровневого смешанного дерева атрибутов / элементов в будущем, но я думаю, что способ сделать это становится очевидным теперь, когда я решил основную проблему, как показано ниже, и оставлю это упражнение читателю в ожидании этого времени.
USE [unit_test];
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[standardize_record_set]') AND type IN ( N'FN', N'IF', N'TF', N'FS', N'FT' ))
DROP FUNCTION [dbo].[standardize_record_set];
GO
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
SET nocount ON;
GO
/*
DECLARE
@relation_name nvarchar(150)= N'standardized_record_set',
@tuple_name nvarchar(150)= N'standardized_record',
@xml xml,
@standardized_result xml;
SET @xml='<Root>
<row id="12" two="now1" three="thr1" four="four1" />
<row id="232" two="now22" three="thr22" />
<row id="233" two="now23" three="thr23" threeextra="extraattrinthree" />
<row id="234" two="now24" three="thr24" fourextra="mealsoin four rwo big mone" />
<row id="235" two="now25" three="thr25" />
</Root>';
execute @standardized_result = [dbo].[standardize_record_set] @relation_name=@relation_name, @tuple_name=@tuple_name, @xml=@xml;
select @standardized_result;
*/
CREATE FUNCTION [dbo].[standardize_record_set] (@relation_name nvarchar(150)= N'record_set',
@tuple_name nvarchar(150)= N'record', @xml xml )
returns XML
AS
BEGIN
DECLARE
@attribute_index int = 1,
@attribute_count int = 0,
@record_set xml = N'<' + @relation_name + ' />',
@record_name nvarchar(50) = @tuple_name,
@builder nvarchar(max),
@record xml,
@next_record xml;
DECLARE @record_table TABLE (
record xml );
INSERT INTO @record_table
SELECT t.c.query('.') AS record
FROM @xml.nodes('/*/*') T(c);
DECLARE record_table_cursor CURSOR FOR
SELECT cast([record] AS xml)
FROM @record_table
OPEN record_table_cursor
FETCH NEXT FROM record_table_cursor INTO @next_record
WHILE @@FETCH_STATUS = 0
BEGIN
SET @attribute_index=1;
SET @attribute_count = @next_record.query('count(/*[1]/@*)').value('.', 'int');
SET @builder = N'<' + @record_name + N' ';
-- build up attribute string
WHILE @attribute_index <= @attribute_count
BEGIN
SET @builder = @builder + @next_record.value('local-name((/*/@*[sql:variable("@attribute_index")])[1])',
'varchar(max)') + '="' + @next_record.value('((/*/@*[sql:variable("@attribute_index")])[1])',
'varchar(max)') + '" ';
SET @attribute_index = @attribute_index + 1
END
-- build record and add to record_set
SET @record = @builder + ' />';
SET @record_set.modify('insert sql:variable("@record") into (/*)[1]');
FETCH NEXT FROM record_table_cursor INTO @next_record
END
CLOSE record_table_cursor;
DEALLOCATE record_table_cursor;
RETURN @record_set;
END;
GO