Viacheslav Eremin | Qmail Reference
 
 
 
XML в MS SQL Server 2000 и технологиях доступа к данным.
Автор: Алексей Шуленин

1. Введение
2. Сохранить как XML
3. Получить как поток XML
4. XML на стороне сервера. FOR XML
5. SQLXML-классы в .NET
6. FOR XML на стороне клиента
7. XML-представление наборов данных в ADO .NET
8. Прямые XPath-запросы к объектам SQL Server
9. Аннотированные схемы
10. XML-шаблоны как разновидность хранимых процедур
11. UpdateGrams
12. XML Bulk Load
13. Доступ к SQL Server по HTTP
14. SQL Server как Web-сервис
15. Заключение

1 Введение

Несколько слов о том, что за текст попался вам на глаза и стоит ли вам его читать, а мне, соответственно, браться сейчас писать. Ну поддерживает SQL Server XML, ну так что с того? Его сейчас поддерживают все, кому не лень, потому что это круто. А если разобраться по существу, то с какого бока приличному серверу баз данных этот XML вообще сдался? Вот с этого философского вопроса, пожалуй, и начнем. С моей сугубо прагматичной точки зрения это, конечно, не дань моде. Наши с вами реалии сегодня таковы, что большинство корпоративных бизнес-сценариев давно вышли за рамки локальных сетей и предусматривают работу с базами данных через Интернет. Кроме того, эта работа ведется чаще всего в гетерогенных средах, где перемешаны и Windows, и Linux, и Solaris, и FreeBSD, и много чего еще. Во-первых, представление как запроса, так и его результатов в виде XML существенно упрощает передачу данных через межсетевые экраны. Понятно, например, что передать recordset как COM-объект через брандмауэр скорее всего не удастся, потому что ни один администратор в здравом уме не откроет порты для произвольных RPC-вызовов снаружи. Издавна придумывались лазейки и средства: вспомните, например, Remote Data Service (RDS), появившуюся еще в составе ADO 1.5 во второй половине 1997 г. (а ее предшественник Advanced Data Connector - ADC - и того раньше). Она позволила-таки маршалировать recordset'ы через HTTP и DCOM, хотя и не скажу, что это было тривиально. Сериализация объекта в XML решает эту задачу легко. Во-вторых, XML и HTTP, являясь де юре и де факто общепринятыми стандартами, упрощают взаимодействие между базами данных различных производителей, на какой бы платформе и ОС они ни стояли. Причем, даже не только между базами данных, но и напрямую с серверами электронной коммерции и бизнес-интеграции. Например, с Commerce Server, BizTalk Server и др. Причем не только с серверами промежуточного слоя, но и вообще между гетерогенными приложениями, поскольку SOAP легко решает извечную задачу мостования между СОМ и CORBA. Впрочем, о SQL Server как о веб-сервисе мы еще поговорим. В-третьих, преимущество поддержки XML в СУБД состоит в том, что на компьютер конечного пользователя или приложения, работающего с базой данных, не требуется устанавливать никакой клиентской части, специфичной для данной СУБД, т.к. все, что ему нужно, - это стандартные протоколы и форматы Интернета, априори поддерживаемые практически всеми современными платформами.
Итак, работа через Интернет, взаимодействие в гетерогенных средах и поддержка тонких мобильных клиентов - вот основные практические плюсы, которые получает СУБД, если умеет общаться на языке XML.
Кроме того, XML представляет собой одно из наиболее заманчивых направлений эволюции СУБД. Допустим на минуту, что наряду с реляционным механизмом в SQL Server появился встроенный XMLный движок, так что тип данных XML является для него теперь родным. Представим также, что SQL Server является полноценным .NET-сервером, т.е. программировать на стороне сервера можно не только на Т-SQL, но и на любом CLR-языке. Тогда любой класс .NET Framework можно не только выразить при помощи XML, но и хранить и обрабатывать средствами SQL Server. Что означает, ни много, ни мало, что из чистой РСУБД, каковой он является на данный момент, SQL Server плавно превращается в объектную. А это означает, в свою очередь, что все многообразие не реляционных структур и хранилищ, представленных нынче в семействе Windows (WebStore, файловая система, Active Directory и т.д.) элегантно сводится к единой системе обработки и хранения. Поскольку пока еще рано предметно говорить о функциональности MS SQL Server "Yukon", все это, естественно, мои личные фантазии. Делать будущее гораздо интереснее, чем о нем гадать. Давайте вернемся к тому, что мы имеем в плане поддержки XML на сегодня.
Второй вопрос: о чем конкретно будет говориться в этом материале. Как SQL Server, так и XML - вещи, достаточно необъятные. Я поставил себя на место прикладного разработчика, который, наслушавшись про XML, решил, наконец, попробовать его в своем клиент-серверном приложении. В принципе, вся необходимая информация для этого есть, но она разбросана: Books-On-Line к SQL Server, хелпы к SQLXML 3.0, безбрежная MSDN Library, наша и зарубежная программерская периодика, дискуссии на разработческих сайтах и т.д. Я просто попытался превратить разрозненные куски в более или менее целостное повествование, выкидывая то, что мне никогда не пригождалось на практике, и останавливаясь на тех вещах, которые на самом деле нужны, но в документации описаны недостаточно подробно. В процессе копания случалось наступать на грабли, которые самому обойти не получалось. Тогда я обращался за помощью к интернациональному сообществу разработчиков Microsoft и сейчас хотел бы сказать спасибо товарищам Ramakrishna Pamulapati, Matt Neerincx, Kyoko Shikamata и Dion Houston, чьи советы мне здорово помогли. Таким образом, несмотря на то, что добавленная мной стоимость ограничивается компиляцией различных источников, сведением мыслей воедино и расстановкой акцентов, я считаю написание данного материала оправданным. Надеюсь, что он будет способствовать увеличению читательских знаний и умений, а не только энтропии Вселенной.
И еще пара соглашений. Я не собираюсь публиковать этот текст в бумажных средствах массовой информации, поэтому не будем заранее планировать объем и пытаться выполнить пятилетку за два дня и угадать мелодию с одной ноты. Считаем, что объем ограничен моим и вашим временем и терпением. По этой же причине я сначала буду следить за техническим содержанием, а уже потом за чистотой языка и стиля. При самом уважительном отношении к русскому языку, я не считаю криминалом использовать сленг и жаргонизмы в профессиональной беседе, каковой является наше с вами общение. Они позволяют более точно и быстро донести смысл.

2 Сохранить как XML

Впервые возможность сохранять (есть еще замечательное слово "персистить") результаты запроса в виде XML появилась в ADO 2.1 (1999 г.) До этого в ADO 2.0 объект Recordset сохранялся только в частном бинарном формате ADTG (Advanced Data TableGram), который использовался для передачи recordset'a при удаленном доступе с помощью RDS.
В Скрипте 1 мы "отперсистили" в XML результат выполнения запроса (ADOшный Recordset).

static void Save_ADODBRecordset_XMLFile()
{
   ADODB.ConnectionClass cnn = new ADODB.ConnectionClass();
   cnn.Open("Provider=SQLOLEDB;Data Source=(local);Initial Catalog=Northwind;Integrated Security=SSPI", "", "", (int)ADODB.ConnectOptionEnum.adConnectUnspecified);

   ADODB.Recordset rst; Object RecsAffected = new Object();
   rst = cnn.Execute("SELECT c.ContactName, c.ContactTitle, o.OrderDate " +
      "FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID",
      out RecsAffected, (int)ADODB.ExecuteOptionEnum.adOptionUnspecified);

   FileInfo f = new FileInfo("..\\Results\\ADODBRecordset_XMLFile.xml");
   if (f.Exists) f.Delete();
   rst.Save(f.FullName, ADODB.PersistFormatEnum.adPersistXML);
   cnn.Close();

   Process.Start("iexplore.exe", f.FullName);

   XmlDocument xmlDoc = new XmlDocument();
   xmlDoc.Load(f.FullName);
   Console.WriteLine("Кол-во заказов, сделанных данным клиентом = {0}",
   xmlDoc.SelectNodes(".//*[@ContactName='Maria Larsson']").Count);
}

Скрипт 1

Получается действительно нормальный XML, как видно на Рис.1.

- <xml xmlns:s=" uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882 " xmlns:rs=" urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
+ <s:Schema id ="RowsetSchema">
-
<rs:data >
  <z:row ContactName=" Paul Henriot" ContactTitle=" Accounting Manager" OrderDate ="1996-07-04T00:00:00"/>
  
<z:row ContactName=" Karin Josephs" ContactTitle=" Marketing Manager" OrderDate ="1996-07-05T00:00:00"/>
  
<z:row ContactName=" Mario Pontes" ContactTitle=" Accounting Manager" OrderDate font color=blue>="1996-07-08T00:00:00"/>
...
 </rs:data>
</xml>

Рис.1

Его можно открыть при помощи объектной модели DOM, выполнить XPath-запрос, возвращающий узлы заказов, сделанные клиентом по имени Maria Larsson, и другие подобающие XML действия. Что-то можно заложить в первоначальный SQL-запрос:

SELECT count(1) FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE c. ContactName = 'Maria Larsson'

но согласитесь, чтобы показать работу с сохраненным recordset'ом как с XML, часть работы для приличия надо проделать средствами XPath, а не SQL. По соображениям экономии места данный пример, как и все последующие, написаны на C#. Обратите внимание, что в нем используется не ADO.Net, а классическая объектная модель ADO (2.7). Для нее не требуется делать tlbimp из библиотек классов в ... \Program Files\Common Files\System\ado, потому что соответствующая обертка существует изначально как Primary Interop Assembly.

3 Получить как поток XML

В рассмотренном примере XML передавался от ADO к DOM через внешний файл. В ADO 2.5 появилась возможность сохранения recordset'a в виде XML не только в файл, но и в любой объект, поддерживающий интерфейс IStream. В Скрипте 2

static void Save_ADODBRecordset_XMLStream()
{
...
   MSXML2.DOMDocument40Class xmlDoc = new MSXML2.DOMDocument40Class();
   rst.Save(xmlDoc, ADODB.PersistFormatEnum.adPersistXML);
   cnn.Close();
   MSXML2.DOMDocument40Class xslDoc = new MSXML2.DOMDocument40Class();
   xslDoc.loadXML("<?xml version='1.0' ?>" +
      "<doc xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xsl:version='1.0'>" +
      "<xsl:copy-of select='//*[@ContactName=\"Maria Larsson\"]'/></doc>");
   FileInfo f = new FileInfo("..\\Results\\ADODBRecordset_XMLStream.xml");
   StreamWriter sw = f.CreateText();
   sw.Write(xmlDoc.transformNode(xslDoc));
   sw.Close();
...
}

Скрипт 2

recordset записывается в поток объекта DOMDocument из СОМовской библиотеки MSXML4 (WINNT\System32\msxml4.dll). Полученный из recordset'a xmlDoc затем подвергается XSLT-преобразованию, заданному в xslDoc, которое выбирает все элементы, относящиеся к заказчику Maria Larsson. Полученная в результате преобразования строка записывается в файл, который отображается в браузере.
Возможна и обратная ситуация - чтение XML из потока и его запись в recordset. Это было достигнуто за счет введения нового сервисного провайдера Microsoft OLE DB Persistence Provider ("Provider=MSPersist"). Простота сохранения recordset'a в XML накладывает в то же время ограничения: ADO жестко задает схему генерируемого документа (см. еще раз рис.1). Это плоский XML. Несмотря на отношения между таблицами, названия элементов не несут никакой информации о самих таблицах, ограничиваясь безликим , и т.д., так что для внятного отображения, скорее всего, требуется XSL-преобразование.

4 XML на стороне сервера. FOR XML

Следующая ступень эволюции - ADO 2.6 и SQL Server 2000. В SQL Server 2000 в синтаксис Т-SQL был добавлен предикат FOR XML для оператора SELECT, что позволило получать XML-текст как результаты запроса на стороне сервера. Рассмотрим запрос SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID FOR XML AUTO. Вначале SQL Server традиционным способом выполняет ту часть запроса, которая находится до FOR XML. Затем к полученному множеству записей сервер применяет преобразование в XML. Если выполнить этот запрос из Query Analyzer, то видно, что содержимое XML-документа разбито по записям длиной 2033 символа Unicode одноколоночного recordset'а. Вообще говоря, это не есть ни recordset, ни XML. Его нельзя использовать как результат подзапросов, хранимых функций и всего остального, что предполагает дальнейшую обработку на SQL Server. Это нечто предназначено только для передачи клиенту, где из него уже происходит сборка полноценного документа. Таким образом, несмотря на то, что в отличие от Скриптов 1 и 2, в Скрипте 3 XML фактически получается на сервере, все XPath-, updategrams- и прочие запросы выполняются на клиенте, поскольку встроенный тип XML в настоящее время в SQL Server отсутствует.

static void Execute_FORXMLQuery_ADODB()
{
   ...
      ADODB.StreamClass str = new ADODB.StreamClass();
      str.Open(System.Type.Missing, ADODB.ConnectModeEnum.adModeUnknown, ADODB.StreamOpenOptionsEnum.adOpenStreamUnspecified, "", "");

      ADODB.CommandClass cmd = new ADODB.CommandClass();
      cmd.ActiveConnection = cnn;
      cmd.CommandText = "SELECT '<Root>' " +
            "SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c " +
            "INNER JOIN Orders o ON c.CustomerID = o.CustomerID " +
            "WHERE c.ContactName = ? AND year(o.OrderDate) = ? FOR XML AUTO " +
            "SELECT '</Root>'";       cmd.Parameters.Append(cmd.CreateParameter("@Name", ADODB.DataTypeEnum.adVarWChar, ADODB.ParameterDirectionEnum.adParamInput, 30, null));
      cmd.Parameters.Append(cmd.CreateParameter("@Year", ADODB.DataTypeEnum.adInteger, ADODB.ParameterDirectionEnum.adParamInput, 4, null));
      cmd.Dialect = "{C8B522D7-5CF3-11CE-ADE5-00AA0044773D}";
      cmd.Properties["Output Stream"].Value = str;
      cmd.Properties["Output Encoding"].Value = "UTF-8";

      object RecsAffected = null, Params = new object[] {"Maria Larsson", 1997};
      cmd.Execute(out RecsAffected, ref Params, (int)ADODB.ExecuteOptionEnum.adExecuteStream);

      MSXML2.DOMDocument40Class xmlDoc = new MSXML2.DOMDocument40Class();
      xmlDoc.load(str);
}

Скрипт 3

Скрипт 3 выполняет тот же запрос, что и в предыдущих примерах, за исключением того, что я его слегка разнообразил передачей параметров: он показывает список всех заказов, сделанных определенным клиентом за определенный год. Результат заключается в скобки <Root> … </Root> для получения well-formed документа. Вместо этого можно использовать cmd.Properties["xml root"].Value = "Root". XML возвращается на клиента в объекте ADODB.Stream. Его можно сохранить сразу в файл - str.SaveToFile(f.FullName, ADODB.SaveOptionsEnum.adSaveCreateOverWrite); , а можно передать как поток в документ типа MSXML2.DOMDocument40Class. В любом случае понятно, что над результатами запроса можно вести дальнейшую работу средствами DOM. Аналогично Скрипту 2 для работы с XML-документами здесь используется библиотека СОМ, а не .NET, поскольку ADODB.Stream нельзя преобразовать к System.IO.Stream, чтобы загрузить в System.Xml.XmlDocument. Передавать же, сохраняя в промежуточный файл, как делалось в Скрипте 1, можно, но неизящно. Свойство Dialect класса ADODB.Command говорит провайдеру, какой тип команды используется. Возможные значения приведены в Табл.1

Тип команды Значение в ADO Константа в OLE DB
Запрос Transact-SQL {C8B522D7-5CF3-11CE-ADE5-00AA0044773D} DBGUID_SQL
Запрос XPath {EC2A4293-E898-11D2-B1B7-00C04F680C56} DBGUID_XPATH
Запрос в XML-шаблоне {5D531CB2-E6Ed-11D2-B252-00C04F681B71} DBGUID_MSSQLXML
Поведение провайдера по умолчанию {C8B521FB-5CF3-11CE-ADE5-00AA0044773D} DBGUID_DEFAULT

Табл.1

Объекты типа Stream появились в ADO 2.5 для поддержки некоторых типов нереляционной информации. Вспомним, что первоначальная спецификация OLE DB в 1996 г. описывала универсальный доступ в рамках прямоугольных recordset'ов для обеспечения совместимости с ранними API, имевшими дело с реляционными данными (ODBC, DB-Library, DAO). Однако несмотря на развитость аппарата реляционной алгебры далеко не все источники удавалось свести к этому классу. Да и потом, иерархические базы данных все-таки предшествовали реляционным, поскольку эта модель, видимо, более естественно отвечает образу мышления. Время шло, диссонанс между объектно-ориентированной средой разработки и реляционной архитектурой хранения (плоские таблицы, связанные отношениями) проявлялся все более отчетливо. В ADO 1.5 была сделана попытка сгладить эту проблему с помощью провайдера MSDataShape. Спецификация OLE DB 1.5 предусматривала новый тип полей - Chapter column. Предполагалось, что индивидуальные уровни иерархии можно представить в виде отдельных rowset'ов, и chapter указывает для родительской записи множество ее детей в дочернем rowset'e. Однако все это было хорошо в однородных иерархиях, когда дочерняя запись имеет тот же набор полей, что и родительская. Например, очевидный негомогенный источник - файловая система - сюда уже не вписывается. Чтобы уложить древовидную структуру в прямоугольную с минимальными переделками и потерями производительности (скажем, не заводя для каждой записи число полей, соответствующее полному набору всех возможных атрибутов узла в дереве, большая часть из которых будет, очевидно, пустовать) в ADO 2.5 были введены классы Record и Stream. Набор полей (атрибутов) записи (файла) может разниться не только с родительской записью (папкой), но и меняться от записи к записи (например, в зависимости от типа файла). Наименьший общий знаменатель полей, присущих всем записям, формировал колонки привычного Recordset. Класс Stream соответствовал содержанию файла. Таким образом, появилась возможность получения результата запроса в виде потoка, чем мы и воспользовались в Скрипте 3. XML естественным образом решает проблему представления древовидных иерархий. Как и RDS, MSDataShape в настоящее время поддерживается по соображениям совместимости, но развиваться в дальнейшем не будет.
Для предиката FOR XML существует три возможных опции: AUTO, RAW и EXPLICIT. Действие первой мы уже видели. Она дает SQL Server указание сформировать простое вложенное дерево, где иерархия (вложенность) определяется порядком связывания таблиц в запросе. Каждой таблице из FROM, хотя бы одно поле которой попадает в вывод, ставится в соответствие элемент, имя которого равно имени / псевдониму таблицы. Поля таблиц отвечают атрибутам элементов. Отсюда следует, что все поля в запросе должны быть поименованы. Недопустимо, например, SELECT 1 FOR XML… Опция RAW формирует плоский неиерархический документ независимо от отношений между таблицами в запросе. Он подобен тому, что мы видели на рис.1. Каждая запись результата соответствует элементу с именем row. Опция EXPLICIT наиболее гибкая из трех и позволяет получить XML произвольной структуры, однако recordset, по которому он строится, должен следовать определенным правилам расположения записей и именования полей, чтобы однозначно задать желаемую структуру дерева. Я не буду сейчас подробно расписывать эти правила, т.к. несмотря на гибкость, способ этот достаточно громоздок и на практике применяется редко. В основном он используется самим SQL Server'ом для преобразования реляционной структуры к аннотированной схеме (см.п.9). Подробно узнать про опцию EXPLICIT можно в документации на SQL Server (см. XML and Internet Support -> Using EXPLICIT Mode).
Поддерживаются параметризованные запросы с FOR XML и процедуры, возвращающие SELECT … FOR XML. Передача параметров осуществляется стандартно при помощи коллекции Parameters объекта ADODB.Command. Поля типа text / ntext возвращаются в виде текста, поля типа image - в виде их XPath-пути. FOR XML …, Binary Base64 возвращает их в кодировке Base64 и для опции RAW это единственный возможный способ вывести значения BLOB-типов. FOR XML AUTO, ELEMENTS отображает поля не на атрибуты, а на подэлементы. SELECT TOP 0 ... FOR XML AUTO, XMLData дает схему XML-результата в формате XDR. Начиная с SQLXML 2.0 включена поддержка XSD-схем и утилита для конвертации XDR в XSD (cvtschema.ехе).

5 SQLXML-классы в .NET

После выхода SQL Server 2000 в августе 2000 г. дополнительная функциональность, в плане развития поддержки XML, выпускалась в виде веб-релизов под названием SQLXML. SQLXML 1.0 содержал Updategrams, XML Bulkload и усовершенствования в части производительности. В версии 2.0 ввели аннотированные XSD-схемы, FOR XML на стороне клиента, SQLXMLOLEDB-провайдер и SQLXML Managed Classes (в плане интеграции с ADO.Net). Наконец, веб-релиз версии 3.0, вышедший в марте 2002 г. и последний на момент подготовки доклада, добавляет к этому поддержку SOAP, превращая SQL Server в Web-сервис. Некоторые из перечисленных возможностей будут здесь разобраны.
Начнем с того, что в дальнейшем в примерах мы будем использовать управляемые (managed) библиотеки System. Data (ADO .Net) и Microsoft.Data.SqlXml. Первая является штатной функциональностью Visual Studio .Net, а вторая устанавливается в составе SQLXML Web Release 2.0 и выше. Если Вы экспериментируете с приводимыми здесь примерами, пожалуйста, отметьте эти библиотеки в References для своего проекта. Это вполне естественный выбор при использовании .Net-средства разработки, который избавляет, например, от возни с СОМ-маршалингом, при передаче массива интерфейсов IDispatch (Variant) в параметры ADODB.Command.. Не следует, впрочем, полагать, что с этого момента все рассматриваемые задачи решаются только с помощью SQLXML Managed Classes. Большинство разбираемых примеров вполне реализуются средствами классического СОМовского ADO подобно тому, как показывалось в п.п. 2 - 4. Однако с помощью новых технологий доступа к данным, специально предназначенных для работы в .NET Framework, этого можно достичь проще и быстрее. Скрипт 4 демонстрирует предыдущий пример (FOR XML-запрос на стороне сервера), переписанный с ADODB на SqlXml.

using System.Xml;
using Microsoft.Data.SqlXml;
...
static void Execute_FORXMLQuery_SQLXML()
{
      SqlXmlCommand cmd = new SqlXmlCommand("Provider=SQLOLEDB;...");
      cmd.CommandText = "SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE c.ContactName = ? and year(o.OrderDate) = ? FOR XML AUTO";
      cmd.RootTag = "Root";
      cmd.CommandType = SqlXmlCommandType.Sql;
      cmd.OutputEncoding = "UTF-8";
      cmd.CreateParameter().Value = "Maria Larsson";
      cmd.CreateParameter().Value = 1997;
      Stream str = cmd.ExecuteStream();

      XmlDocument xml = new XmlDocument();
      xml.Load(str);
...
}

Скрипт 4

Логика примера не изменилась - выполняется SQL-запрос, результаты получаются в виде XML и передаются для последующей возможной работы в System.Xml.XmlDocument (аналог DOMDocument). Кроме того, для целей демонстрации XML-поток сохраняется в файл, который затем открывается в браузере. Класс SqlXml.Command для получения результатов в виде XML-потока имеет два метода: ExecuteStream() и ExecuteToStream(). Их отличие состоит в том, что первый создает новый Stream, а второй пишет в уже существующий. Кроме того, результаты запроса можно сразу получить как XmlReader (метод ExecuteXmlReader). В отличие от класса XmlDocument (DOM) XmlReader является более легковесным, он не требует загрузки всего документа в память. В терминологии баз данных его ближайшим аналогом будет forward-only read-only курсор. Свойство Dialect класса Command в ADO соответствует свойству CommandType, а Output Encoding и XML Root перешли из расширенных свойств в стандартные (OutputEncoding и RootTag).

6 FOR XML на стороне клиента

Провайдер SQLXMLOLEDB является типичным сервисным провайдером, как уже упоминавшиеся ранее MSDataShape и MSPersist, в том плане, что он получает данные от другого провайдера, а не непосредственно из источника. SQLXMLOLEDB позволяет преобразовывать recordset в XML непосредственно на клиенте. Таким образом, провайдер SQLOLEDB, стоящий между SQLXMLOLEDB и SQL Server, получает от сервера не XML-поток, а обычный recordset и передает его SQLXMLOLEDB, который уже занимается превращением реляционной информации в XML. Скрипт 5 демонстрирует FOR XML на стороне клиента.

public static void Execute_FORXMLQuery_OnClient()
{
      ...
      cmd.CommandText = "SELECT c.ContactName, COUNT(o.OrderDate) as 'Кол-во заказов' FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE o.OrderDate between ? and ? GROUP BY c.ContactName FOR XML NESTED";
      cmd.CreateParameter().Value = "19970101";
      cmd.CreateParameter().Value = "19971231 23:59:59";
      cmd.ClientSideXml = true;
      cmd.OutputEncoding = "UTF-8"; cmd.RootTag = "Корень";
      Stream str = cmd.ExecuteStream();
      StreamReader sr = new StreamReader(str);
      FileInfo f = new FileInfo("..\\Results\\FORXMLQueryResults.xml");
      StreamWriter sw = new StreamWriter(f.FullName, false, System.Text.Encoding.UTF8);
      sw.WriteLine("<!--Этот XML сделан на стороне клиента-->");
      str.Position = 0; sw.Write(sr.ReadToEnd());
      sr.Close(); sw.Close();
      ...
}

Скрипт 5

При помощи SQL Profiler можно оттрассировать и сравнить запросы, которые в действительности обрабатываются сервером при выполнении Скриптов 4 и 5.

Скрипт 4:

exec sp_executesql N'SELECT '''' SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE c.ContactName = @P1 AND year(o.OrderDate) = @P2 FOR XML AUTO SELECT ''''', N'@P1 nvarchar(30),@P2 int', N'Maria Larsson', 1997

Скрипт 5:

exec sp_executesql N'SELECT c.ContactName, COUNT(o.OrderDate) as ''Кол-во заказов'' FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE o.OrderDate between @P1 and @P2 GROUP BY c.ContactName ', N'@P1 nvarchar(8),@P2 nvarchar(17)', N'19970101', N'19971231 23:59:59'

В Скрипте 5 я слегка изменил текст запроса, чтобы показать использование GROUP BY (этот предикат не разрешается в серверных FOR XML-запросах). Запрос считает количество заказов, сделанных каждым клиентом за определенный период времени. Обратите внимание на отсутствие предиката FOR XML во втором случае. Это значит, что преобразование recordset'a в XML действительно происходит в данном случае на клиенте.
Несмотря на бОльшие издержки на стороне клиента такая схема работы является более универсальной и позволяет получать клиенту XML от тех источников данных, которые его не поддерживают и не будут. Для этого будет достаточно в строке соединения:

"Provider= SQLXMLOLEDB;DataProvider=SQLOLEDB, …"

поставить вместо SQL Server соответствующего OLE DB-провайдера. К сожалению, для этого придется подождать следующего, 4-го, веб-релиза. Пока SQLXMLOLEDB умеет работать только с SQL Server 2000.
При клиентском XML-форматировании не поддерживается возврат в качестве результата одной команды нескольких recordset'ов, зато допускается предикат GROUP BY с агрегатными функциями. Режим FOR XML NESTED клиентского форматирования примерно соответствует FOR XML AUTO серверного, за исключением того, что вместо псевдонимов в имена элементов ставятся настоящие названия таблиц. О других различиях можно прочитать в документации на SQLXML 3.0.

7 XML-представление наборов данных в ADO .NET

На самом деле даже без провайдера SQLXMLOLEDB и SQLXML веб-релизов в Visual Studio .Net (точнее, в ADO.Net) имеются достаточно мощные средства для представления реляционных наборов данных в виде XML, и наоборот, XML в реляционном виде. Типовой сценарий работы выглядит следующим образом: получить внутри объекта DataSet таблицы как результаты запросов к источнику данных (возможно, к разным), связать их между собой на основе объектов DataRelation и создать XML-представление DataSet'a при помощи XmlDataDocument, как показано в Скрипте 6.

using System.Data;
using System.Data.OleDb;
using System.Xml;
...
static void Transform_ADONetDataSet_Xml()
{
   DataSet ds = new DataSet("Новый набор данных на клиенте");
   (new OleDbDataAdapter("SELECT CustomerID, ContactName, ContactTitle FROM Customers", ConstDeclarations.ConnectionString)).Fill(ds, "Клиентская копия табл.клиентов");
   (new OleDbDataAdapter("SELECT OrderID, CustomerID, OrderDate FROM Orders", ConstDeclarations.ConnectionString)).Fill(ds, "Клиентская копия табл.заказов");
   ds.Relations.Add("Джойн двух копий на клиенте",
   ds.Tables["Клиентская копия табл.клиентов"].Columns["CustomerID"],
   ds.Tables["Клиентская копия табл.заказов"].Columns["CustomerID"]).Nested = true;
   XmlDataDocument xml = new XmlDataDocument(ds);
   FileInfo f = new FileInfo("..\\Results\\ADONetDataSet.xml");
   xml.Save(f.FullName);
...
}

Скрипт 6

Результирующий XML можно видеть на рис.2.

<Новый_x0020_набор_x0020_данных_x0020_на_x0020_клиенте>
   <Клиентская_x0020_копия_x0020_табл.клиентов>
      <CustomerID>
ALFKI</CustomerID>
      <ContactName>
Maria Anders</ContactName>
      <ContactTitle>
Sales Representative</ContactTitle>
      <Клиентская_x0020_копия_x0020_табл.заказов>
         <OrderID>
10643</OrderID>
         <CustomerID>
ALFKI</CustomerID>
         <OrderDate>
1997-08-25T00:00:00.0000000+04:00</OrderDate>
      </Клиентская_x0020_копия_x0020_табл.заказов>
      <Клиентская_x0020_копия_x0020_табл.заказов>
         <OrderID>
10692</OrderID>
...

Рис. 2

По умолчанию из DataSet будет сгенерирован документ, в котором каждой записи DataRow соответствует элемент с именем DataTable. Значения полей присутствуют в виде подэлементов DataRow с названиями соответствующих полей DataColumns. Поскольку DataSet предполагает отсоединенный режим работы, отношения между таблицами в источнике (в БД на SQL Server) не принимаются во внимание. Так, несмотря на связывание таблиц в запросе:

(new OleDbDataAdapter("SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID", Connection)).Fill(ds);

с точки зрения DataSet это плоское множество записей, потому что связи отработал сервер и прислал в DataSet готовый табличный результат. Для образования иерархического XML-документа, где записи дочерней таблицы являются вложенными элементами родительской, отношение между таблицами нужно указывать явно в DataSet.Relations, при этом свойство .Nested объекта DataRelation должно быть выставлено в true. (Иначе записи из родительской и дочерней таблиц будут перечислены друг за другом на одном и том же уровне иерархии). Класс XmlDataDocument является производным от DOMовского XmlDocument, поэтому с его помощью над DataSet'ом можно выполнять все стандартные XML-операции: XPath-запросы, XSL-преобразования и т.д.

static void Update_ADONetDataSet_Xml()
{
   OleDbConnection cn = new OleDbConnection("Provider=SQLOLEDB;...");
   DataSet ds = new DataSet();

   OleDbDataAdapter daCust = new OleDbDataAdapter("SELECT CustomerID, ContactName, ContactTitle FROM Customers", cn);
   //Создаем UpdateCommand вручную
   daCust.UpdateCommand = new OleDbCommand("UPDATE Customers SET ContactName = ?, ContactTitle = ? WHERE CustomerID = ?", cn);
   daCust.UpdateCommand.Parameters.Add("@ContactName", OleDbType.VarChar, 40, "ContactName");
   daCust.UpdateCommand.Parameters.Add("@ContactTitle", OleDbType.VarChar, 40, "ContactTitle");
   daCust.UpdateCommand.Parameters.Add("@CustomerID", OleDbType.Char, 5, "CustomerID");
   daCust.Fill(ds, "Cust");

   OleDbDataAdapter daOrds = new OleDbDataAdapter("SELECT OrderID, CustomerID, OrderDate FROM Orders", cn);
   //Создаем UpdateCommand автоматически
   OleDbCommandBuilder cbOrds = new OleDbCommandBuilder(daOrds);
   daOrds.Fill(ds, "Ords");

   ds.Relations.Add("Джойн двух копий на клиенте",
   ds.Tables["Cust"].Columns["CustomerID"],
   ds.Tables["Ords"].Columns["CustomerID"]).Nested = true;
   ds.EnforceConstraints = false;

   XmlDataDocument xml = new XmlDataDocument(ds);
   //Эквивалентно ds.Tables["Cust"].Select("CustomerID = 'ALFKI'")[0]["ContactName"] = "Maria Anders";
   xml.SelectSingleNode("//Cust[CustomerID='ALFKI']/ContactName").InnerText = "Maria Anders";
   xml.SelectSingleNode("//Cust[CustomerID='ALFKI']/Ords[OrderID=10643]/OrderDate").InnerText = "1997-08-25T00:00:00.0000000+04:00";
   daCust.Update(ds.Tables[1]); daCust.Update(ds.Tables[0]);
   ...
}

Скрипт 7

Скрипт 7 демонстрирует, что данные источника можно модифицировать не только напрямую через DataSet (ds.Tables[<Имя или номер таблицы в коллекции>].Rows[<Номер записи в коллекции>][<Имя или номер поля в коллекции>] = …), но и через его XML-представление. В примере изменяются значений некоторых XPath-узлов объекта XmlDataDocument. Эти изменения отражаются в DataSet'е, над которым построен данный XmlDataDocument, а поскольку у соответствующих DataAdapter'ов таблиц определены UpdateCommand'ы, то они будут транслированы далее в источник данных (SQL Server). Обратное тоже верно. Т.е. в DataSet можно подгрузить XML-документ, который затем читать и модифицировать реляционными операциями. В Скрипте 8 мы получаем схему построенного в предыдущем примере XML-файла при помощи утилиты xsd.exe, входящей в состав .NET Framework, читаем ее в XmlDataDocument и загружаем туда данные из этого документа. На основе XSD-схемы ADO.Net создает DataSet эквивалентной реляционной структуры, так что становится возможным обращаться и модифицировать XML-документ, как если бы он был совокупностью связанных таблиц.

static void Update_XML_ADONetDataset()
{
   FileInfo f = new FileInfo("..\\Results\\ADONetDataSet.xml");
   Process.Start("xsd.exe", f.FullName + " /o:..\\Results");
   XmlDataDocument xml = new XmlDataDocument();
   xml.DataSet.ReadXmlSchema(Path.ChangeExtension(f.FullName, ".xsd"));
   xml.Load(f.FullName);
   xml.DataSet.Tables["Cust"].Select("CustomerID='ALFKI'")[0]["ContactName"] = "Абра Кадабра";
   xml.DataSet.Tables["Ords"].Select("OrderID=10643")[0]["OrderDate"] = DateTime.Now;

...
}

Скрипт 8

Неплохим иллюстративным примером было бы приложение, которое документирует пользовательские библиотеки классов .Net в базе данных. Определения классов и объекты сохраняются в виде XSD-схем и XML-документов (см. System.Xml.Serialization), а на их основе, в свою очередь, при помощи рассмотренного соответствия реляционного и XML-представлений, которое обеспечивает ADO.Net, создается и наполняется БД. В качестве самостоятельного упражнения вы можете попробовать сами написать такое приложение и назвать его, скажем, Cheops.
Впрочем, я отвлекся. Чрезвычайно мощная и развитая функциональность ADO.Net по своей сути представляет собой результат эволюции простой возможности сохранения ADODB.Recordset в формате XML на стороне клиента, с которой начинался наш разговор (см. п.2). Вернемся, тем не менее, к основной теме - поддержке XML в SQL Server.

8 Прямые XPath-запросы к объектам SQL Server

В Скрипте 7 было показано, как осуществлять XPath-навигацию по связанным таблицам в ADO.Net Dataset. Подобным же образом XPath-запросы можно адресовать к SQL Server 2000, как если бы это был XML-ресурс, а не сервер реляционных баз данных. Под словом "прямые" подразумевается, что эти запросы обращаются к объектам базы данных напрямую, а не через аннотированные схемы, о которых речь пойдет в следующем параграфе. В Скрипте 9 приведен запрос, выводящий всех клиентов с именами, начинающимися с букв X, Y, Z.

static void Direct_XPathQuery_SQLXML()
{
   SqlXmlCommand cmd = new SqlXmlCommand("Provider=SQLOLEDB;...");
   cmd.CommandText = "Customers[@ContactName>=$НачБуква]/@ContactName";
   cmd.CommandType = SqlXmlCommandType.XPath;
   SqlXmlParameter prm = cmd.CreateParameter();
   prm.Name = "@НачБуква"; prm.Value = "X";
   cmd.RootTag = "Root";
   XmlDocument xml = new XmlDocument();
   xml.Load(cmd.ExecuteStream());
...
}

Скрипт 9

Обратите внимание на разное именование параметра в XPath-запросе и в параметрах объекта команды. Если посмотреть, во что XPath превращается на сервере:

exec sp_executesql N' SELECT ContactName FROM Customers WHERE ContactName>=@НачБуква ', N'@НачБуква nvarchar(1)', N'X',

то видно, что первая @ автоматически получается из $ при переводе XPath-запроса в SQL, а о второй нужно позаботиться самим в приложении (SqlXmlParameter.Name), иначе sp_executesql его попросту не поймет.
Кроме того, из результатов видно, что возвращать нормальный XML в ответ на прямой XPath-запрос SQL Server не умеет - результатом может быть только скалярная строка - например, одно поле единственной записи recordset'a. Если записей, паче чаяния, оказывается несколько, строки конкатенируются в одну. Прямые запросы исповедуют стандартную схему: таблицы воспринимаются как элементы, а поля - как атрибуты. Эти и другие серьезные ограничения наводят на мысль о том, что нужен специальный механизм, позволяющий задавать XML-представление над объектами базы данных, подобно тому, что мы наблюдали в случае ADO.Net DataSet (см. пред. параграф). В SQL Server такая возможность тоже существует и называется "аннотированные схемы".

9 Аннотированные схемы

По своей структуре это обычные XSD-схемы, в которых допускаются специальные аннотации, задающие их привязку к сущностям реляционной структуры: таблицам, полям, первичным и внешним ключам, отношениям между таблицами и т.д., благодаря чему данные, хранящиеся на SQL Server, можно привести к желаемой XSD- структуре и запрашивать затем как XML-документ (XPath, XQuery). В VS .Net входит удобный редактор XSD-схем, позволяющий собирать их, натаскивая drag-n-drop'ом элементы, атрибуты, типы и т.д. из панели инструментов. Редактор имеет две панели: одна показывает традиционный XML-код схемы, а другая - ее реляционный эквивалент в виде таблиц и связей между ними. При переключении происходит автоматическая валидация схемы, доступная также из меню (Schema -> Validate). В нем есть цветовое выделение синтаксических конструкций, intelisense-подсказки и многие другие приятные вещи. Итак, с помощью этого замечательного редактора я создаю вид моего XML-документа, который будет содержать, допустим, информацию по клиентам и сделанных ими заказах.


<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="XMLSchema1" xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<xs:complexType name="Клиент">
		<xs:sequence>
			<xs:element name="Адрес" type="Адрес" />
			<xs:element name="Заказы" type="Заказы" />
		</xs:sequence>
		<xs:attribute name="Имя" type="xs:string" />
		<xs:attribute name="Должность" type="xs:string" />
		<xs:attribute name="Фирма" type="xs:string" />
	</xs:complexType>
	<xs:complexType name="Адрес">
		<xs:sequence>
			<xs:element name="Страна" type="xs:string" />
			<xs:element name="Город" type="xs:string" />
			<xs:element name="Улица_дом" type="xs:string" />
			<xs:element name="Индекс" type="xs:string" />
		</xs:sequence>
	</xs:complexType>
	<xs:complexType name="Заказы">
		<xs:sequence>
			<xs:element name="Заказ" type="Заказ" minOccurs="0"
			maxOccurs="unbounded" />
		</xs:sequence>
	</xs:complexType>
	<xs:complexType name="Заказ">
		<xs:sequence>
			<xs:element name="Дата" type="xs:date" />
			<xs:element name="Стоимость" type="xs:float" />
		</xs:sequence>
	</xs:complexType>
</xs:schema>

Рис.3

Теперь, чтобы по этой схеме представить данные из SQL Server, сопоставим их элементам и атрибутам при помощи аннотаций - см. рис.4 - для поддержки которых в схеме делается ссылка на соответствующее пространство имен (xmlns:ms="urn:schemas-microsoft-com:mapping-schema").


<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="SQLSchema1" xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:ms="urn:schemas-microsoft-com:mapping-schema">
	<xs:annotation>
		<xs:appinfo>
			<ms:relationship name="CustOrds" parent="Customers" parent-key="CustomerID" 
			child="Orders" child-key="CustomerID" />
		</xs:appinfo>
	</xs:annotation>
	<xs:complexType name="Клиент">
		<xs:sequence>
			<xs:element name="Адрес" type="Адрес" ms:is-constant="1" />
			<xs:element name="Заказы" type="Заказы" ms:is-constant="1" />
		</xs:sequence>
		<xs:attribute name="Имя" type="xs:string" ms:field="ContactName" />
		<xs:attribute name="Должность" type="xs:string" ms:field="ContactTitle" />
		<xs:attribute name="Фирма" type="xs:string" ms:field="CompanyName" />
	</xs:complexType>
	<xs:complexType name="Адрес">
		<xs:sequence>
			<xs:element name="Страна" type="xs:string" ms:field="Country" />
			<xs:element name="Город" type="xs:string" ms:field="City" />
			<xs:element name="Улица_дом" type="xs:string" ms:field="Address" />
			<xs:element name="Индекс" type="xs:string" ms:mapped="false" />
		</xs:sequence>
	</xs:complexType>
	<xs:complexType name="Заказы">
		<xs:sequence>
			<xs:element name="Заказ" type="Заказ" minOccurs="0" 
			maxOccurs="unbounded" 
			ms:relation="Orders" ms:relationship="CustOrds" />
		</xs:sequence>
	</xs:complexType>
	<xs:complexType name="Заказ">
		<xs:sequence>
			<xs:element name="Дата" type="xs:date" ms:field="OrderDate" />
			<xs:element name="Стоимость" type="xs:float" ms:field="Freight" />
		</xs:sequence>
		<xs:attribute name="Номер" type="xs:string" ms:field="OrderID" />
	</xs:complexType>
	<xs:element name="Клиент" type="Клиент" ms:relation="Customers" />
</xs:schema>

Рис.4

Аннотация sql:relation используется для отображения узла на таблицу. Она не поддерживается в тэгах определения типа, т.е. только в xs:element и xs:attribute, поэтому нам пришлось ввести в схему элемент составного типа "Клиент": <xs:element name="Клиент" type="Клиент" ms:relation="Customers" />. Вложенные элементы соответствуют записям дочерней таблицы, поэтому для них требуется еще задать ms:relationship. Отношения между таблицами в терминах родительской и дочерней таблиц (parent / child) и полей, по которым устанавливается связь (parent-key / child-key), определяются как атрибуты элемента <ms:relationship> в разделе определения аннотаций <xs:annotation>, <xs:appinfo>. Затем это отношение можно использовать, чтобы вложить дочерние записи внутрь родительского элемента <xs:element name="Заказ" type="Заказ"... ms:relation="Orders" ms:relationship="CustOrds"/>. Если вложенный элемент не соответствует никакой дочерней таблице, а несет чисто контейнерную функцию (как, например, Адрес), то он помечается атрибутом ms:is-constant="1": <xs:element name="Адрес" type="Адрес" ms:is-constant="1"/>. Аннотация ms:field привязывает XML-узел к полю таблицы. Она не требуется, когда название атрибута совпадает с названием поля. Непривязанные атрибуты также не допускаются. Если мы не планируем брать значение узла из БД, но в силу каких-либо причин не можем исключить его из схемы, его нужно пометить аннотацией ms:mapped="false": <xs:element name="Индекс" type="xs:string" ms:mapped="false" />. От is-constant она отличается тем, что узел вообще исключается из результирующего XML-документа.
Разберем еще несколько аннотаций на примере схемы, которая воссоздает по таблице parent-child дерево иерархии в виде XML:


<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:ms="urn:schemas-microsoft-com:mapping-schema" 
id="SQLSchema2">
	<xs:annotation>
		<xs:appinfo>
			<ms:relationship name="Начальник-Подчиненный" 
			parent="Employees" parent-key="EmployeeID" 
			child="Employees" child-key="ReportsTo" />
		</xs:appinfo>
	</xs:annotation>
	<xs:element name="Сотрудник" type="Тип_Сотрудник" 
	ms:relation="Employees" ms:key-fields="EmployeeID" 
	ms:limit-field="ReportsTo" />
	<xs:complexType name="Тип_Сотрудник">
		<xs:sequence>
			<xs:element name="Сотрудник" type="Тип_Сотрудник" 
			ms:relation="Employees" ms:key-fields="EmployeeID" 
			ms:relationship="Начальник-Подчиненный" ms:max-depth="3" />
		</xs:sequence>
		<xs:attribute name="ID_сотрудника" type="xs:ID" 
		ms:field="EmployeeID" ms:hide="true" />
		<xs:attribute name="Имя" type="xs:string" ms:field="FirstName" />
		<xs:attribute name="Фамилия" type="xs:string" 
		ms:field="LastName" />
	</xs:complexType>
</xs:schema>

Рис.5

Он взят из документации к SQLXML 3.0. Таблица Employees содержит записи по сотрудникам и связана сама с собой, т.е. в поле ReportsTo для каждого сотрудника указывается EmployeeID его начальника.
ms:max-depth задает максимальную глубину вложенности рекурсии при раскрытии отношения "родитель-потомок". В отличие от предыдущей ситуации, где количество уровней в иерархии определялось длиной максимальной ветки связанных таблиц, глубина дерева в случае parent-child таблицы зависит от ветки, по которой мы идем от корня, и априори неочевидна. Не обладая в текущей версии специальным оператором построения иерархии по такому типу связи, SQL Server разрешает ее в последовательность соединенных UNION'ами SELECT'ов, каждый из которых соответствует уровню иерархии. Поэтому их число (ms:max-depth) SQL Server должен знать заранее. Максимальное значение для него волевым образом установлено в 50.
Другая аннотация - ms:limit-field - позволяет провести ограничение (WHERE) по какому-либо полю еще на уровне схемы, т.е. до того, как дело дойдет до XPath. Обычно она употребляется в паре с ms:limit-value, которая задает значение критерия. В данном случае эта аннотация опущена, что означает, что по умолчанию берется значение NULL. Таким образом, верхним уровнем в данной иерархии будут самые-самые начальники (у которых начальников нет: ReportsTo = Null).
Почему атрибут ID_сотрудника аннотирован как ms:hide="true"? Он несет чисто служебную информацию и вряд ли понадобится в XML-результате. Но его не хочется выключать из схемы при помощи ms:mapped="false", потому что он действительно привязан к информации в БД, которая понадобится в дальнейшем. Например, он может фигурировать в критерии XPath-запроса: cmd.CommandText = "Сотрудник[@ID_сотрудника='4']" (Чтобы этот запрос возвратил сотрудника с EmployeeID = 4, нужно убрать фильтрацию в схеме - ms:limit-field). Наконец, еще одна аннотация, которая сейчас необязательна, но встретится нам через параграф - это ms:key-fields. Она задает значения полей, составляющих первичный ключ таблицы.
Полный список аннотаций, естественно, не ограничивается теми, которые встретились в этих двух простых примерах схем. Он достаточно обширен и позволяет строить довольно нетривиальные соответствия между XML-схемой и реляционным содержанием. Его можно найти в документации на SQLXML 3.0.


static void Annotated_XPathQuery_SQLXML()
{
	...
	cmd.CommandText = "Клиент[Адрес/Страна='Spain' or Адрес/Страна='France']";
	cmd.SchemaPath = "..\\Schemas\\SQLSchema1.xsd";
	cmd.CommandType = SqlXmlCommandType.XPath;
	cmd.RootTag = "Клиенты";
	XmlDocument xml = new XmlDocument();
	xml.Load(cmd.ExecuteStream());
	...
}

Скрипт 10

В Скрипте 10, как и в предыдущем примере (Скрипт 9), на SQL Server посылается XPath-запрос, однако теперь данные рассматриваются через призму выбранной аннотированной схемы (указывается в свойстве SqlXmlCommand.SchemaPath) и трактуются в соответствии с задаваемой ею структурой. В данном случае запрос выбирает всех клиентов из Испании и Франции и сделанные ими заказы. Встроенной поддержкой XPath (а также XQuery) SQL Server в настоящее время не располагает, поэтому XPath по дороге превращается в то, что ему более понятно, а именно - в SQL-запрос. Если быть совсем точным, то в запрос типа FOR XML EXPLICIT. Ради любопытства можете открыть Profiler и посмотреть его для Скрипта 10 (здесь я его приводить не буду, потому что он занял бы еще как минимум страницу). Поддерживается подмножество стандартного синтаксиса XPath в части осей, функций и операторов. Отрадно, что каждым SQLXML веб-релизом это подмножество расширяется.

10 XML-шаблоны как разновидность хранимых процедур

Шаблоны (templates) объединяют в себе квантово-волновой (SQL / XML) дуализм SQL Server. Козьма Прутков мог бы смело их уподобить хранимым процедурам, которые допускают как SQL-, так и XPath-запросы. Шаблон - это XML-файл, структура которого показана на рис.6.


<?xml version="1.0" encoding="utf-8" ?> 
<Солянка xmlns:sql="urn:schemas-microsoft-com:xml-sql">
  <sql:header>
     <sql:param name="Колво"></sql:param>
     <sql:param name="Сумма">1000</sql:param>
  </sql:header>
  <sql:query client-side-xml="0">
    SELECT TOP 1 * FROM Employees FOR XML AUTO
  </sql:query>
  <sql:query client-side-xml="1">
    SELECT c.ContactName, COUNT(o.OrderDate) AS 'Кол-во' 
	FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID 
	GROUP BY c.ContactName HAVING COUNT(o.OrderDate) >= @Колво 
	FOR XML NESTED
  </sql:query>
  <sql:xpath-query mapping-schema="..\Schemas\SQLSchema1.xsd">
	Клиент[number(Заказы/Заказ/Стоимость)&gt;$Сумма]
  </sql:xpath-query>
  <sql:xpath-query mapping-schema="..\Schemas\SQLSchema2.xsd">
    Сотрудник
  </sql:xpath-query>
</Солянка>

Рис.6

Он состоит из частей трех основных типов. Каждая часть является опциональной. В <sql:query> перечисляются операторы SQL, в <sql:xpath-query> - запросы XPath, в <sql:header> описываются параметры для запросов (как SQL, так и XPath). Привязка описаний параметров в заголовке к параметрам в запросах осуществляется при помощи атрибута name ("Колво" - @Колво, "Сумма" - $Сумма). В одной секции <sql:query> может находиться несколько SQL-запросов, в отличие от XPath-запросов, каждый из которых должен заключаться в свою секцию <sql:xpath-query>. В данном примере SQL-запросы пришлось разнести по разным секциям, так как первый из них выполняется на сервере, а второй - на клиенте. Атрибут client-side-xml эквивалентен свойству ClientSideXml класса SqlXmlCommand, с которым мы сталкивались в п.6. В <sql:query> могут присутствовать не только select'ы, но и операторы объявления и модификации данных, а также вызовы хранимых процедур, что позволяет помещать в секцию не просто несколько разрозненных операторов, а последовательность, связанную логикой выполнения. Условие - весь select-вывод должен идти в виде XML. Допускаются опции AUTO и EXPLICIT. XPath-запросы выполняются относительно заданной в атрибуте mapping-schema аннотированной схемы. Для каждого XPath-запроса она может быть своя. Отметим, что некоторые (хоть все) из задействованных аннотированных схем могут располагаться непосредственно в файле шаблона, так что предыдущий шаблон можно записать как:


<?xml version="1.0" encoding="utf-8" ?> 
<Солянка xmlns:sql="urn:schemas-microsoft-com:xml-sql">
  <xs:chema xmlns:xs="http://www.w3.org/2001/XMLSchema"            	                             
  _    xmlns:ms="urn:schemas-microsoft-com:mapping-schema"
	     id="SQLSchema2" sql:is-mapping-schema="1">
...Схема с рис.5...
  </xs:schema>
 ...<sql:header> и <sql:query>-секции с рис.6...
  <sql:xpath-query mapping-schema="..\Schemas\SQLSchema1.xsd">
	Клиент[number(Заказы/Заказ/Стоимость)&gt;$Сумма]
  </sql:xpath-query>
  <sql:xpath-query mapping-schema="#SQLSchema2">
    Сотрудник
  </sql:xpath-query>
</Солянка>

Рис.7

Таким образом, результат выполнения шаблона может состоять из фрагментов различных схем, соответствующих разным секциям, объединенных корневым элементом. Остается сохранить его в файле формата UTF-8 (Скрипт 11).


static void Execute_TemplateFile_SQLXML()
{
	...
	cmd.CommandText = "..\\Templates\\XMLTemplate1.xml";
	cmd.CommandType = SqlXmlCommandType.TemplateFile;
	SqlXmlParameter prm = cmd.CreateParameter();
	prm.Name = "@Колво"; prm.Value = 20; 
	XmlDocument xml = new XmlDocument();
	xml.Load(cmd.ExecuteStream());
	...
}

Скрипт 11

Вызывающее приложение передает шаблону только один параметр @Колво = 20 (показать клиентов и количество сделанных ими заказов, если оно превышает 20). Для другого запроса будет взято значение параметра по умолчанию из шаблона - <sql:param name="Сумма">1000</sql:param> (показать только тех клиентов, которые сделали хотя бы один заказ на сумму свыше 1000). Если приложение не обеспечивает параметра при вызове шаблона и секция <sql:header> не содержит значение параметра по умолчанию, то для него будет использовано значение по умолчанию, определенное в схеме SQL Server (DEFAULT для поля таблицы или для параметра хранимой процедуры). Если значение по умолчанию в схеме SQL Server также не определено, значение параметра полагается в NULL. Стоит обратить внимание на конвертацию number(Заказы/Заказ/Стоимость) в одном из XPath-запросов в шаблонах рис.6, 7. Дело в том, что несмотря на объявление стоимости как xs:type="xs:float" параметр $Сумма норовит передаваться как nvarchar, что приводит к неверным результатам (напр. '5.00' > '1000'). Еще один, эквивалентный, способ вызова шаблона из SqlXml состоит в использовании свойства CommandStream вместо CommandText. Соответственно должно быть скорректировано свойство CommandType.


...
cmd.CommandStream = new FileStream("..\\Templates\\XMLTemplate1.xml", FileMode.Open, FileAccess.Read);
cmd.CommandType = SqlXmlCommandType.Template;
...

Или даже так:


...
cmd.CommandStream = new MemoryStream();
		StreamWriter sw = new StreamWriter(cmd.CommandStream, System.Text.Encoding.UTF8); 
		sw.Write("<?xml version='1.0' encoding='utf-8' ?> " +
		"<Солянка xmlns:sql='urn:schemas-microsoft-com:xml-sql'>" +
		
...Содержание шаблона с рис.6...
"</Солянка>"); sw.Flush(); cmd.CommandStream.Position = 0; cmd.CommandType = SqlXmlCommandType.Template; ...

В свойстве XslPath класса SqlXmlCommand может задаваться ссылка на XSL-преобразование, которому подвергается сформированный на основе шаблона XML-файл. Преобразование можно также оговорить в корневом элементе шаблона (например, sql:xsl="....xsl">).
Для повышения производительности применяется кэширование шаблонов, что означает, что они не выгружаются из памяти после первого выполнения. Емкость кэша (в штуках) задается в HKLM\SOFTWARE\Microsoft\MSSQLServer\Client\SQLXML3\TemplateCacheSize и по умолчанию равна 31. Чтобы запретить кэширование, необходимо зайти в закладку Advanced свойств виртуальной директории SQL Server (см. п.13) и отметить Disable Caching of template. Аналогично настраивается кэширование аннотированных схем (HKLM\SOFTWARE\Microsoft\MSSQLServer\Client\SQLXML3\SchemaCacheSize).

11 UpdateGrams

До сих пор под XML-взаимодействием с SQL Server понималось, в основном, чтение данных с сервера в XML-формате с помощью SELECT ... FOR XML или XPath. Возникает вопрос: можно ли их модифицировать в рамках XML-представления. Про возможность модификации косвенно упоминалось пару раз: когда мы говорили про возможности ADO.Net (п.7) и про шаблоны (п.10). Первый способ предусматривает связь с сервером через DataAdapter и работу с DataSet в рамках его XMLной ипостаси. Второй можно реализовать, поместив запросы на обновление в секцию <sql:query> шаблона:


<Root xmlns:sql="urn:schemas-microsoft-com:xml-sql">
  <sql:header>
     <sql:param name="Имя"></sql:param>
     <sql:param name="Фамилия"></sql:param>
  </sql:header>
  <sql:query>
    UPDATE Customers SET ContactName = 'Maria Anders' WHERE CustomerID = 'ALFKI'
    INSERT Employees (FirstName, LastName) Values (@Имя, @Фамилия)
  </sql:query>
</Root>

Его выполнение:


static void Execute_UpdateTemplate_SQLXML()
{
	...
	cmd.CommandText = "..\\Templates\\XMLTemplate3.xml";
	cmd.CommandType = SqlXmlCommandType.TemplateFile;
	SqlXmlParameter prm = cmd.CreateParameter();
	prm.Name = "@Имя"; prm.Value = "ааа"; 
	prm = cmd.CreateParameter();
	prm.Name = "@Фамилия"; prm.Value = "ббб"; 
	cmd.ExecuteNonQuery();
}

равнозначно тому, как если бы эти запросы были выполнены обычным путем. Возникает вопрос: можно ли модифицировать данные непосредственно на сервере (не кэшируя их предварительно в DataSet) и работая с ними, как с XML, а не через реляционные операторы. Такой способ предоставляет UpdateGrams.
Пример файла UpdateGrams, с которым мы будем работать, приведен на рис.8.


<?xml version="1.0" encoding="utf-8" ?> 
<ROOT xmlns:updg="urn:schemas-microsoft-com:xml-updategram">

<updg:header>
	<updg:param name="Дата"/>
	<updg:param name="Стоимость" />
</updg:header>

<updg:sync mapping-schema="..\Schemas\SQLSchema1.xsd">
 	
	<updg:before>
		<Клиент Имя="Ana Trujillo" updg:id="x" />
		<Клиент Имя="Antonio Moreno" updg:id="y" /> 
	</updg:before>
	<updg:after>
		<Клиент updg:id="y"
		Фирма="Рога&Копыта" 
		Имя="Дон Педро" />
	</updg:after>
	
	<updg:before>
		<Клиент CustomerID="ALFKI" />
	</updg:before>
	<updg:after>
		<Клиент CustomerID="ALFKI">
			<Заказы>
				<Заказ>
					<Дата>
						$Дата
					</Дата>
					<Стоимость>
						cast(@Стоимость as money)
					</Стоимость>
				</Заказ>
			</Заказы>
		</Клиент>
  </updg:after>

</updg:sync>

<updg:sync mapping-schema="..\Schemas\SQLSchema2.xsd">
   
	<updg:after>
		<Сотрудник updg:at-identity="x" 
		Имя="Альбус" 
		Фамилия="Дамблдор" >
			<Сотрудник Имя="Минерва" 
			Фамилия="МакГонагалл" 
			ReportsTo="x" />
		</Сотрудник>
	
	</updg:after>

</updg:sync>

</ROOT>

Поскольку я постарался напихать в него по максимуму показательных вещей, разберем данный updategrams по частям. Начнем с того, что это XML-файл, структура которого довольно близка к уже рассмотренным нами шаблонам. В </updg:sync> может задаваться аннотированная схема, которая отображает реляционную схему на XSD и благодаря которой мы можем работать с информацией в БД как с XML. Если схема не указана, предполагается отображение по умолчанию: каждая запись таблицы - элемент, поля в ней - атрибуты. По большому счету updategrams состоит из секций трех типов: <updg:header> - в ней передаются возможные параметры; <updg:before> и <updg:after>. Если запись фигурирует только в <updg:before>, она удаляется; если только в <updg:after> - вставляется; если и там, и там - обновляется. Рассмотрим, например, ситуацию, когда весь updategrams состоит только из


<updg:sync mapping-schema="SQLSchema1.xsd">
    	<updg:before>
		<Клиент Имя="Ana Trujillo" />
</updg:before>
	<updg:after>
	</updg:after>
</updg:sync>

В процессе его выполнения на SQL Server происходит следующее:

SET XACT_ABORT ON
BEGIN TRAN
DECLARE @eip INT, @r__ int, @e__ int
SET @eip = 0
DELETE Customers WHERE ( ContactName=N'Ana Trujillo' ) ;
SELECT @e__ = @@ERROR, @r__ = @@ROWCOUNT
IF (@e__ != 0 OR @r__ != 1) SET @eip = 1
IF (@r__ > 1) RAISERROR
( N'SQLOLEDB Error Description: Ambiguous delete, unique identifier required Transaction aborted ', 16, 1)
ELSE IF (@r__ < 1) RAISERROR
( N'SQLOLEDB Error Description: Empty delete, no deletable rows found Transaction aborted ', 16, 1)
IF (@eip != 0) ROLLBACK ELSE COMMIT
SET XACT_ABORT OFF

Из этого сразу видно несколько важных вещей. Первое: каждая секция <updg:sync> открывает отдельную транзакцию (см. BEGIN TRAN). Второе: описание модифицируемого элемента (<Клиент Имя="Ana Trujillo" />) в <updg:before> и/или <updg:after> должно соответствовать одной записи, неважно, идет ли маппирование по дефолту или через аннотированную схему. Если записей, отвечающих эквивалентному условию WHERE не находится (@r__ < 1) или больше одной (@r__ > 1), будет сгенерирована ошибка (RAISERROR) и транзакция откатится (ROLLBACK).
Поскольку в каждой секции <updg:before> и <updg:after> может находиться несколько записей, то необходимо как-то сопоставить их друг другу в случае обновления. Например, при парсинге этого шаблона


	<updg:before>
		<Клиент Имя="Ana Trujillo" updg:id="x" />
		<Клиент Имя="Antonio Moreno" updg:id="y" /> 
	</updg:before>
	<updg:after>
		<Клиент updg:id="y"
		Фирма="Рога&Копыта" 
		Имя="Дон Педро" />
	</updg:after>

SqlXml должен понимать, что клиента по имени Ana Trujillo мы хотим просто удалить, а клиента по имени Antonio Moreno обновляем, поэтому строка <Клиент updg:id="y" Фирма="Рога&Копыта" Имя="Дон Педро" /> в <updg:after> соответствует именно ему. Это можно сделать двумя способами. Первый - пометить их одним и тем же updg:id. Второй способ - однозначно идентифицировать записи при помощи первичного ключа таблицы. Для этого нужно а) определить ключ в аннотированной схеме (вспоминайте аннотацию ms:key-fields) и б) явно сослаться на него в <updg:before>/<updg:after> (скажем, <Сотрудник ID_Сотрудника="..." />).

Следующая часть


	<updg:before>
		<Клиент CustomerID="ALFKI" />
	</updg:before>
	<updg:after>
		<Клиент CustomerID="ALFKI">
			<Заказы>
				<Заказ>
					<Дата>
						$Дата
					</Дата>
					<Стоимость>
						cast(@Стоимость as money)
					</Стоимость>
				</Заказ>
			</Заказы>
		</Клиент>
  </updg:after>

производит обновление и вставку одновременно. В заказы, сделанные клиентом с идентификатором ALFKI добавляется еще один заказ. При этом SQL Server сам распознает, что новую запись в таблице Orders нужно подчинить данному клиенту в таблице Customers и автоматически устанавливает для нее CustomerID в "ALFKI".


exec sp_executesql N'... 
INSERT Orders (OrderDate, Freight, CustomerID) 
VALUES (@Дата, cast(@Стоимость as money), N''ALFKI'') ... ', 
        N'@Дата nvarchar(19),@Стоимость sql_variant', 
        N'08.04.2002 18:41:34', 100

Это происходит благодаря тому, что поле CustomerID указано в качестве связующего в <ms:relationship> схемы SQLSchema1.xsd. Обратите внимание, что несмотря на то, что в аннотирующей схеме ему явно не соответствует никакой элемент/атрибут, на него можно ссылаться в updategrams-файле. Для автоматического подчинения родительскую запись в <updg:before>/<updg:after> необходимо идентифицировать именно по CustomerID, которое значится как parent-key в схеме (parent-key="CustomerID"). Определение записи по другим атрибутам, пусть даже однозначно ее идентифицирующим (например, <Клиент Имя="Maria Anders" Фирма="Alfreds Futterkiste" ...>), к такому эффекту не приводит. Следующая часть updategrams-файла:


<updg:sync mapping-schema="..\Schemas\SQLSchema2.xsd">
   
	<updg:after>
		<Сотрудник updg:at-identity="x" 
		Имя="Альбус" 
		Фамилия="Дамблдор" >
			<Сотрудник Имя="Минерва" 
			Фамилия="МакГонагалл" 
			ReportsTo="x" />
		</Сотрудник>
	
	</updg:after>

</updg:sync>

открывает другую транзакцию и работает с другой аннотирующей схемой, которая, как вы помните, превращает parent-child таблицу в XML-иерархию. Здесь демонстрируется не автоматическое, а "ручное" подчинение. Мы вставляем одновременно две записи. Чтобы подчинить вторую первой, нужно значение поля ReportsTo для второй установить в первичный ключ первой. Положение осложняется тем, что первичный ключ - это identity, и его значение априори неизвестно. Выйти из положения позволяет аннотация updg:at-identity. Кстати, здесь мы снова обращаемся к полю (ReportsTo), которое нигде в схеме не засвечено, а используется только в описании отношения (<ms:relationship>).
Дата и стоимость заказа передаются в виде параметров. Если параметр подставляется standalone, можно использовать XPath-обозначение ($Дата), если же над ним по ходу выполняются какие-то SQL-преобразования - то SQLное (@Стоимость).
Я не стал дальше усложнять пример, но хотел бы отметить еще две полезных аннотации. <updg:nullvalue> позволяет оговорить SQLный NULL:


<updg:sync mapping-schema="SQLSchema2.xsd" updg:nullvalue="Пусто">
	<updg:after>
		<Сотрудник updg:at-identity="x" 
		Имя="Альбус" 
		Фамилия="Дамблдор" >
			<Сотрудник Имя="Минерва" 
			Фамилия="МакГонагалл" 
			ReportsTo="Пусто" />
		</Сотрудник>
	
	</updg:after>

</updg:sync>

В этом случае второй сотрудник не будет подчинен первому, т.к. несмотря на то, что в XML эта запись вложена, при занесении ее в таблицу ей не будет назначено никакого руководителя (ReportsTo=NULL).
Аннотация ms:inverse применяется не в UpdateGrams, а при описании аннотированной схемы. Мы не рассматривали ее в п.9, потому что она имеет смысл только тогда, когда та используется затем в updategrams. Дело в том, что SQL Server считает, что иерархия "родитель-потомок" в XML соответствует ограничению первичный - внешний для ключей связанных таблиц. Рассмотрим updategrams вида


<updg:sync mapping-schema="..\Schemas\SQLSchema2.xsd">
	<updg:before>
		<Клиент CustomerID="ALFKI">
			<Заказы>
				<Заказ />
			</Заказы>
		</Клиент>
	</updg:before>
</updg:sync>

Сразу ясно, что этот пример вызовет ошибку, поскольку данный клиент сделал более одного заказа, а условием UpdateGrams является однозначная идентификация записи. Но дело не в этом. На сервере будут выполнены действия в следующем порядке: DELETE Orders WHERE CustomerID = N'ALFKI'; DELETE Customers WHERE CustomerID = N'ALFKI'. (Использование атрибутов, не входящих в определение <ms:relationship>, - напр., <Клиент Имя="Maria Anders"> приведет к полному DELETE Orders). Из этой последовательности видно, что SqlXml сначала пытается произвести обновления / удаления в дочерней таблице, а уже потом из родительской, чтобы по возможности не противоречить referential constraints. В жизни бывают ситуации, когда схема может задавать вложенность элементов, противоположную направлению действия ограничения primary key/foreign key в таблицах. Тогда SqlXml, предполагая, что вложенный элемент соответствует внешнему ключу, полезет на самом деле в первичную таблицу, получит от SQL Server по рукам и откатит транзакцию. Чтобы побороть такую ситуацию, в <ms:relationship> нужно поставить ms:inverse="true".
Полный список аннотаций, как всегда, можно найти в документации к SQLXML 3.0. Вызов UpdateGrams из приложения происходит аналогично вызову шаблона через файл (см. Скрипт 12) или Stream. При его тестировании в БД Northwind предварительно нужно превратить связь Orders -> [Order Details] из строгой в каскадную. Поскольку [Order Details] не участвует в updategrams, ее FK будет препятствовать обновлению Orders (в отличие от Customer -> Orders, где это учитывается автоматически за счет указания в relationship в аннотированной схеме).

...
cmd.CommandText = "..\\Templates\\UpdateGrams1.xml";
cmd.CommandType = SqlXmlCommandType.TemplateFile;
SqlXmlParameter prm = cmd.CreateParameter();
prm.Name = "@Дата"; prm.Value = DateTime.Now.ToString();
prm = cmd.CreateParameter();
prm.Name = "@Стоимость"; prm.Value = 100;
cmd.ExecuteNonQuery();

Скрипт 12

12 XML Bulk Load

Большие объемы данных неэффективно "заливать" в базу чередой последовательных insert'ов. Для этого применяются средства массовой загрузки. Аналогом bcp / BULK INSERT при работе с XML-файлами выступает XML Bulk Load. Он не грузит весь документ целиком в память, а прочитывает его по отдельным узлам, понимая на основе аннотированной схемы, когда запись заканчивается и ее можно отправить SQL Server'у для вставки. Использование аннотированных схем позволяет осуществлять наполнение одновременно нескольких связанных таблиц. XML Bulk Load не есть утилита с графическим интерфейсом, а всего лишь СОМ-компонент, устанавливаемый SQLXML веб-релизами. Массовая закачка XML-документа осуществляется программным путем - достаточно написать VB-скрипт из нескольких строчек. Я надеюсь, что эта идея вас не пугает, потому что если вы дочитали до этого места, значит, вы, скорее, разработчики, нежели пользователи или администраторы. Для работы из-под .NET Framework необходимо импортировать библиотеку типов Microsoft SQLXML Bulkload 3.0 Type Library (...\Program Files\Common Files\System\Ole DB\xblkld3.dll).
Объектная модель XML Bulk Load тривиальна. Она состоит из единственного объекта - SQLXMLBulkLoad с единственным методом Execute, принимающим два параметра: аннотированную схему и сам XML-документ. Аннотированная схема, как всегда, задает правила разноски XML- содержания: в какое поле какой таблицы положить тот или иной элемент или атрибут. Собственно, все.
Вот XML, который требуется перенести в БД:


<Книги_по_XML>
    <Книга Название="The Guru's Guide to SQL Server Stored Procedures, 
	XML, and HTML" ISBN="0201700468" Страниц="576">
        <Порядковый_номер>1</Порядковый_номер>
        <Издательство>Wesley Professional</Издательство>
        <Цена_на_Амазоне>34.99</Цена_на_Амазоне>
        <Дата_выхода>2001-12-21</Дата_выхода>
        <Авторы>
            <Автор Имя="Ken" Фамилия="Henderson" />
            <Автор Имя="Ron" Фамилия="Soukup" />
        </Авторы>
    </Книга>
    <Книга Название="Programming Microsoft SQL Server 2000 With XML 
	(Pro-Developer)" ISBN="0735613699" Страниц="400">
        <Порядковый_номер>2</Порядковый_номер>
        <Издательство>Microsoft Press</Издательство>
        <Цена_на_Амазоне>41.99</Цена_на_Амазоне>
        <Дата_выхода>2001-06-01</Дата_выхода>
        <Авторы>
            <Автор Имя="Graeme" Фамилия="Malcolm" />
        </Авторы>
    </Книга>
...
</Книги_по_XML>

Вот аннотированная схема, которую я для него определил (схема должна находиться в отдельном файле, размещение ее в самом XML-документе не допускается).


<?xml version="1.0" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:ms="urn:schemas-microsoft-com:mapping-schema">
	<xs:annotation>
		<xs:appinfo>
			<ms:relationship name="Книга_Авторы" parent="Book" 
			parent-key="BookID" child="Author" child-key="BookID" />
		</xs:appinfo>
	</xs:annotation>
	<xs:element name="Книга" ms:relation="Book">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="Порядковый_номер" 
				ms:field="BookID" ms:datatype="int" />
				<xs:element name="Издательство" 
				ms:field="Publishing" ms:datatype="varchar(50)" />
				<xs:element name="Цена_на_Амазоне" 
				ms:field="Price" ms:datatype="numeric(6,2)" />
				<xs:element name="Дата_выхода" 
				ms:field="IssueDate" ms:datatype="smalldatetime" />
				<xs:element name="Авторы" 
				maxOccurs="1" ms:is-constant="1">
					<xs:complexType>
						<xs:sequence>
							<xs:element name="Автор" minOccurs="0" 
							maxOccurs="unbounded" ms:relation="Author" 
							ms:relationship="Книга_Авторы">
								<xs:complexType>
									<xs:attribute name="Имя" 
									ms:field="FirstName" 
									ms:datatype="nvarchar(50)" />
									<xs:attribute name="Фамилия" 
									ms:field="LastName" 
									ms:datatype="nvarchar(50)" />
								</xs:complexType>
							</xs:element>
						</xs:sequence>
					</xs:complexType>
				</xs:element>
			</xs:sequence>
			<xs:attribute name="Название" ms:field="Title" 
			ms:datatype="nvarchar(200)" />
			<xs:attribute name="ISBN" ms:field="ISBN" 
			ms:datatype="char(10)" />
			<xs:attribute name="Страниц"
			ms:field="NumPages" ms:datatype="smallint" />
		</xs:complexType>
	</xs:element>
</xs:schema>

И вот скрипт, который осуществляет загрузку:


class BulkExample
{
	static void BulkLoad_File_SQLXML()
	{
		SQLXMLBULKLOADLib.SQLXMLBulkLoad3Class bl = 
		new SQLXMLBULKLOADLib.SQLXMLBulkLoad3Class();
		bl.ConnectionString = "Provider=SQLOLEDB;...";
		bl.SchemaGen = true;
		bl.SGDropTables = true;
		bl.KeepNulls = true;
		bl.Transaction = true;
		bl.ErrorLogFile = "..\\BulkCopy\\XMLDocForBulkLoad.err";
		bl.Execute("..\\BulkCopy\\XMLDocForBulkLoad.xsd", 
		"..\\BulkCopy\\XMLDocForBulkLoad.xml");
	}

	[STAThread]
	static void Main()
	{
		BulkLoad_File_SQLXML();
	}
}

Скрипт 13

Следует обратить внимание на атрибут [STAThread], поскольку компонент пока работает только в однопоточном режиме. Свойство SchemaGen, определяет, должны ли таблицы под загрузку создаваться или вставка идет в уже имеющиеся. Чтобы просто создать структуры и не переносить при этом сами данные, нужно еще поставить BulkLoad = false. Имена таблиц и полей, их типы и прочие метаданные определяются аннотированной схемой (ms:relation, ms:field, ms:datatype).
Если таблицы уже существуют, то SGDropTables = true заставляет их пересоздаться. Удаление таблиц происходит в том порядке, в котором они упоминаются в XML-файле и может приводить к конфликту с ограничениями primary key/foreign key.
Рассмотренный в п.11 ms:inverse при этом не помогает. В том случае, если таблица есть и для поля определено значение по умолчанию, оно будет использоваться, когда в XML-файле соответствующий элемент или атрибут пропущен, но если поставить KeepNulls = true, то значение по умолчанию будет проигнорировано и в поле будет поставлен Null. Аналогично действует свойство KeepIdentity. Если вставка идет в поле типа identity, то false заставляет игнорировать значения для этого поля в XMLном файле и использовать автоинкремент, определенный для него на сервере. CheckConstraints определяет, будут ли проверяться constraints (primary key/foreign key и пр.) при загрузке данных. Свойство Transaction заставляет все действия по загрузке проходить в масштабе единой транзакции, так что ежели что случится, то все будет откачено. Его нельзя использовать при загрузке BLOBов. ForceTableLock устанавливает табличную блокировку на таблицы, задействованные в ходе bulk load. Если в аннотированной схеме поле помечено атрибутом dt:type="id" (xmlns:dt="urn:schemas-microsoft-com:xml:datatypes"), то свойство SGUseID = true приведет к тому, что при создании таблицы на него будет создано ограничение primary key.
Если в схеме в <xs:element name<font color=blue>="Книга" ms:relation="Book"> поставить атрибут, ms:key-fields="BookID", то между таблицами Book и Author будет создано ограничение primary key/foreign key по полю BookID. Независимо от этого указание в схеме отношения между таблицами <ms:relationship name="Книга_Авторы" parent="Book" parent-key="BookID" child="Author" child-key="BookID" /> обеспечивает во время массовой загрузки автоматическое заполнение поля BookID в дочерней таблице Author значениями BookID из родительской записи таблицы Book, как происходит в нашем случае:

BookID Publishing Price IssueDate NumPages ISBN Title
1. Wesley Professional 34.99 2001-12-21 00:00:00 576 0201700468 The Guru's Guide to SQL Server Stored Procedures, XML, and HTML
2. Microsoft Press 41.99 2001-06-01 00:00:00 400 0735613699 Programming Microsoft SQL Server 2000 With XML (Pro-Developer)
... ... ... ... ... ... ...

LastName FirstName BookID
Henderson Ken 1
Soukup Ron 1
Malcolm Graeme 2
Griffin John 3
Williams Kevin 4
... ... ...

Методу Execute можно передавать не только название XML-документа, но и ADODB.Stream:


...

StreamReader sr = File.OpenText("..\\BulkCopy\\XMLDocForBulkLoad.xml");
ADODB.StreamClass sc = new ADODB.StreamClass();
sc.Charset = "UTF-8"; 
sc.Open(System.Type.Missing, ADODB.ConnectModeEnum.adModeUnknown, 
ADODB.StreamOpenOptionsEnum.adOpenStreamUnspecified, "", "");
sc.WriteText(sr.ReadToEnd(), ADODB.StreamWriteEnum.stWriteChar);
sc.Position = 0;
bl.Execute("..\\BulkCopy\\XMLDocForBulkLoad.xsd", sc);
sr.Close(); sc.Close();

что позволяет загружать в БД динамически сгенерированный внутри кода XML без необходимости его промежуточного сохранения. Для загрузки XML-фрагмента (набора элементов без корневого, не являющегося каноническим well-formed документом) необходимо использовать свойство XMLFragment.

13 Доступ к SQL Server по HTTP

Насколько вы заметили, в п.1 среди одного из ключевых преимуществ поддержки XML в СУБД называлась возможность доступа по HTTP. Опять же, насколько вы заметили, HTTP у нас до сих пор, в общем-то, не пахло. Настало время исправить этот недостаток. Напрямую по HTTP с SQL Server общаться нельзя. Во всяком случае пока между клиентом и SQL Server-ом приходится иметь IIS. В состав SQLXML 3.0 входит ISAPI-фильтр ...\Program Files\Common Files\System\Ole DB\sqlis3.dll, который принимает на себя обработку методов GET, HEAD, POST, вытаскивает из них тело запроса и отправляет его на SQL Server. SQL Server его обрабатывает, ответ в виде XML передается Web-серверу, а от него по HTTP - клиенту. Таким образом, второй участок пути (IIS - SQL Server) относится к локальному взаимодействию с SQL Server, с которым, я надеюсь, все ясно, т.к. им мы занимались на протяжении 12-ти предыдущих параграфов. Чтобы разобраться с первым участком (клиент - IIS), нам нужно понять две вещи: как правильно упаковать запрос в GET и POST-обращения и как их отправить фильтру. С фильтром все происходит классически: на IISе создается виртуальная директория, к которой он подвязывается. Для ее создания можно воспользоваться графической утилитой Configure IIS Support в SQLXML 3.0 (в отличие от аналогичной утилиты в составе SQL Server она поддерживает все те новшества, которые были представлены в этом релизе), либо сделать это программным путем, написав скрипт, похожий на тот, что представлен ниже.


static void Main()
{
	const string SQLSrv = "(local)";
	const string DBName = "Northwind";
	string SQLVirtDirName = "SQLXML3";
	string SQLVirtDirPhysPath = Directory.GetParent(Directory.GetCurrentDirectory()).FullName;

	//Получаем ссылку на основной объект
	SQLVDIRLib.SQLVDirControl3 sqlVDirCtl = new SQLVDIRLib.SQLVDirControl3();
		
	//Соединяемся с сервером localhost и дефолтовым web-сайтом
	sqlVDirCtl.Connect("localhost", "1");
		
	//Получаем коллекцию виртуальных директорий
	SQLVDIRLib.IVRootEnum2 sqlVDirs = sqlVDirCtl.SQLVDirs;
		
	//Если вирт.дир. с таким именем существует, удаляем
	if (sqlVDirs.get_Exists(SQLVirtDirName))
		sqlVDirs.xxxRemoveVirtualDirectory(SQLVirtDirName);
		
	//Создаем новую вирт.дир.
	SQLVDIRLib.ISQLVDir2 sqlVDir = sqlVDirs.AddVirtualDirectory(SQLVirtDirName);
		
	//1-я закладка New Virtual Directory Properties
	sqlVDir.PhysicalPath = SQLVirtDirPhysPath;
		
	//2-я закладка New Virtual Directory Properties
	sqlVDir.SecurityMode = SQLVDIRLib.tagSecurityModes.smINTEGRATED;
		
	//3-я закладка New Virtual Directory Properties
	sqlVDir.ServerName = SQLSrv; sqlVDir.DatabaseName = DBName;
		
	//4-я закладка New Virtual Directory Properties
	sqlVDir.AllowFlags = 
	SQLVDIRLib.tagAllowFlags.afURL_QUERIES | SQLVDIRLib.tagAllowFlags.afTEMPLATES | 
	SQLVDIRLib.tagAllowFlags.afXPATH | SQLVDIRLib.tagAllowFlags.afPOST;
		
	//5-я закладка New Virtual Directory Properties
	//Получить коллекцию виртуальных имен для данной вирт.дир.
	SQLVDIRLib.IVirtualNames2 sqlVDirVNames = sqlVDir.VirtualNames;
	//Добавляем новые виртуальные имена
	//Параметры: название поддир., ее тип, отн.физ.путь (там, где не нужно - пустая строка)
	sqlVDirVNames.xxxAddVirtualName("dbobj", SQLVDIRLib.tagVirtualTypes.vtDBOBJECT, "");
	sqlVDirVNames.xxxAddVirtualName("schem", SQLVDIRLib.tagVirtualTypes.vtSCHEMA, "Schemas");
	sqlVDirVNames.xxxAddVirtualName("templ", SQLVDIRLib.tagVirtualTypes.vtTEMPLATE, "Templates");
		
	SQLVDIRLib.IVirtualName sqlWebSvc = sqlVDirVNames.xxxAddVirtualName("websvc", 
	SQLVDIRLib.tagVirtualTypes.vtSOAP, "WebService");
	sqlWebSvc.WebService = "SQLSoapSample"; sqlWebSvc.Domain = "localhost";
		
	sqlWebSvc.SoapMethods.AddStoredProcMethod("ЗаказыКлиентаЗаГод", "CustomerOrdersForYear", 1, 1, 
	SQLVDIRLib.tagSoapMethodOutput.smoDATASET);
	sqlWebSvc.SoapMethods.AddTemplateMethod("ПримерВызоваШаблона",
	"..\\Templates\\XMLTemplate1.xml", 1);
		
	sqlWebSvc.SoapMethods.GenerateConfig(0, 0); sqlWebSvc.SoapMethods.GenerateWSDL();

	//Отсоединяемся от сервера
	sqlVDirCtl.Disconnect();
}

Скрипт 14

Некоторые пояснения к тому, что здесь делалось. Для начала, чтобы воспользоваться функциональностью объекта SQLVDirControl из .NET Framework, нужно импортировать библиотеку классов соответствующей СОМовской dll'и - ...\Program Files\Common Files\System\Ole DB\sqlvdr3.dll. Процесс работы с этим объектом практически повторяет шаги, выполняемые из графического интерфейса визарда по созданию виртуальной директории SQL Server. Для обслуживания анонимных соединений с SQL Server по HTTP следует выбрать учетную запись Windows, авторизованную на доступ к SQL Server, либо SQLный логин, под которыми анонимусы будут ходить на SQL Server. В локальных интранет-сценариях лучше выбирать Windows Integrated Authentication, когда пользователь под своей доменной учетной записью сквозным образом авторизуется на IIS и далее на SQL Server (что соответствует его интегрированному или смешанному режиму безопасности). Basic Authentication (Clear Text) to SQL Server account, как следует из названия, будет запрашивать у пользователя при обращении к виртуальной директории его логин и пароль на SQL Server. Так же, как базовая аутентификация на IISе, они передаются в открытом виде, поэтому при выборе этого сценария следует применять HTTPS. Виртуальная директория, естественно, должна быть привязана к определенному экземпляру SQL Server (он может стоять на машине, отличной от той, где установлен IIS) и к определенной базе данных на нем. Естественно, все механизмы проверки полномочий при обращении к объектам сервера и совершении на нем каких-либо действий, остаются в силе, однако еще до того, как дело дойдет до SQL Server, можно ввести дополнительные ограничения на уровне виртуальной директории. Опция Allow URL queries (SQL, template) разрешает непосредственно по HTTP запрашивать SQL Server при помощи SELECT ... FOR XML или передав текст шаблона. По умолчанию она запрещена, чтобы кто ни попадя из Интернет не приставал к SQL Server с произвольными запросами. Конечно, если у человека нет прав, он и так будет послан, но зачем напрягать сервер и заставлять его тратить время на проверку. Если вы все-таки хотите открыть предыдущую опцию только для запросов на чтение, запретите Allow posted updategrams - при этом будут отвергаться все ad hoc (т.е. те, которые приходят от пользователя, как было показано в конце п.10, а не те, что подготовили вы и сохранили в поддиректории типа template) шаблоны типа UpdateGrams. Allow XPath - то же, что первая опция, но для ad hoc XPath-запросов. Allow template queries - разрешить пользователям вызывать хранящиеся на сервере шаблоны (а не направлять ему свои). Очень похоже на ситуацию, когда при классическом доступе пользователям даются права только на вызовы определенных хранимых процедур, чтобы даже с теми объектами, на которые у них есть права, они не творили, что хотели, а действовали в рамках предусмотренных бизнес-правил. Allow POST - поскольку очень длинный запрос в GET не пролезет, то можно сделать доступной посылку запросов методом POST. Maximum size of POST queries (in kilobytes) позволяет тем не менее ограничить сверху длину запроса.
Виртуальная директория может иметь подкаталоги следующих типов. Template - в них хранятся шаблоны (п.10) - параметризованные сочетания SQL- и XPath-запросов, которые пользователи могут вызывать подобно хранимым процедурам. Schema - для хранения аннотированных схем (п.9), определяющих различные XML-представления реляционной базы данных, привязанной к текущей виртуальной директории, что дает возможность запрашивать ее при помощи XPath. Dbobject - для адресации XPath-запросов напрямую без аннотированной схемы к объектам БД (используется представление по умолчанию, рассмотренное в п.8). Поскольку в данном случае схем хранить не требуется, никакой физической директории ему не отвечает. SOAP - рассмотрение этого типа подкаталогов, равно как и все, что касается конфигурирования поддержки Web-сервисов, мы отложим до следующего параграфа.
После того, как виртуальная директория создана, все способы XML-взаимодействия с SQL Server: запросы SELECT ... FOR XML, XPath-запросы напрямую и через аннотированные схемы, XML-шаблоны, включая UpdateGrams, - становятся доступны через HTTP.

FOR XML-запрос из Скриптов 3, 4 передается, например, так:

http://localhost/SQLXML3?sql=SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE c.ContactName = ? and year(o.OrderDate) = ? FOR XML AUTO&x=Maria Anders&y=1997&root=root

Здесь SQLXML3 - название виртуальной директории SQL Server, параметр sql содержит текст FOR XML-запроса, параметры x и y передают значения параметров запроса (имена параметров запроса в данном случае несущественны в отличие от, например, шаблонов), параметр root - название корневого тэга. В параметре xsl может передаваться имя локального файла, содержащего XSL-преобразование с тем, чтобы результаты запроса сразу получались отфильтрованными и отформатированными. Скрипт 15 демонстрирует отправку этого запроса методом GET, как если бы вышеприведенная строка была просто набрана в URL-строке браузера. Web-сервер отвечает Stream'ом, в котором передается XML-результат запроса от SQL Server. Этот Stream загружается в XML-документ, с которым затем работает приложение.


static void Execute_FORXMLQuery_HTTPGET()
{
HttpWebRequest rqst = (HttpWebRequest) WebRequest.Create("http://localhost/SQLXML3?sql=SELECT 
c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c 
INNER JOIN Orders o ON c.CustomerID = o.CustomerID 
WHERE c.ContactName = ? and year(o.OrderDate) = ? 
FOR XML AUTO&x=Maria Anders&y=1997&root=root");
	rqst.Method = "GET";
	rqst.Credentials = CredentialCache.DefaultCredentials;
	HttpWebResponse rspn = (HttpWebResponse) rqst.GetResponse();
	Stream s = rspn.GetResponseStream();
	XmlDocument xml = new XmlDocument();
	xml.Load(s); rspn.Close(); 
	...
}

Скрипт 15

Скрипт 16 делает то же самое методом POST. Строка запроса передается не в URL, а во входном Stream'е, предварительно запрошенном у Web-сервера. Результаты получаются и обрабатываются аналогично предыдущему примеру.


static void Execute_FORXMLQuery_HTTPPOST()
{
	string query = "sql=SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c 
	INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE c.ContactName = ? 
	and year(o.OrderDate) = ? FOR XML AUTO&x=Maria Anders&y=1997&root=root";
	HttpWebRequest rqst = (HttpWebRequest) WebRequest.Create("http://localhost/SQLXML3");
	rqst.Method = "POST";
	rqst.Credentials = CredentialCache.DefaultCredentials;
	rqst.ContentLength = query.Length;
	StreamWriter sw = new StreamWriter(rqst.GetRequestStream());
	sw.Write(query); sw.Close();
	HttpWebResponse rspn = (HttpWebResponse) rqst.GetResponse();
	XmlDocument xml = new XmlDocument();
	xml.Load(rspn.GetResponseStream()); rspn.Close();
	...
}

Скрипт 16

Форма, представленная в Скрипте 17, функционально эквивалентна 16. В случае формы параметры Web-запроса передаются в полях (связка "имя параметра / значение" соответствует атрибутам name / value). Htm-файл с формой может находиться в любом локальном каталоге, т.к. он служит только средством передачи Web-запроса с клиента на Web-сервер. При ссылках на аннотированные схемы (например, когда из формы выполняется не SQL-запрос, а шаблон) следует иметь в виду, что схемы должны находиться либо в текущем каталоге, либо в каком-нибудь из вложенных в него подкаталогов. Излишне напоминать, что в случае использования русских названий и других расширенных символов файл нужно сохранять не в ANSI, а как UTF-8 или подобном формате.


<head>
	<TITLE>Пример FOR XML через POST из формы</TITLE>
</head>
<body>
	Демонстрирует отправку FOR XML-запроса методом POST из формы
	<form action="http://localhost/sqlxml3" method="POST">
		<B>Имя клиента</B> <input type="text" name="name">
		<br>
		<B>Год</B> <input type="text" name="year"> 
		<input type="hidden" name="contenttype" value="text/xml">
		<input type="hidden" name="sql" value="
SELECT c.ContactName, c.ContactTitle, o.OrderDate FROM Customers c INNER JOIN Orders o 
ON c.CustomerID = o.CustomerID WHERE c.ContactName like ? + '%' and year(o.OrderDate) = ? 
FOR XML AUTO	
">
		<input type="hidden" name="Root" value="Корень">
		<p><input type="submit" value="Сабмит">
	</form>
</body>

Скрипт 17

Все остальные типы XML-запросов к SQL Server передаются по HTTP совершенно аналогично Скриптам 15, 16. Различия составляют только правила формирования строки запроса к Web-серверу. Вот пример передачи по HTTP прямого XPath-запроса из п.8:

http://localhost/SQLXML3/dbobj/Customers[@ContactName>=$НачБуква]/@ContactName?НачБуква='X'

Dbobj - подкаталог виртуальной директории типа Dbobject, определенной при создании виртуальной директории SQLXML3. XPath-запрос входит в URI, параметром служит параметр XPath-запроса.

Вот пример XPath-запроса через аннотированную схему, из п.9 (Скрипт 10).

http://localhost/SQLXML3/schem/SQLSchema1.xsd/Клиент[Адрес/Страна='Spain'%20or%20Адрес/Страна='France']?root=Клиенты

При желании можно передать параметры:

http://localhost/SQLXML3/schem/SQLSchema1.xsd/Клиент[Адрес/Страна=$Country]?Country=Spain&root=Клиенты

Schem - виртуальный подкаталог типа Schema, предполагается, что файл с аннотированной схемой SQLSchema1.xsd находится в нем. В качестве еще одного параметра в строке URL можно передавать ...&xsl=<Путь к файлу с XSLT-преобразованием>.
Шаблоны должны храниться в подкаталоге типа Template. Обращение к шаблону происходит следующим образом:


http://localhost/SQLXML3/templ/XmlTemplate1.xml?Колво=20

По сути, это HTTP-реализация Скрипта 11: происходит вызов шаблона XmlTemplate1 и передача ему одного из параметров (для другого используется значение по умолчанию). Текст шаблона приводился на рис.6. Кроме того, как мы видели в завершение п.10, можно вызывать не только шаблоны, хранящиеся на сервере, но (при наличии прав), передать в HTTP-запросе текст собственного шаблона, который сервер выполнит аналогично хранимым шаблонам (т.н. ad hoc-шаблон). Это очень похоже на передачу FOR XML-запроса, только вместо параметра sql используется параметр template:

http://localhost/SQLXML3?template=<Root xmlns:sql='urn:schemas-microsoft-com:xml-sql'><sql:query> SELECT TOP 1 * FROM Employees FOR XML AUTO</sql:query></Root>

Большие ad-hoc шаблоны проще передавать методом POST:


static void Execute_FORXMLQuery_HTTPPOST()
{
	string query = "template=<Солянка xmlns:sql='urn:schemas-microsoft-com:xml-sql'>
	<sql:header><sql:param name='q'></sql:param><sql:param name='s'>1000
	</sql:param></sql:header>
	<sql:query client-side-xml='0'>SELECT TOP 1 * FROM Employees FOR XML AUTO</sql:query>
	<sql:query client-side-xml='1'>SELECT c.ContactName, COUNT(o.OrderDate) AS 'Кол-во' F
	ROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID GROUP BY c.ContactName 
	HAVING COUNT(o.OrderDate) >= @q FOR XML NESTED</sql:query>
	<sql:xpath-query mapping-schema='Schemas\\SQLSchema1.xsd'>
	Клиент[number(Заказы/Заказ/Стоимость)>$s]
	</sql:xpath-query>
	<sql:xpath-query mapping-schema='Schemas\\SQLSchema2.xsd'>Сотрудник</sql:xpath-query>
	<Солянка>&q=20";
	HttpWebRequest rqst = (HttpWebRequest) WebRequest.Create("http://localhost/SQLXML3");
	rqst.Method = "POST";
	rqst.ContentType = "application/x-www-form-urlencoded";
	byte[] postData = System.Text.Encoding.UTF8.GetBytes(query);
	rqst.ContentLength = postData.Length;
	rqst.Credentials = CredentialCache.DefaultCredentials;
	rqst.GetRequestStream().Write(postData, 0, postData.Length);
	HttpWebResponse rspn = (HttpWebResponse) rqst.GetResponse();
	XmlDocument xml = new XmlDocument();
	xml.Load(rspn.GetResponseStream()); rspn.Close(); 
	...		
}

Скрипт 18

14 SQL Server как Web-сервис

Web-сервис - это компонент, к которому можно обращаться с помощью XML. Результаты вызова метода также оборачиваются в XML. Протокол устройства XML-сообщений, которыми обмениваются клиент и Web-сервис, называется SOAP. В отличие от RPC и других бинарных, зависимых от платформы протоколов, ориентированных на плотное взаимодействие, SOAP предусматривает работу в слабосвязанных средах (Интернет) и, будучи основан на XML, позволяет общаться компонентам независимо от операционной системе или инструмента разработки, в которых они были созданы. Потребность в удаленном взаимодействии компонентов через Интернет назрела давно. В свое время HTML-формы и ASP превратили посетителя Web-узла из пассивного зрителя, просматривающего заранее подготовленные странички, в активного участника, формирующего страницу по информации из базы на основе своих критериев. Однако эта информация доставлялась потребителю по-прежнему в HTML-формате, что, может быть, удобно с точки зрения визуального восприятия, но никуда не годится, если вы хотите, например, выцепить из полученной странички котировки акций, превратить их в recordset и передать своему приложению. Web-сервисы публикуют описания своих интерфейсов в wsdl-файлах (аналог библиотеки типов в СОМ). Например, методу, возвращающему котировки, нужно подать на вход интересующую дату и биржевой символ. Чтобы найти подходящий Web-сервис или выяснить назначение найденного, можно использовать универсальный реестр UDDI и протокол DISCO. В Visual Studio.Net входят средства создания как самих Web-сервисов, так и клиентов, их использующих. Например, не составляет труда создать Web-сервис, который будет принимать имя и параметры хранимой процедуры на SQL Server, выполнять ее и отсылать результаты клиенту. Словом, типичная трехуровневая архитектура, только клиентская компонента, компонента бизнес-логика и сервер теперь не ограничены пределами локальной сети, а находятся где угодно в Интернете. Начиная с SQLXML 3.0 в роли Web-сервиса может выступать SQL Server 2000, так что надобность в промежуточной компоненте теперь отпадает. Присутствие ПО промежуточного слоя наиболее часто оправдывают требованиями безопасности, масштабируемости и программируемости. Последние два, вероятно, наиболее честные, потому что я с трудом понимаю, как толщина middleware может влиять на устойчивость сервера к попыткам несанкционированного проникновения. Если вы дыряво настроили политики безопасности на сервере, то злоумышленник все равно на него придет независимо от того, 5 шагов ему перед этим придется сделать или 10. Вот программируемость - это уже серьезней. Т-SQL до сих пор остается странной смесью мощи и ограничений. Отсутствие многих обыденных для современных языков конструкций, не говоря уже об ООП, приводило к тому, что народ утешал себя тем, что SQL не для того предназначался, и уходил писать логику на С++, Visual Basic, Delphi и т.д. В последнее время круг задач, решаемых SQL Server, очень сильно расширился за рамки пресловутого SQL, следовательно, возможности языка программирования на серверной стороне, как бы он ни назывался, должны им соответствовать. И они будут соответствовать в следующей версии. Пункт третий - масштабируемость. С целью повышения масштабируемости и распределения нагрузки в SQL Server 2000, как известно, были введены распределенные фрагментированные представления. Несмотря на то, что благодаря этому механизму SQL Server занимает в настоящий момент первые места в рейтингах производительности, следует понимать, что в действительности это лишь первый этап на пути к базе данных, автоматически распределяемой по многим серверам в сети, когда каждый узел, получивший запрос, разбивает его на подзапросы к тем серверам, где в действительности лежат нужные данные (а если они продублированы, то к наименее загруженным). Таким образом, понятно, что все три критерия обособления являются достаточно условными, и не всегда компоненты бизнес-логики будут выноситься за пределы SQL Server. Следовательно, тот факт, что клиентское приложение сможет общаться с ним напрямую как с Web-сервисом, на самом деле очень важен. Вернемся к Скрипту 14.
Для запросов к SQL Server по SOAP мы создаем еще один подкаталог websvc виртуальной директории и присваиваем ему тип vtSOAP. Его необходимо проассоциировать с физической поддиректорией (\WebService), т.к. в ней будет храниться wsdl и др. файлы, описывающие данный Web-сервис. Не мудрствуя лукаво, обзовем его SQLSoapSample. Методами могут выступать хранимые процедуры / функции, либо хранимые шаблоны. Первые добавляются как AddStoredProcMethod. Параметрами выступают имя метода, под которым он будет виден и вызываться с клиента, имя хранимой процедуры или функции, если в SELECT участвуют несколько таблиц, соединенных оператором JOIN, то должны ли они на клиенте получаться как вложенные элементы в случае XML-вывода / связанные таблицы в случае DataSet-вывода (1 - да, 0 - как независимые), должны ли ошибки выполнения возвращаться как ошибки SOAP (1 - да, 0 - нет) и что является результатом работы метода Web-сервиса: массив элементов XML, единственный DataSet или массив DataSet'ов. В данном случае выбран второй вариант. Массив DataSet'ов имеет смысл использовать в том случае, когда внутри процедуры выполняются несколько независимых SELECT'ов. Хранимая процедура CustomerOrdersForYear состоит из одного параметризованного запроса, с которым мы работали еще со Скрипта 3.


CREATE PROCEDURE CustomerOrdersForYear @Name nvarchar(20), @Year smallint AS
SELECT c.ContactName, c.ContactTitle, o.OrderDate, o.Freight FROM Customers c 
INNER JOIN Orders o ON c.CustomerID = o.CustomerID 
WHERE c.ContactName like @Name + '%'  and year(o.OrderDate) = @Year

Скрипт 19

В качестве второго метода Web-сервиса выбираем шаблон из п.10 (рис.6). Он добавляется вызовом AddTemplateMethod. Параметрами служат внешнее имя метода, местоположение шаблона относительно данного скрипта и способ передачи ошибок (SOAP - да/нет). Результат шаблона можно передавать только как набор XML-элементов. Строка sqlWebSvc.SoapMethods.GenerateWSDL(); производит автоматическую генерацию wsdl-файла, который кладется в физическую директорию, соответствующую виртуальному подкаталогу типа soap. После этого Web-сервис становится доступным для обращений. Как и в классическом DCOM, на машине клиента нужно создать прокси, содержащего сигнатуру методов Web-сервиса. В графической среде разработки Visual Studio это происходит автоматически при добавлении ссылки на wsdl-файл в раздел Web References проекта. Прокси можно также сгенерировать с помощью утилиты командной строки wsdl.exe:


wsdl.exe /l:CS /o:SQLSrvWebService_Proxy.cs /n:SQLSrvWebRef
http://localhost/SQLXML3/websvc?wsdl

В качестве параметров здесь передаются язык, на котором будет создан прокси, имя файла с прокси, его пространство имен и URI Web-сервиса с его описанием в виде wsdl. Обращение клиента к Web-сервису показано в Скрипте 20.


static void SQLSrvWebService_Client()
{
	SQLSrvWebRef.SQLSoapSample proxy = new SQLSrvWebRef.SQLSoapSample();
	proxy.Credentials = System.Net.CredentialCache.DefaultCredentials;
	
	Console.WriteLine("----------------------------------------------------------------");
	Console.WriteLine("Результаты выполнения шаблона (всегда как массив XML-элементов):");
	object[] resultXml = proxy.ПримерВызоваШаблона("20", "1000");
	foreach (XmlElement x in resultXml)
	Console.WriteLine(x.OuterXml);
		
	Console.WriteLine("\n----------------------------------------------------------------");
	Console.WriteLine("Результаты выполнения хранимой процедуры (заказаны как DataSet):");
	int retVal;
	DataSet resultRel = proxy.ЗаказыКлиентаЗаГод("Maria", 1997, out retVal);

	DataRelation rel = resultRel.Relations[0];
	Console.WriteLine("\nТаблицы связаны отношением " + rel.RelationName);
	Console.Write("Родительская таблица " + rel.ParentTable.TableName + ", родительский ключ - ");
	foreach (DataColumn c in rel.ParentColumns) Console.Write(c.ColumnName + " ");
	Console.Write("\nДочерняя таблица " + rel.ChildTable.TableName + ", дочерний ключ - ");
	foreach (DataColumn c in rel.ChildColumns) Console.Write(c.ColumnName + " ");
	Console.WriteLine("");

	foreach (DataRow rParent in rel.ParentTable.Rows)
	{
		Console.WriteLine("\nТаблица " + rel.ParentTable.TableName);
		foreach (DataColumn cParent in rel.ParentTable.Columns) Console.Write(cParent.Caption 
		+ ": " + rParent[cParent] + "; ");
		Console.Write("\n\tСвязанные записи из таблицы " + rel.ChildTable.TableName);
		foreach (DataRow rChild in rParent.GetChildRows(rel))
		{
			Console.Write("\n\t");
			foreach (DataColumn cChild in rel.ChildTable.Columns) 
			Console.Write(cChild.Caption + ": " + rChild[cChild] + "; ");
		}
		Console.WriteLine("");
	}
}

Скрипт 20

Результат выполнения шаблона получается в виде массива XmlElement, состоящего из единственного элемента, OuterXml которого показан ниже:


<Солянка xmlns:sql="urn:schemas-microsoft-com:xml-sql">
  <Employees EmployeeID="1" LastName="Davolio" FirstName="Nancy" Title="Sales Re
presentative" TitleOfCourtesy="Ms." ... />
  <Customers ContactName="Horst Kloss" Кол-во="28" /><Customers ContactName="Jos
e Pavarotti" Кол-во="31" /><Customers ContactName="Roland Mendel" Кол-во="30" />
  <Клиент Имя="Horst Kloss" Фирма="QUICK-Stop" Должность="Accounting Manager"><А
дрес><Страна>Germany</Страна><Город>Cunewalde</Город><Улица_дом>Taucherstra?e 10
</Улица_дом></Адрес><Заказы><Заказ Номер="10273"><Дата>1996-08-05</Дата><Стоимос
ть>76.07</Стоимость></Заказ><Заказ Номер="10285"><Дата>1996-08-20</Дата><Стоимос
ть>76.83</Стоимость></Заказ>...</Заказы></Клиент>
  <Сотрудник Фамилия="Fuller" Имя="Andrew"><Сотрудник Фамилия="Davolio" Имя="Nan
cy" /><Сотрудник Фамилия="Leverling" Имя="Janet" /><Сотрудник Фамилия="Peacock"
Имя="Margaret" /><Сотрудник Фамилия="Buchanan" Имя="Steven"><Сотрудник Фамилия="
Suyama" Имя="Michael" /><Сотрудник Фамилия="King" Имя="Robert" /><Сотрудник Фами
лия="Dodsworth" Имя="Anne" /></Сотрудник><Сотрудник Фамилия="Callahan" Имя="Laur
a" /></Сотрудник>
</Солянка>

DataSet как результат второго метода состоит из двух таблиц: Customers и Orders. Выбор Row Formatting = Nested на стадии определения метода Web-сервиса приводит к тому, что условие соединения таблиц в запросе (JOIN) переходит в свойство Relations объекта DataSet и позволяет для каждого клиента выбрать сделанные им заказы:


Таблица Customers
ContactName: Maria Larsson; ContactTitle: Owner; Customers_Id: 0; 
	Связанные записи из таблицы Orders
	OrderDate: 03.02.1997 0:00:00; Freight: 17.92; Customers_Id: 0; 
	OrderDate: 28.02.1997 0:00:00; Freight: 16.27; Customers_Id: 0; 
	OrderDate: 12.05.1997 0:00:00; Freight: 188.04; Customers_Id: 0; 
	OrderDate: 06.06.1997 0:00:00; Freight: 242.21; Customers_Id: 0; 

Таблица Customers
ContactName: Maria Anders; ContactTitle: Sales Representative; Customers_Id: 1; 
	Связанные записи из таблицы Orders
	OrderDate: 25.08.1997 0:00:00; Freight: 29.46; Customers_Id: 1; 
	OrderDate: 03.10.1997 0:00:00; Freight: 61.02; Customers_Id: 1; 
	OrderDate: 13.10.1997 0:00:00; Freight: 23.94; Customers_Id: 1; 

Таблица Customers
ContactName: Maria Larsson; ContactTitle: Owner; Customers_Id: 2; 
	Связанные записи из таблицы Orders
	OrderDate: 14.10.1997 0:00:00; Freight: 152.3; Customers_Id: 2; 
	OrderDate: 02.12.1997 0:00:00; Freight: 328.74; Customers_Id: 2; 
	OrderDate: 11.12.1997 0:00:00; Freight: 48.2; Customers_Id: 2; 
The program '[2460] ConsoleApplication1.exe' has exited with code 0 (0x0).

15 Заключение «Назад

У меня закончилось время, которое я отвел на написание данного материала. В соответствии с нашей договоренностью (п.1) я ставлю точку. Отрадно, что мы успели разобрать практически все основные моменты. Для дальнейшего чтения рекомендуются темы OpenXML, DiffGrams и XQuery. Спасибо за внимание.

Конференция "Корпоративные базы данных 2002"
Автор: Алексей Шуленин