Методы типа данных XML
Тип данных XML впервые появился в SQL Server 2005.
Он может содержать до 2 Гб данных.
В языке Transact-SQL имеется пять методов для работы с типом данных XML:
- query() – используется для извлечения XML фрагментов из XML документов;
- value() – используется для извлечения значений конкретных узлов или атрибутов XML документов;
- exist() – используется для проверки существования узла или атрибута. Возвращает 1, если узел или атрибут найден, и 0, если не найден;
- modify() – изменяет XML документ;
- nodes() – разделяет XML документ на несколько строк по узлам.
Методы типа данных XML принимают на вход выражение XPath или запрос XQuery.
Для примеров в данном учебнике будем использовать базу данных с таблицами, содержащими столбцы типа данных XML.
Таблица tArtist содержит информацию о музыкальных группах, исполнителях и их альбомах.
CREATE TABLE dbo.tArtist (
artistId INT NOT NULL PRIMARY KEY
,name VARCHAR(100) NOT NULL
,xmlData XML NOT NULL
);
Заполним таблицы тестовыми данными.
INSERT INTO dbo.tArtist (artistId, name, xmlData) VALUES
(1, 'Radiohead',
'<albums>
<album title="The King of Limbs">
<labels>
<label>Self-released</label>
</labels>
<song title="Bloom" length="5:15"/>
<song title="Morning Mr Magpie" length="4:41"/>
<song title="Little by Little" length="4:27"/>
<song title="Feral" length="3:13"/>
<song title="Lotus Flower" length="5:01"/>
<song title="Codex" length="4:47"/>
<song title="Give Up the Ghost" length="4:50"/>
<song title="Separator" length="5:20"/>
<description link="http://en.wikipedia.org/wiki/The_King_of_Limbs">
The King of Limbs is the eighth studio album by English rock band Radiohead,
produced by Nigel Godrich. It was self-released on 18 February 2011 as a
download in MP3 and WAV formats, followed by physical CD and 12" vinyl
releases on 28 March, a wider digital release via AWAL, and a special
"newspaper" edition on 9 May 2011. The physical editions were released
through the band''s Ticker Tape imprint on XL in the United Kingdom,
TBD in the United States, and Hostess Entertainment in Japan.
</description>
</album>
<album title="OK Computer">
<labels>
<label>Parlophone</label>
<label>Capitol</label>
</labels>
<song title="Airbag" length="4:44"/>
<song title="Paranoid Android" length="6:23"/>
<song title="Subterranean Homesick Alien" length="4:27"/>
<song title="Exit Music (For a Film)" length="4:24"/>
<song title="Let Down" length="4:59"/>
<song title="Karma Police" length="4:21"/>
<song title="Fitter Happier" length="1:57"/>
<song title="Electioneering" length="3:50"/>
<song title="Climbing Up the Walls" length="4:45"/>
<song title="No Surprises" length="3:48"/>
<song title="Lucky" length="4:19"/>
<song title="The Tourist" length="5:24"/>
<description link="http://en.wikipedia.org/wiki/OK_Computer">
OK Computer is the third studio album by the English alternative rock band
Radiohead, released on 16 June 1997 on Parlophone in the United Kingdom and
1 July 1997 by Capitol Records in the United States. It marks a deliberate
attempt by the band to move away from the introspective guitar-oriented
sound of their previous album The Bends. Its layered sound and wide range
of influences set it apart from many of the Britpop and alternative rock
bands popular at the time and laid the groundwork for Radiohead''s later,
more experimental work.
</description>
</album>
</albums>'),
(2, 'Guns N'' Roses',
'<albums>
<album title="Use Your Illusion I">
<labels>
<label>Geffen Records</label>
</labels>
<song title="Right Next Door to Hell" length="3:02"/>
<song title="Dust N'' Bones" length="4:58"/>
<song title="Live and Let Die (Paul McCartney and Wings cover)" length="3:04"/>
<song title="Don''t Cry (original version)" length="4:44"/>
<song title="Perfect Crime" length="2:24"/>
<song title="You Ain''t the First" length="2:36"/>
<song title="Bad Obsession" length="5:28"/>
<song title="Back Off Bitch" length="5:04"/>
<song title="Double Talkin'' Jive" length="3:24"/>
<song title="November Rain" length="8:57"/>
<song title="The Garden (featuring Alice Cooper and Shannon Hoon)" length="5:22"/>
<song title="Garden of Eden" length="2:42"/>
<song title="Don''t Damn Me" length="5:19"/>
<song title="Bad Apples" length="4:28"/>
<song title="Dead Horse" length="4:18"/>
<song title="Coma" length="10:13"/>
<description link="http://ru.wikipedia.org/wiki/Use_Your_Illusion_I">
Use Your Illusion I is the third studio album by GnR. It was the first of two
albums released in conjunction with the Use Your Illusion Tour, the other
being Use Your Illusion II. The two are thus sometimes considered a double album.
In fact, in the original vinyl releases, both Use Your Illusion albums are
double albums. Material for all two/four discs (depending on the medium) was
recorded at the same time and there was some discussion of releasing a
''quadruple album''. The album debuted at No. 2 on the Billboard charts, selling
685,000 copies in its first week, behind Use Your Illusion II''s first week sales
of 770,000. Use Your Illusion I has sold 5,502,000 units in the U.S. as of 2010,
according to Nielsen SoundScan. It was nominated for a Grammy Award in 1992.
</description>
</album>
<album title="Use Your Illusion II">
<labels>
<label>Geffen Records</label>
</labels>
<song title="Civil War" length="7:42"/>
<song title="14 Years" length="4:21"/>
<song title="Yesterdays" length="3:16"/>
<song title="Knockin'' on Heaven''s Door (Bob Dylan cover)" length="5:36"/>
<song title="Get in the Ring" length="5:41"/>
<song title="Shotgun Blues" length="3:23"/>
<song title="Breakdown" length="7:05"/>
<song title="Pretty Tied Up" length="4:48"/>
<song title="Locomotive (Complicity)" length="8:42"/>
<song title="So Fine" length="4:06"/>
<song title="Estranged" length="9:24"/>
<song title="You Could Be Mine" length="5:43"/>
<song title="Don''t Cry (Alternate lyrics)" length="4:44"/>
<song title="My World" length="1:24"/>
<description link="http://ru.wikipedia.org/wiki/Use_Your_Illusion_II">
Use Your Illusion II is the fourth studio album by GnR. It was one of two albums
released in conjunction with the Use Your Illusion Tour, and as a result the two
albums are sometimes considered a double album. Bolstered by lead single ''You
Could Be Mine'', Use Your Illusion II was the slightly more popular of the two
albums, selling 770,000 copies its first week and debuting at No. 1 on the U.S.
charts, ahead of Use Your Illusion I''s first week sales of 685,000.
</description>
</album>
</albums>');
Метод query()
В общем случае этот метод принимает на вход выражение XPath и возвращает новый XML документ.
Выражение XPath ‘/albums/album[2]/labels/label[1]’ указывает, что мы хотим получить первый лейбл второго альбома для каждого исполнителя.
Метод query() возвращает фрагмент XML документа, содержащий всё между начальным и конечным тегами элемента “label”, включая и сам элемент.
select name, xmlData.query('/albums/album[2]/labels/label[1]') as SecondAlbumLabel
from dbo.tArtist;
name | SecondAlbumLabel |
---|---|
Radiohead | <label>Parlophone</label> |
Guns N’ Roses | <label>Geffen Records</label> |
Рассмотрим другой пример. Нужно найти такие альбомы, в описании которых есть слово “record”.
select name,
xmlData.query ('/albums/album[description[contains(., "record")]]') as ContainsRecord
from dbo.tArtist;
Если детально разобрать выражение XPath, то можно его описать следующей фразой: найти в списке альбомов (albums) такие альбомы (album), у которых имеется описание (description), содержащее слово “record” (contains(., “record”)). Точка в функции contains() означает обращение к содержимому текущего элемента, в данном случае это элемент description.
name | ContainsRecord |
---|---|
Radiohead | |
Guns N’ Roses | <album title="Use Your Illusion I">...</album> |
Видим, что такой альбом нашёлся только у группы Guns N’ Roses. Но у группы Radiohead в описании альбома OK Computer встречается слово “Records”. Этот альбом не нашёлся, т.к. XQuery чувствителен к регистру. Чтобы данный альбом тоже был найден, используем функцию lower-case(), приводящую обрабатываемый текст к нижнему регистру.
select name,
xmlData.query ('/albums/album[description[contains(lower-case(.), "record")]]')
as ContainsRecord
from dbo.tArtist;
name | ContainsRecord |
---|---|
Radiohead | <album title="OK Computer">...</album> |
Guns N’ Roses | <album title="Use Your Illusion I">...</album> |
Метод value()
Метод value() позволяет извлекать из XML документа содержимое единичного элемента или атрибута с указанием его типа данных.
Если выражение XPath указывает на несколько узлов, то будет возвращено сообщение об ошибке. Например,
select name,
xmlData.value('/albums/album[2]/labels/label[1]/text()', 'varchar(100)')
as SecondAlbumLabel
from dbo.tArtist;
Эта ошибка сообщает, что данное XPath-выражение может вернуть более одной сущности. Например, если бы в исходном XML документе было несколько элементов “albums” или более одного элемента “labels”.
Чтобы исправить эту ошибку, замените выражение XPath на одно из следующих:
- ‘/albums[1]/album[2]/labels[1]/label[1]/text()[1]’
- ‘(/albums/album[2]/labels/label/text())[1]’
Результат будет одинаковым:
name | SecondAlbumLabel |
---|---|
Radiohead | Parlophone |
Guns N’ Roses | Geffen Records |
Ещё один пример с типом данных “time”. Допустим, мы хотим получить для каждого исполнителя название первого альбома, первой песни из этого альбома и её длительность.
select name
, xmlData.value('/albums[1]/album[1]/@title', 'varchar(100)') as FirstAlbum
, xmlData.value('/albums[1]/album[1]/song[1]/@title', 'varchar(100)') as FirstSongTitle
, xmlData.value('/albums[1]/album[1]/song[1]/@length', 'time(0)') as FirstSongLength
from dbo.tArtist;
name | FirstAlbum | FirstSongTitle | FirstSongLength |
---|---|---|---|
Radiohead | The King of Limbs | Bloom | 05:15:00 |
Guns N’ Roses | Use Your Illusion I | Right Next Door to Hell | 03:02:00 |
К сожалению, поле “FirstSongLength” распозналось в формате чч:мм:сс, тогда как мы уверены, что в XML-документе время записано в формате мм:сс. Чтобы исправить эту ошибку, можно использовать обычный синтаксис T-SQL, а можно воспользоваться XQuery:
select name as artist
, xmlData.value('/albums[1]/album[1]/@title', 'varchar(100)') as FirstAlbum
, xmlData.value('/albums[1]/album[1]/song[1]/@title', 'varchar(100)') as FirstSongTitle
, xmlData.value('concat("00:", /albums[1]/album[1]/song[1]/@length)', 'time(0)')
as FirstSongLength
from dbo.tArtist;
artist | FirstAlbum | FirstSongTitle | FirstSongLength |
---|---|---|---|
Radiohead | The King of Limbs | Bloom | 00:05:15 |
Guns N’ Roses | Use Your Illusion I | Right Next Door to Hell | 00:03:02 |
Метод exist()
Данный метод не используется для получения каких-либо данных из XML документа. Он используется только для того, чтобы убедиться, что в нём присутствуют требуемые данные. Метод exist() проверяет существование указанного выражения XPath.
В данном примере метод exist() будет использован для определения, есть ли определённая песня в списке композиций. Пусть песня, которую мы ищем, называется “Garden of Eden”.
SELECT name, xmlData.exist('/albums[1]/album/song[@title="Garden of Eden"]') AS SongExists
from dbo.tArtist;
name | SongExists |
---|---|
Radiohead | 0 |
Guns N’ Roses | 1 |
Как можно видеть, песня с таким названием присутствует среди композиций группы Guns N’ Roses (1 эквивалентно ‘True’), и отсутствует среди композиций группы Radiohead (0 эквивалентно ‘False’).
Теперь проверим наличие песни длительностью более 10 минут.
SELECT name
, xmlData.exist('/albums[1]/album/song[@length>"10:00"]') AS LongSongExists
from dbo.tArtist;
name | SongExists |
---|---|
Radiohead | 1 |
Guns N’ Roses | 1 |
Из полученного результата мы видим, что оба исполнителя имеют в своём репертуаре как минимум одну песню длительностью более 10 минут. Но это не так. На самом деле все песни группы Radiohead длятся менее 10 минут.
Проблема заключается в том, что значение атрибута @length по умолчанию принимает строковый тип данных. Получается, что мы сравнивали строки, а не время.
Чтобы решить эту проблему, мы можем преобразовать значение атрибута @length и значение, с которым оно сравнивается, к типу данных time. Оба они должны быть приведены к формату “чч:мм:сс” до преобразования типов данных. Для этого будем использовать функцию xs:time(), принимающую на вход строку и возвращающую время.
SELECT name
, xmlData.exist('
/albums[1]/album/song/@length[
(
if (string-length(.) = 4)
then xs:time(concat("00:0", .))
else xs:time(concat("00:", .))
)
> xs:time("00:10:00")
]') AS LongSongExists
from dbo.tArtist;
Теперь мы видим, что в репертуаре группы Radiohead нет ни одной песни длительностью более 10 минут, в то время как в репертуаре группы Guns N’ Roses есть как минимум одна такая песня.
name | SongExists |
---|---|
Radiohead | 0 |
Guns N’ Roses | 1 |
В данном примере функция time() используется в пространстве имён xs. Все встроенные функции преобразования типов должны использоваться в этом пространстве имён (xs:string, xs:boolean, xs:decimal, xs:float, xs:double, xs:dateTime, xs:time, xs:date и т.д.). Прочие встроенные функции используются в пространстве имён fn, но его указание не обязательно. То есть string-length(.) и fn:string-length(.) эквивалентны.
Метод modify()
Метод modify() позволяет изменять значения непосредственно в XML-документе.
Так же, как и другие методы, он требует выражение XPath, чтобы понять, какое значение нужно изменить. Однако, в отличие от других методов, modify() работает с оператором UPDATE (и не работает с оператором SELECT). Также, modify() может работать только с одним значением за раз. В математике и программировании это называется одноэлементным множеством.
Так как не существует никакого ограничения на количество элементов, содержащихся внутри другого элемента, выражение XPath может возвращать много дочерних элементов. Например, если имеется такой XML документ:
<week>
<day>Monday</day>
<day>Tuesday</day>
<day>Wednesday</day>
</week>
выражение XPath /week/day возвращает три элемента, которые не являются одноэлементным множеством:
<day>Monday</day>
<day>Tuesday</day>
<day>Wednesday</day>
Однако, если заменить выражение XPath на (/week/day)[1], то будет возвращён только один элемент “Monday”.
<day>Monday</day>
Попробуем получить название первого лейбла альбома “OK Computer” группы “Radiohead”.
SELECT xmlData.query('(/albums/album[@title="OK Computer"]/labels/label/text())[1]')
AS FirstLabelText
from dbo.tArtist
WHERE name = 'Radiohead';
FirstLabelText |
---|
Parlophone |
Метод modify() имеет единственной целью изменение значений в XML-документе, что является полезной возможностью. Представьте, что XML-документ уже импортирован на SQL Server, и обнаружилась опечатка или нужно обновить только одно значение. Чтобы не загружать заново уже исправленный XML-документ, можно просто использовать метод modify() и изменить необходимые значения непосредственно в сохранённом XML-документе на SQL Server.
В следующем примере мы поменяем местами названия лейблов альбома “OK Computer” группы “Radiohead”. Первый лейбл должен быть заменён на “Capitol”, а второй – на “Parlophone”.
Метод modify() может быть использован в предложении SET оператора UPDATE по отношению к переменной или столбцу типа данных XML.
UPDATE dbo.tArtist
SET xmlData.modify('replace value of
(/albums/album[@title="OK Computer"]/labels/label/text())[1] with "Capitol"')
WHERE name = 'Radiohead';
UPDATE dbo.tArtist
SET xmlData.modify('replace value of
(/albums/album[@title="OK Computer"]/labels/label/text())[2] with "Parlophone"')
WHERE name = 'Radiohead';
Отлично – исправленный запрос отработал корректно, что подтверждается сообщениями:
(1 row(s) affected)
(1 row(s) affected)
Теперь вернёмся к исходному запросу (с использованием оператора SELECT) и выполним его, чтобы проверить, что названия лейблов были обновлены корректно.
SELECT xmlData.query('/albums/album[@title="OK Computer"]/labels')
FROM dbo.tArtist
WHERE name = 'Radiohead';
<labels>
<label>Capitol</label>
<label>Parlophone</label>
</labels>
Эта задача может быть решена по-другому. Можно поменять местами элементы “label” без изменения их значений. Просто вставим копию первого лейбла в конец списка лейблов.
UPDATE dbo.tArtist
SET xmlData.modify('insert (/albums/album[@title="OK Computer"]/labels/label)[1] as last
into (/albums/album[@title="OK Computer"]/labels)[1]')
WHERE name = 'Radiohead';
После чего удалим первый лейбл.
UPDATE dbo.tArtist
SET xmlData.modify('delete (/albums/album[@title="OK Computer"]/labels/label)[1]')
WHERE name = 'Radiohead';
Чтобы удостовериться, что лейблы поменялись местами корректно, выполним следующий запрос:
SELECT xmlData.query('/albums/album[@title="OK Computer"]/labels')
FROM dbo.tArtist
WHERE name = 'Radiohead';
<labels>
<label>Parlophone</label>
<label>Capitol</label>
</labels>
Теперь попробуем отметить песню “Perfect Crime” из альбома “Use Your Illusion I” группы Guns N’ Roses, как популярную. Для этого добавим в XML-документ в соответствующий элемент атрибут isPopular со значением 1.
UPDATE dbo.tArtist
SET xmlData.modify('
insert attribute isPopular { "1" }
into (/albums[1]/album[@title="Use Your Illusion I"]/song[@title="Perfect Crime"])[1]
')
WHERE name = 'Guns N'' Roses';
В качестве ещё одного примера добавим информацию о дате выпуска и дате записи песни “Estranged” из альбома “Use Your Illusion II” группы Guns N’ Roses. Обратите внимание, что в данном примере в элемент добавляется сразу два атрибута.
UPDATE dbo.tArtist
SET xmlData.modify('
insert (
attribute Recorded { "1991" },
attribute Released { "1994-01-17" }
)
into (/albums[1]/album[@title="Use Your Illusion II"]/song[@title="Estranged"])[1]
')
WHERE name = 'Guns N'' Roses';
Метод nodes()
Разбивает XML-структуру на одно или несколько поддеревьев, в соответствии с указанным выражением XPath.
Выполним следующий запрос:
SELECT name as artist
, album.query('.') as album
FROM dbo.tArtist A
CROSS APPLY A.xmlData.nodes('/albums[1]/album')col(album);
Данный запрос разобьет исходную структуру на строки, по количеству элементов “album” и вернет по две строки для каждого исполнителя:
artist | album |
---|---|
Radiohead | <album title="The King of Limbs"><labels><label>S... |
Radiohead | <album title="OK Computer"><labels><label>Parlop... |
Guns N’ Roses | <album title="Use Your Illusion I"><labels><label>G... |
Guns N’ Roses | <album title="Use Your Illusion II"><labels><label>G... |
Разберем текст запроса подробнее.
CROSS APPLY A.xmlData.nodes(’/albums[1]/album’) – разбивает каждую строку таблицы на столько строк, сколько элементов “album” было найдено.
col – это имя производной таблицы, а album – это имя столбца. Они нужны будут для дальнейшей работы с результатами.
album.query(’.’) – здесь осуществляется запрос к каждой строке результатов, при помощи псевдонима. Данный подзапрос просто выбирает все данные из поддерева.
Разберём другой пример. Допустим мы хотим получить в виде таблицы первые две песни каждого альбома для каждой из групп.
SELECT name as artist
, song.value('../@title', 'varchar(50)') as album
, song.value('@title', 'varchar(100)') as song
FROM dbo.tArtist A
CROSS APPLY A.xmlData.nodes('/albums[1]/album/song[position()<=2]')col(song);
artist | album | song |
---|---|---|
Radiohead | The King of Limbs | Bloom |
Radiohead | The King of Limbs | Morning Mr Magpie |
Radiohead | OK Computer | Airbag |
Radiohead | OK Computer | Paranoid Android |
Guns N’ Roses | Use Your Illusion I | Right Next Door to Hell |
Guns N’ Roses | Use Your Illusion I | Dust N’ Bones |
Guns N’ Roses | Use Your Illusion II | Civil War |
Guns N’ Roses | Use Your Illusion II | 14 Years |
[position()<=2] – указывает, что нам нужны только два первых элемента “song” внутри каждого элемента “album”.
‘../@title’ – обращается к родительскому элементу и берёт его атрибут @title.
Результат отобразился корректно, но при больших объёмах данных такой запрос будет крайне медленным и неэффективным. Причина заключается в том, что для каждой песни среда выполнения запроса ищет родительский элемент и считывает его атрибут. Перепишем запрос следующим образом:
SELECT name as artist
, album.value('@title', 'varchar(50)') as album
, song.value('@title', 'varchar(100)') as song
FROM dbo.tArtist A
CROSS APPLY A.xmlData.nodes('/albums[1]/album')c1(album)
CROSS APPLY c1.album.nodes('song[position()<=2]')c2(song);
Результат будет аналогичным, однако теперь логика запроса изменена следующим образом: сначала выбираются все альбомы, а потом для каждого альбома будут присоединяться песни.
Поскольку песен всегда будет намного больше, чем альбомов, выполнение данного запроса покажет существенный прирост производительности по сравнению с предыдущим.