Операции над полями DataRow

43

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

В дополнение к специфичному для DataRow классу компаратора для операций множеств существует потребность в некоторых специфичных для DataRow операциях. Эти операции определены в статическом классе System.Data.DataRowExtensions внутри сборки System.Data.DataSetExtensions.dll.

Вы наверняка заметили, что почти во всех приведенных до сих пор примерах для извлечения значения объекта DataColumn из DataRow использовалась операция Field<T>. У этой операции два предназначения: корректная проверка эквивалентности и обработка значения null.

С объектами DataRow возникает проблема. Их значения DataColumn не сравниваются правильно на предмет эквивалентности, когда обращение происходит через индексатор объекта DataRow, если столбец имеет тип значения. Причина в том, что поскольку тип данных столбца может быть любым, индексатор возвращает объект типа System.Object. Это позволяет индексатору вернуть целое число, строку или любой другой необходимый тип для этого столбца. И это значит, что если типом столбца является int, то это тип значения, который должен быть упакован в объект типа Object. Подобное действие в Microsoft .NET Framework так и называется — упаковка (boxing). Извлечение типа значения обратно из объекта известно как распаковка (unboxing). Именно в упаковке и кроется проблема.

Рассмотрим некоторый простой код. Для начала возьмем пример сравнения целочисленного литерала с другим целочисленным литералом, имеющим то же значение:

Console.WriteLine("\n(3 == 3) равно {0}", (3 == 3));
Сравнение 3 и 3

Здесь нет никакого сюрприза. Но что случится, когда целое число упаковывается? Рассмотрим код ниже и посмотрим на результат его работы:

Console.WriteLine("\n((Object)3 == (Object)3) равно {0}", ((Object)3 == (Object)3));
Сравнение значения 3, приведенного к Object с другим значением 3, приведенным к Object

Что же произошло? Дело в том, что за счет приведения целочисленного литерала 3 к типу Object было создано два объекта, и сравнивались ссылки (адреса) двух объектов, которые не эквивалентны. При обращении к объектам DataColumn с использованием индексатора объекта DataRow, если любой из столбцов имеет тип значения, то значения таких столбцов упаковываются и не могут правильно сравниваться на предмет эквивалентности.

Чтобы продемонстрировать это, будет создан более сложный пример, который действительно использует объекты DataColumn. В примере есть два массива, каждый своего типа класса. Один — тот же базовый массив студентов, что применялся ранее. Второй массив назначений на курсы с внешними ключами, указывающими на массив студентов. Ниже показан класс StudentClass:

class StudentClass
    {
        public int Id;
        public string Class;
    }

Имея два разных типа классов, необходим метод для преобразования этого массива в объект типа DataTable. Вот этот метод:

static DataTable GetDataTable2(StudentClass[] studentClasses)
        {
            DataTable table = new DataTable();

            table.Columns.Add("Id", typeof(Int32));
            table.Columns.Add("Class", typeof(string));

            foreach (StudentClass studentClass in studentClasses)
            {
                table.Rows.Add(studentClass.Id, studentClass.Class);
            }

            return (table);
        }

Этот метод — не что иное, как копия существующего метода GetDataTable, который был модифицирован для работы с массивами объектов StudentClass. Очевидно, если вы собираетесь работать с массивами в реальном рабочем коде, вам понадобится нечто более абстрактное, чем создание метода для каждого типа класса, для которого нужен объект DataTable.

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

Для примера построим последовательность объектов DataRow из каждого массива и попробуем соединить их по общему столбцу Id, который получается посредством индексации в DataRow с именем столбца, которым является Id. Код показан ниже:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name="Елена Волкова"},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=16, Name="Андрей Мухамедшин"}
                                 };

            StudentClass[] classes = {
                new StudentClass { Id=1, Class="EM-1"},
                new StudentClass { Id=5, Class="MTI"},
                new StudentClass { Id=9, Class="AM-18"},
                new StudentClass { Id=16, Class="EMF"}
                                     };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();
            DataTable dt2 = GetDataTable2(classes);
            IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

            string erohinClass = (from s in seq1
                                  where s.Field<string>("Name") == "Александр Ерохин"
                                    from c in seq2
                                    where c["Id"] == s["Id"]
                                    select (string)c["Class"]).
                                   SingleOrDefault<string>();

            Console.WriteLine("\nКласс для Александра Ерохина: {0}",erohinClass != null ? erohinClass : "null");

В этом запросе следует отметить несколько моментов. Объект DataRow индексируется для получения значений столбцов. Поскольку типом данных значений столбцов является string, они упаковываются что означает потенциальную проблему определения эквивалентности.

Вдобавок можно видеть, что в этом примере при сравнении поля Name со значением "Александр Ерохин" используется операция Field<T>. Пока не обращайте на это внимания. Просто примите к сведению, что операция Field<T> вызывается для предотвращения проблем с упаковкой поля Name, которое используется наряду с полем Id. Также отметьте, что этот запрос комбинирует синтаксис выражений со стандартным синтаксисом точечной нотации. Как видите, выполняется также соединение двух объектов DataTable. Запустим код и посмотрим результат:

Соединение двух столбцов типа значения посредством индексации в DataRow

Строка erohinClass равна null. Это потому, что соединение не может найти в seq2 записи с эквивалентным значением поля Id. Причина — в упаковке поля Id при извлечении с использованием индексатора DataRow. Теперь можно обработать распаковку самостоятельно, изменив строку:

where c["Id"] == s["Id"]

на

where (int)c["Id"] == (int)s["Id"]

После запуска видоизмененного кода получается следующий результат:

Использование приведения для корректной проверки эквивалентности

Это решает проблему упаковки. Однако остается еще одна проблема. Когда вы пытаетесь извлечь значение столбца, используя индексатор объекта DataRow, помните, что значение столбца возвращается как объект типа Object. Поэтому для того, чтобы сравнить его с любым значением или присвоить переменной, необходимо привести его к другому типу данных, как это было сделано путем приведения к int.

Поскольку объекты DataSet используют значение DBNull.Value в качестве значения столбца null, то если значением столбца является DBNull.Value, то приведение его к другому типу данных вызовет генерацию исключения.

К счастью, появление LINQ to DataSet привело к исчезновению обеих этих проблем — сравнения упакованных значений и обработки null — благодаря операциям Field<T> и SetField<T>. Ниже показан предыдущий пример с использованием операции Field<T>:

...
string erohinClass = (from s in seq1
                                  where s.Field("Name") == "Александр Ерохин"
                                    from c in seq2
                                    where c.Field("Id") == s.Field("Id")
                                    select (string)c["Class"]).
                                   SingleOrDefault();
...

Этот код почти такой же, как в предыдущем примере, за исключением вызова операции Field<T> вместо приведения поля к типу int.

Field<T>

Как только что было показано, операция Field<T> позволяет получить значение столбца из объекта DataRow и устраняет описанные выше проблемы приведения DBNull.Value и сравнения упакованных значений.

Операция Field имеет шесть прототипов, которые описаны ниже:

Первый прототип Field

Первый прототип возвращает значение столбца для DataColumn и указанной версии.

public static T Field ( 
        this DataRow first, 
        System.Data.DataColumn column, 
        System.Data.DataRowVersion version);
Второй прототип Field

Второй прототип возвращает значение столбца с указанным именем и версией.

public static T Field ( 
         this DataRow first, 
         string columnName,
         System.Data.DataRowVersion version);
Третий прототип Field

Третий прототип возвращает значение указанного по порядку столбца заданной версии.

public static T Field ( 
         this DataRow first, 
         int ordinal,
         System.Data.DataRowVersion version);
Четвертый прототип Field

Четвертый прототип возвращает текущее значение столбца только для указанного DataColumn.

public static T Field ( 
         this DataRow first, 
         System.Data.DataColumn column);
Пятый прототип Field

Пятый прототип возвращает текущее значение только столбца с указанным именем.

public static T Field ( 
      this DataRow first, 
      string columnName);
Шестой прототип Field

Шестой прототип возвращает текущее значение только для столбца с указанным порядковым номером.

public static T Field ( 
      this DataRow first, 
      int ordinal);

Как вы, возможно, заметили, первые три прототипа позволяют указать, какие версии DataRowVersion значений объектов DataColumn должны быть извлечены.

До сих нор вы уже не раз сталкивались с разнообразными вызовами операции Field<T>. Но только теперь вы можете увидеть все прототипы в действии. Ниже показан тривиальный пример каждого из них:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name="Елена Волкова"},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=16, Name="Андрей Мухамедшин"}
                                 };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

            int id;

            //  Prototype 1.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>(dt1.Columns[0], DataRowVersion.Current)).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 1: {0}", id);

            //  Prototype 2.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>("Id", DataRowVersion.Current)).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 2: {0}", id);

            //  Prototype 3.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>(0, DataRowVersion.Current)).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 3: {0}", id);

            //  Prototype 4.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>(dt1.Columns[0])).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 4: {0}", id);

            //  Prototype 5.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>("Id")).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 5: {0}", id);

            //  Prototype 6.
            id = (from s in seq1
                  where s.Field<string>("Name") == "Александр Ерохин"
                  select s.Field<int>(0)).
                 Single<int>();
            Console.WriteLine("ID номер Ерохина Александра, полученный с помощью прототипа 6: {0}", id);

В коде объявляется массив студентов, из которого создается объект DataTable, как и в большинстве других примеров. Затем также получается последовательность объектов DataRow. После этого друг за другом используются прототипы операции Field<T> для получения поля по имени Id. Обратите внимание, что в каждом запросе поля Id также применяется операция Field<T>, в части Where запроса. И вот результат:

Прототипы операции Field

Одним из дополнительных преимуществ операции Field<T> является ее способность справляться с ситуацией, когда поля содержат значение null. Взглянем на пример ниже, где имя студента имеет значение null, но операция Field<T> не используется:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name=null},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=16, Name="Андрей Мухамедшин"}
                                 };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

            try
            {
                string name = seq1.Where(student => student.Field<int>("Id") == 5)
                                  .Select(student => (string)student["Name"])
                                  .Single();
                Console.WriteLine("Имя студента '{0}'", name);
            }
            catch (Exception ex)
            {
                Console.Write("\nВыдано исключение: ");
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write(ex.Message);
            }

Это чрезвычайно простой пример. Обратите внимание, что значение члена Name записи Student с Id, равным 7, устанавливается в null. Кроме того, вместо использования операции Field<T> производится обращение по индексу к значению внутри DataRow и приведение его к типу string. Посмотрим на результат:

Пример без операции Field, когда присутствует значение null

Так что же произошло? А случилось то, что значение объекта DataColumn равно DBNull, и привести его к string нельзя. Существуют довольно громоздкие решения, которые позволили бы избежать этой сложности, но специально для упрощения предусмотрена операция Field<T>. Рассмотрим такой же пример, но на этот раз с применением операции Field<T> для получения значения объекта DataColumn:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name=null},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=16, Name="Андрей Мухамедшин"}
                                 };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

            
                string name = seq1.Where(student => student.Field<int>("Id") == 5)
                                  .Select(student => student.Field<string>("Name"))
                                  .Single();
                Console.WriteLine("Имя студента '{0}'", name);

Все то же самое, но теперь вместо приведения к string используется операция Field<T>. Посмотрим, что получилось:

Пример с операцией Field, когда присутствует значение null

С этим гораздо легче иметь дело.

SetField<T>

Как и в случае извлечения объектов DataColumn, значение null неблагоприятно влияет и на установку объектов DataColumn. Чтобы помочь справиться с этой проблемой, была создана операция SetField<T>. Она применяется в ситуации, когда устанавливается значение объекта DataColumn из допускающего null типа данных, текущее значение которого равно null.

Операция SetField<T> имеет три прототипа, описанные ниже:

Первый прототип SetField<T>

Первый прототип позволяет устанавливать текущее значение столбца для указанного объекта DataColumn.

public static void SetField ( 
      this DataRow first,
      System.Data.DataColumn column,
      T value);
Второй прототип SetField<T>

Второй прототип позволяет устанавливать текущее значение столбца с указанным именем.

public static void SetField ( 
         this DataRow first, 
         string columnName, 
         T value);
Третий прототип SetField<T>

Третий прототип позволяет вам устанавливать текущее значение столбца с указанным порядковым номером.

public static void SetField ( 
     this DataRow first, 
     int ordinal, 
     T value);

В примере применения операции SetField<T>, приведенном ниже, сначала отображается последовательность объектов DataRow, содержащих сведения о студентах. Затем из этой последовательности запрашивается один из студентов по имени, после чего имя изменяется с использованием операции SetField<T>. После проведенных изменений последовательность объектов DataRow снова отображается. Далее все повторяется с каждым прототипом:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name="Елена Волкова"},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=16, Name="Андрей Мухамедшин"}
                                 };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

            Console.WriteLine("{0}Результаты перед вызовом всех прототипов:",
              System.Environment.NewLine);

            foreach (DataRow dataRow in seq1)
            {
                Console.WriteLine("Студент с Id = {0} is {1}", dataRow.Field<int>("Id"),
                  dataRow.Field<string>("Name"));
            }

            //  Prototype 1.
            (from s in seq1
             where s.Field<string>("Name") == "Александр Ерохин"
             select s).Single<DataRow>().SetField(dt1.Columns[1], "Дмитрий Петров");

            Console.WriteLine("{0}Результаты после вызова прототипа 1:",
              System.Environment.NewLine);

            foreach (DataRow dataRow in seq1)
            {
                Console.WriteLine("Студент с Id = {0} is {1}", dataRow.Field<int>("Id"),
                  dataRow.Field<string>("Name"));
            }

            //  Prototype 2.
            (from s in seq1
             where s.Field<string>("Name") == "Дмитрий Петров"
             select s).Single<DataRow>().SetField("Name", "Иван Сидоров");

            Console.WriteLine("{0}Результаты после вызова прототипа 2:",
                System.Environment.NewLine);

            foreach (DataRow dataRow in seq1)
            {
                Console.WriteLine("Студент с Id = {0} is {1}", dataRow.Field<int>("Id"),
                  dataRow.Field<string>("Name"));
            }

            //  Prototype 3.
            (from s in seq1
             where s.Field<string>("Name") == "Иван Сидоров"
             select s).Single<DataRow>().SetField("Name", "Василий Тарасов");

            Console.WriteLine("{0}Результаты после вызова прототипа 3:",
              System.Environment.NewLine);

            foreach (DataRow dataRow in seq1)
            {
                Console.WriteLine("Студент с Id = {0} is {1}", dataRow.Field<int>("Id"),
                  dataRow.Field<string>("Name"));
            }

Этот код не так плох, как может показаться. После получения последовательности студентов и ее отображения на консоли, идет блок кода, повторяющийся три раза — по одному для каждого прототипа. Каждый блок содержит запрос LINQ, извлекающий поле и обновляющий его значение, отображает строку заголовка на консоли, а затем — каждую строку в последовательности, чтобы показать изменение, проведенное в поле.

В этом примере следует отметить две вещи. В каждом запросе LINQ, где запрашивается DataRow по полю Name, синтаксис выражений запросов смешивается со стандартным синтаксисом точечной нотации.

Также используется операция Field<T> для нахождения записи, которая должна быть установлена операцией SetField<T>. После получения последовательности объектов DataRow студентов с ними производится работа с использованием всех прототипов SetField<T>. На протяжении примера ранее измененный элемент запрашивается по значению и снова изменяется.

После обновления значения каждого элемента последовательность выводится на консоль для подтверждения, что значение этого элемента действительно было обновлено.

Одна из изюминок этого примера состоит в том, что запрос элемента и обновление его значения производится в единственном операторе. Это настолько мощно, что может показаться какой-то иллюзией, но как будет показано далее, никакой магии здесь нет. Ниже приведен вывод этого примера:

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