Запросы LINQ to XML

78

В предыдущих статьях, посвященных LINQ to XML, были продемонстрированы основные принципы, необходимые для выполнения запросов XML с использованием LINQ to XML. Однако большая часть примеров была специально спроектирована для демонстрации операции или свойства. В этой статье представлены примеры, которые более ориентированы на решение определенной задачи.

Отсутствие иерархического спуска

Во многих примерах, представленных в предыдущих статьях, для получения ссылки на определенный элемент осуществлялся спуск вниз по иерархии посредством рекурсивного вызова операций Element и Elements до тех пор, пока не будет достигнут нужный элемент. Многие примеры содержали следующие строки кода:

IEnumerable<XElement> elements = 
       xDoc.Element("Employees").Elements("Employee");

Здесь спуск начинается с уровня документа, затем получается его дочерний элемент по имени Employees, после чего — его дочерний элемент по имени Employee. Однако вовсе не обязательно спускаться по иерархии вниз подобным способом. Вместо этого можно было бы просто написать код, подобный показанному ниже:

XDocument xDoc = new XDocument(
             new XElement("Employees",
               new XElement("Employee",
                 new XAttribute("type", "Programmer"),
                 new XElement("FirstName", "Alex"), 
                 new XElement("LastName", "Erohin")),
               new XElement("Employee",
                 new XAttribute("type", "Editor"),
                 new XElement("FirstName", "Elena"), 
                 new XElement("LastName", "Volkova"))));

            IEnumerable<XElement> elements = xDoc.Descendants("Employee");

            foreach (XElement e in elements)
                Console.WriteLine("Элемент {0} : значение = {1}",e.Name,e.Value);

В этом примере извлекаются все элементы-потомки документа по имени Employee. Поскольку спуск по определенной ветви дерева XML не производится, необходимо знать схему, поскольку есть вероятность получить элементы не из той ветви, которая нужна. Однако во многих случаях, включая данный, этот подход работает хорошо. Вот результат:

Получение элементов без иерархического спуска

Но предположим, что интересуют не все элементы Employee, т.е. требуется как-то ограничить возвращаемые элементы. В коде ниже представлен пример возврата только элементов, у которых значение дочернего элемента FirstName равно "Elena":

...
IEnumerable<XElement> elements = xDoc
                .Descendants("Employee")
                .Where(e => ((string)e.Element("FirstName")) == "Elena");

На этот раз добавлен вызов операции Where. Обратите внимание на приведение элемента FirstName к типу string. Это нужно, чтобы получить его значение для сравнения с "Elena". Результат показан ниже:

Получение ограниченных элементов без иерархического спуска

Конечно, иногда необходимо контролировать порядок. На этот раз, чтобы обеспечить возврат более одного элемента, когда порядок будет иметь значение, изменим лямбда-выражение операции Where для возврата обоих элементов. Чтобы сделать пример интереснее, запрос выполняется по атрибуту type, и будет переписан с использованием синтаксиса выражений запросов, как показано ниже:

...
IEnumerable<XElement> elements = 
                from e in xDoc.Descendants("Employee")
                where ((string)e.Attribute("type")) != "Illustrator"
                orderby ((string)e.Element("LastName"))
                select e;                  

В этом примере по-прежнему запрашиваются элементы Employee документа, но только те, чей атрибут type отличается от Illustrator. В данном случае это будут все элементы Employee. Затем они упорядочиваются по значению элемента LastName. Обратите внимание, что для получения значений атрибута type и элемента LastName выполняется их приведение к string. Результат получается аналогичным первому примеру.

Сложный запрос

До сих пор все примеры запросов были тривиальными, поэтому прежде чем закрыть тему запросов, следует рассмотреть пример более сложного запроса. В этом примере используются те же данные, которые рекомендованы W3C специально для тестирования прецедентов использования запросов XML.

Пример ниже содержит данные из трех разных XML-документов. Каждый документ создается за счет разбора текстового представления каждого из рекомендованных W3C документов XML. Поскольку пример сложен, он сопровождается объяснениями.

Первый шаг состоит в создании документов из XML:

XDocument users = XDocument.Parse(
        @"<users>
            <user_tuple>
              <userid>U01</userid>
              <name>Tom Jones</name>
              <rating>B</rating>
            </user_tuple>
            <user_tuple>
              <userid>U02</userid>
              <name>Mary Doe</name>
              <rating>A</rating>
            </user_tuple>
            <user_tuple>
              <userid>U03</userid>
              <name>Dee Linquent</name>
              <rating>D</rating>
            </user_tuple>
            <user_tuple>
              <userid>U04</userid>
              <name>Roger Smith</name>
              <rating>C</rating>
            </user_tuple>
            <user_tuple>
              <userid>U05</userid>
              <name>Jack Sprat</name>
              <rating>B</rating>
            </user_tuple>
            <user_tuple>
              <userid>U06</userid>
              <name>Rip Van Winkle</name>
              <rating>B</rating>
            </user_tuple>
          </users>");

      XDocument items = XDocument.Parse(
        @"<items>
            <item_tuple>
              <itemno>1001</itemno>
              <description>Red Bicycle</description>
              <offered_by>U01</offered_by>
              <start_date>1999-01-05</start_date>
              <end_date>1999-01-20</end_date>
              <reserve_price>40</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1002</itemno>
              <description>Motorcycle</description>
              <offered_by>U02</offered_by>
              <start_date>1999-02-11</start_date>
              <end_date>1999-03-15</end_date>
              <reserve_price>500</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1003</itemno>
              <description>Old Bicycle</description>
              <offered_by>U02</offered_by>
              <start_date>1999-01-10</start_date>
              <end_date>1999-02-20</end_date>
              <reserve_price>25</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1004</itemno>
              <description>Tricycle</description>
              <offered_by>U01</offered_by>
              <start_date>1999-02-25</start_date>
              <end_date>1999-03-08</end_date>
              <reserve_price>15</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1005</itemno>
              <description>Tennis Racket</description>
              <offered_by>U03</offered_by>
              <start_date>1999-03-19</start_date>
              <end_date>1999-04-30</end_date>
              <reserve_price>20</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1006</itemno>
              <description>Helicopter</description>
              <offered_by>U03</offered_by>
              <start_date>1999-05-05</start_date>
              <end_date>1999-05-25</end_date>
              <reserve_price>50000</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1007</itemno>
              <description>Racing Bicycle</description>
              <offered_by>U04</offered_by>
              <start_date>1999-01-20</start_date>
              <end_date>1999-02-20</end_date>
              <reserve_price>200</reserve_price>
            </item_tuple>
            <item_tuple>
              <itemno>1008</itemno>
              <description>Broken Bicycle</description>
              <offered_by>U01</offered_by>
              <start_date>1999-02-05</start_date>
              <end_date>1999-03-06</end_date>
              <reserve_price>25</reserve_price>
            </item_tuple>
          </items>");

      XDocument bids = XDocument.Parse(
        @"<bids>
            <bid_tuple>
              <userid>U02</userid>
              <itemno>1001</itemno>
              <bid>35</bid>
              <bid_date>1999-01-07</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U04</userid>
              <itemno>1001</itemno>
              <bid>40</bid>
              <bid_date>1999-01-08</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U02</userid>
              <itemno>1001</itemno>
              <bid>45</bid>
              <bid_date>1999-01-11</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U04</userid>
              <itemno>1001</itemno>
              <bid>50</bid>
              <bid_date>1999-01-13</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U02</userid>
              <itemno>1001</itemno>
              <bid>55</bid>
              <bid_date>1999-01-15</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U01</userid>
              <itemno>1002</itemno>
              <bid>400</bid>
              <bid_date>1999-02-14</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U02</userid>
              <itemno>1002</itemno>
              <bid>600</bid>
              <bid_date>1999-02-16</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U03</userid>
              <itemno>1002</itemno>
              <bid>800</bid>
              <bid_date>1999-02-17</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U04</userid>
              <itemno>1002</itemno>
              <bid>1000</bid>
              <bid_date>1999-02-25</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U02</userid>
              <itemno>1002</itemno>
              <bid>1200</bid>
              <bid_date>1999-03-02</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U04</userid>
              <itemno>1003</itemno>
              <bid>15</bid>
              <bid_date>1999-01-22</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U05</userid>
              <itemno>1003</itemno>
              <bid>20</bid>
              <bid_date>1999-02-03</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U01</userid>
              <itemno>1004</itemno>
              <bid>40</bid>
              <bid_date>1999-03-05</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U03</userid>
              <itemno>1007</itemno>
              <bid>175</bid>
              <bid_date>1999-01-25</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U05</userid>
              <itemno>1007</itemno>
              <bid>200</bid>
              <bid_date>1999-02-08</bid_date>
            </bid_tuple>
            <bid_tuple>
              <userid>U04</userid>
              <itemno>1007</itemno>
              <bid>225</bid>
              <bid_date>1999-02-12</bid_date>
            </bid_tuple>
          </bids>");

Этот пример данных в основном предназначен для представления на сайте Интернет-аукциона. Здесь просто были созданы три документа XML вызовом метода XDocument.Parse на строковом представлении данных XML. Документы предназначены для пользователей, товаров и предложенных цен.

Запрос должен производить список всех цен, превышающих $50. В результатах должна присутствовать дата и цена, наряду с пользователем, предложившим ее, а также наименование товара и его описание. Вот как выглядит запрос:

var biddata = from b in bids.Descendants("bid_tuple")
                    where ((double)b.Element("bid")) > 50
                    join u in users.Descendants("user_tuple")
                    on ((string)b.Element("userid")) equals
                      ((string)u.Element("userid"))
                    join i in items.Descendants("item_tuple")
                    on ((string)b.Element("itemno")) equals
                      ((string)i.Element("itemno"))
                    select new
                    {
                      Item = ((string)b.Element("itemno")),
                      Description = ((string)i.Element("description")),
                      User = ((string)u.Element("name")),
                      Date = ((string)b.Element("bid_date")),
                      Price = ((double)b.Element("bid"))
                    };

Как видите, это сложный запрос. Первый шаг состоит в запрашивании потомков по имени bid_tuple в документе bids, используя метод Descendants. Затем выполняется конструкция where для элементов, имеющих дочерние элементы по имени bid, чье значение превышает 50. Таким образом, извлекаются предложения цены, превышающие $50. Такое скорое выполнение where в запросе может показаться несколько необычным. В действительности это можно было сделать в запросе позжже — непосредственно перед вызовом конструкции select. Однако это означало бы необходимость извлечения и соединения всех записей из XML-документов пользователей и товаров, для которых предложения цены не превышают $50, что явно излишне. За счет как можно более ранней фильтрации результирующего набора сокращается рабочая нагрузка на остальную часть запроса, что повышает производительность.

Отфильтровав рабочий набор по предложениям цены, превышающим $50, выполняется соединение (join) этих предложений с XML-документом пользователей через одноименный в двух наборах элемент userid, чтобы получить имя пользователя. В этой точке есть предложения и связанные с ними пользователи, предложившие цену свыше $50.

Затем результат соединяется с XML-документом наименований товара через одноименный элемент itemno, чтобы получить описание товара. Таким образом, появляется соединение предложенной цены, пользователей и товаров.

Снова обратите внимание на необходимость выполнять приведение всех элементов к интересующему типу данных, чтобы получить стоимость товара. Особенно интересен тот факт, что предложенная цена получается за счет приведения элемента bid к типу double. Несмотря на то что входное значение предложенной цены является строкой, поскольку оно может быть успешно преобразовано в double, то есть возможность выполнить приведение к double и получить значение цены в числовом виде двойной точности. Здорово, не правда ли?

Следующий шаг состоит в выборе анонимного класса, содержащего информацию из дочерних элементов соединенных элементов.

Теперь отобразим заголовок и выведем результаты:

Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10}",
        "Date",
        "User",
        "Item",
        "Description",
        "Price");

      Console.WriteLine("\n==========================================================\n");

      foreach (var bd in biddata)
      {
        Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10}$",
          bd.Date,
          bd.User,
          bd.Item,
          bd.Description,
          bd.Price);
      }

Эта часть тривиальна. В действительности здесь все тривиально, кроме самого запроса. Давайте посмотрим на результат:

Сложный запрос, объединяющий три документа с помощью синтаксиса выражений запросов

Как видите, три XML-документа были соединены в одном запросе. Определенно, теперь вы увидели мощь LINQ to XML. Однако это еще не все :)

Пройди тесты
Лучший чат для C# программистов