Как создать XML определенного формата из таблицы MSSQL с помощью FOR XML PATH?

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

Код создания таблиц для тестов (данные от балды, просто чтобы показать суть того, что я имею и что хочу получить):

if object_id('dbo.TestAutoInfo', 'U') is not null
    drop table dbo.TestAutoInfo;
go

create table dbo.TestAutoInfo
(
    CityID int
   ,CarBrand varchar(20)
   ,CarModel varchar(20)
   ,CarBodyType varchar(20)
   ,CarYear smallint
   ,CarColor varchar(20)
)
go

insert into dbo.TestAutoInfo 
(
    CityID
   ,CarBrand
   ,CarModel
   ,CarBodyType
   ,CarYear
   ,CarColor
)
values
(1283,'Toyota','Corolla','Sedan',2013,'White')
,(1283,'Toyota','Rav4','SUV',2021,'Black')
,(1283,'Ford','Mustang','Coupe',2024,'Red')
go

select CityID
   ,CarBrand
   ,CarModel
   ,CarBodyType
   ,CarYear
   ,CarColor
from TestAutoInfo

Примечание к данным - CityID всегда одно и тоже в рамках этого блока

Что я хочу получить из этих данных:

  1. В идеале вот такое
<data>
    <field name="CityID">1283</field>
</data> 
<data>
    <field name="CarBrand">Toyota</field>
    <field name="CarModel">Corolla</field>
    <field name="CarBodyType">Sedan</field>
    <field name="CarYear">2013</field>
    <field name="CarColor">White</field>
</data>
<data>
    <field name="CarBrand">Toyota</field>
    <field name="CarModel">Rav4</field>
    <field name="CarBodyType">SUV</field>
    <field name="CarYear">2021</field>
    <field name="CarColor">Black</field>
</data>
<data>
    <field name="CarBrand">Ford</field>
    <field name="CarModel">Mustang</field>
    <field name="CarBodyType">Coupe</field>
    <field name="CarYear">2024</field>
    <field name="CarColor">Red</field>
</data>

Но если не такое, то хотя бы вот такое

<data>
  <field name="CityID">1283</field>
  <field name="CarBrand">Toyota</field>
  <field name="CarModel">Corolla</field>
  <field name="CarBodyType">Sedan</field>
  <field name="CarYear">2013</field>
  <field name="CarColor">White</field>
</data>
<data>
  <field name="CityID">1283</field> 
  <field name="CarBrand">Toyota</field>
  <field name="CarModel">Rav4</field>
  <field name="CarBodyType">SUV</field>
  <field name="CarYear">2021</field>
  <field name="CarColor">Black</field>
</data>
<data>
  <field name="CityID">1283</field>
  <field name="CarBrand">Ford</field>
  <field name="CarModel">Mustang</field>
  <field name="CarBodyType">Coupe</field>
  <field name="CarYear">2024</field>
  <field name="CarColor">Red</field>
</data>

Пробовал по разному, но никак не выходит. Если никто не сможет помочь кодом, то может хотя бы накидайте ссылок, пожалуйста, на адекватные статьи как работает этот "%:/** @ XML PATH с кучей примеров, а не просто два-три.

Привожу два решения, которые работают, но которые мне не нравятся по следующим причинам - первое мне не нравится тем, что оно лично на мой вкус оно кривое, да и еще необходим курсор для работоспособности. Второе ничем, по сути, не отличается от ручной сборки XML.

    @xml xml
    ,@xml_f xml

select @xml =
(
select CityID as "CityID"
       ,CarBrand as "CarBrand"
       ,CarModel as "CarModel"
       ,CarBodyType as "CarBodyType"
       ,CarYear as "CarYear"
       ,CarColor as "CarColor"
from TestAutoInfo
for xml path('data')  
)

--выведем промежуточный XML
select @xml

    declare @fields table(
     id int not null
    ,fname varchar(100)
    ,flabel varchar(100)
    )

    insert into @fields(id, fname, flabel)
    select 1, 'CityID', 'CityID'
    union all
    select 2, 'CarBrand', 'CarBrand'
    union all
    select 3, 'CarModel', 'CarModel'
    union all
    select 4, 'CarBodyType', 'CarBodyType'
    union all
    select 5, 'CarYear', 'CarYear'
    union all
    select 6, 'CarColor', 'CarColor'

    --Какая-то неизвестная магия
    set @xml_f = (
                select c.col_name as 'field/@name'
                        ,isnull(@xml.value('(//*[local-name()=sql:column("c.col_name")])[1]','varchar(max)'),'') as 'field'
                from (select fname as col_name from @fields) c
                for xml path(''), root('data')
                )

--Выведем окончательный XML и поймем, что он обрабатывает только один блок, поэтому и нужен курсор
select @xml_f 

Второе решение от ИИ следующее

(
    SELECT 
         distinct 'CityID' AS 'field/@name',
        CityID AS 'field'
    FROM TestAutoInfo
    FOR XML PATH('data'), TYPE
)
UNION ALL
(
    SELECT 
    (
        SELECT 
            CarBrand,
            CarModel,
            CarBodyType,
            CarYear,
            CarColor
        FROM TestAutoInfo
        FOR XML PATH('data'), TYPE
    ).query('
        for $d in /data
        return 
        <data>
            {
                for $c in $d/*
                return 
                <field name="{local-name($c)}">{data($c)}</field>
            }
        </data>
    ')
)
FOR XML PATH(''), TYPE```

Ответы (3 шт):

Автор решения: rotabor

Особо ничего делать не нужно:

SELECT *
  FROM [dbo].[TestAutoInfo]
  FOR XML PATH ('Data')

Можно повыбирать поля:

SELECT CarBrand,CarModel,CarBodyType,CarYear,CarColor
  FROM [dbo].[TestAutoInfo]
  FOR XML PATH ('Data')

Это:

<data>
    <field name="CityID">1283</field>
</data> 
<data>
    <field name="CarBrand">Toyota</field>
...

неправильные выходные данные, может быть как-то так:

<City>
  <field name="CityID">1283</field>
  <data>
    <field name="CarBrand">Toyota</field>
  </data> 
  <data>
    <field name="CarBrand">Toyota</field>
  </data> 
</City>

Как это сделать - читайте тут.

→ Ссылка
Автор решения: Yitzhak Khabinsky

Попробуйте следующее решение.

Если корневой элемент не нужен, просто закомментируйте , ROOT('root').

SQL

DECLARE @TestAutoInfo TABLE
(
    CityID int
   ,CarBrand varchar(20)
   ,CarModel varchar(20)
   ,CarBodyType varchar(20)
   ,CarYear smallint
   ,CarColor varchar(20)
);

INSERT @TestAutoInfo 
(
    CityID
   ,CarBrand
   ,CarModel
   ,CarBodyType
   ,CarYear
   ,CarColor
)
values
(1283,'Toyota','Corolla','Sedan',2013,'White'),
(1283,'Toyota','Rav4','SUV',2021,'Black'),
(1283,'Ford','Mustang','Coupe',2024,'Red');

SELECT 'CityID' AS [field/@name]
    , CityID AS [field] 
    , ''
    , 'CarBrand' AS [field/@name]
    , CarBrand AS [field] 
    , ''
    , 'CarModel' AS [field/@name]
    , CarModel AS [field] 
    , ''
    , 'CarBodyType' AS [field/@name]
    , CarBodyType AS [field] 
    , ''
    , 'CarYear' AS [field/@name]
    , CarYear AS [field] 
    , ''
    , 'CarColor' AS [field/@name]
    , CarColor AS [field] 
FROM @TestAutoInfo
FOR XML PATH('data'), TYPE, ROOT('root');

Результат

<root>
  <data>
    <field name="CityID">1283</field>
    <field name="CarBrand">Toyota</field>
    <field name="CarModel">Corolla</field>
    <field name="CarBodyType">Sedan</field>
    <field name="CarYear">2013</field>
    <field name="CarColor">White</field>
  </data>
  <data>
    <field name="CityID">1283</field>
    <field name="CarBrand">Toyota</field>
    <field name="CarModel">Rav4</field>
    <field name="CarBodyType">SUV</field>
    <field name="CarYear">2021</field>
    <field name="CarColor">Black</field>
  </data>
  <data>
    <field name="CityID">1283</field>
    <field name="CarBrand">Ford</field>
    <field name="CarModel">Mustang</field>
    <field name="CarBodyType">Coupe</field>
    <field name="CarYear">2024</field>
    <field name="CarColor">Red</field>
  </data>
</root>
→ Ссылка
Автор решения: Alexander Petrov
SELECT CAST(N'' AS XML).query('
  element data {
    element field { attribute name {"CityId"},      text {sql:column("CityId")}      },
    element field { attribute name {"CarBrand"},    text {sql:column("CarBrand")}    },
    element field { attribute name {"CarModel"},    text {sql:column("CarModel")}    },
    element field { attribute name {"CarBodyType"}, text {sql:column("CarBodyType")} },
    element field { attribute name {"CarYear"},     text {sql:column("CarYear")}     },
    element field { attribute name {"CarColor"},    text {sql:column("CarColor")}    }
  }')
FROM TestAutoInfo

Конструируем XML фактически вручную, но зато очень наглядно.

XML будет не единым целым, а по строкам.


Узел с CityId можно получать отдельно:

SELECT CAST(N'' AS XML).query('
  element data {    
    element field { attribute name {"CityId"},      text {sql:column("CityId")}      }
  }')
FROM TestAutoInfo
GROUP BY CityId

UNION ALL

SELECT CAST(N'' AS XML).query('
  element data {    
    element field { attribute name {"CarBrand"},    text {sql:column("CarBrand")}    },
    element field { attribute name {"CarModel"},    text {sql:column("CarModel")}    },
    element field { attribute name {"CarBodyType"}, text {sql:column("CarBodyType")} },
    element field { attribute name {"CarYear"},     text {sql:column("CarYear")}     },
    element field { attribute name {"CarColor"},    text {sql:column("CarColor")}    }
  }')
FROM TestAutoInfo

Впрочем, можно ещё проще:

SELECT CAST(N'' AS XML).query('
  <data>
    <field name="CarBrand">      {sql:column("CarBrand")}   </field>
    <field name="CarModel">      {sql:column("CarModel")}   </field>
    <field name="CarCarBodyType">{sql:column("CarBodyType")}</field>
    <field name="CarYear">       {sql:column("CarYear")}    </field>
    <field name="CarColor">      {sql:column("CarColor")}   </field>
  </data>
')
FROM TestAutoInfo
→ Ссылка