Операции DataTable

61

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

В дополнение к ориентированным на DataRow операциям в классе DataRowExtensions предусмотрены также некоторые операции, специфичные для DataTable. Эти операции определены в статическом классе System.Data.DataTableExtensions внутри сборки System.Data.Entity.dll.

AsEnumerable

Возможно, вы удивитесь, встретив здесь операцию AsEnumerable. Эта операция специально предназначена для класса DataTable и возвращает последовательность объектов DataRow. Хотя она не упоминалась до сих пор, но эта операция вызывалась почти в каждом примере.

Если вы посмотрите на статический класс System.Data.DataTableExtensions, то увидите, что там есть операция AsEnumerable. Назначение этой операции — возвращать последовательность типа IEnumerable<DataRow> из объекта DataTable.

Операция AsEnumerable имеет один прототип, описанный ниже:

public static IEnumerable<DataRow> AsEnumerable ( 
        this DataTable source);

Эта операция, будучи вызванной на объекте DataTable, возвращает последовательность объектов DataRow. Обычно так выглядит первый шаг при выполнении запроса LINQ to DataSet на DataTable объекта DataSet. За счет вызова этой операции можно получить последовательность IEnumerable<T>, где T является DataRow, что позволяет вызывать множество операций LINQ, которые могут быть вызваны на последовательности типа IEnumerable<T>.

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

CopyToDataTable<DataRow>

Теперь, когда известно, как запрашивать и модифицировать значение DataColumn объекта DataRow, может возникнуть вопрос, а как поместить эту последовательность модифицированных объектов DataRow в DataTable? Операция CopyToDataTable предназначена именно для этой цели.

Операция CopyToDataTable имеет два прототипа, которые описаны ниже:

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

Первый прототип вызывается на IEnumerable<DataRow> и возвращает DataTable. Он используется для создания нового объекта DataTable из последовательности объектов DataRow. Первый прототип автоматически устанавливает исходные версии для каждого поля, не требуя явного вызова метода AcceptChanges.

public static DataTable CopyToDataTable<T> (
     this IEnumerable<T> source) where T : DataRow;
Второй прототип CopyToDataTable

Второй прототип вызывается на IEnumerable<DataRow> исходного DataTable, чтобы обновить уже существующий целевой объект DataTable, на основе указанного значения LoadOptions.

public static void CopyToDataTable<T> ( 
       this IEnumerable<T> source, 
       DataTable table, 
       LoadOption options
) where T : DataRow;

Значение переданного аргумента LoadOptions информирует операцию о том, что должны быть изменены только исходные значения столбцов, текущие значения либо те и другие. Это полезно при управлении изменениями DataTable. Для LoadOption доступны описанные ниже значения:

  • OverwriteChanges: в каждом столбце будут обновлены и исходное значение и текущее.

  • PreserveChanges: обновляется только исходное значение каждого столбца.

  • Upsert: обновляется только текущее значение каждого столбца.

Однако этот аргумент LoadOption создает небольшую проблему. Обратите внимание, что описание каждого возможного значения ссылается на обновление значений столбца. Это, конечно же, означает также обновление столбцов записи, уже находящейся в целевом DataTable. Как операция CopyToDataTable узнает, какая именно запись в целевом DataTable соответствует записи в исходном DataTable?

Другими словами, когда операция CopyToDataTable пытается копировать запись из исходного DataTable в целевой и получает параметр LoadOption, каким образом она узнает, следует просто добавить запись из исходного DataTable или же обновить уже существующую запись целевого DataTable? Ответ — никак, если только ей ничего не известно о полях первичного ключа в DataTable.

Поэтому для правильной работы этого прототипа операции CopyDataTable целевой объект DataTable должен иметь соответствующую информацию о полях первичного ключа. Если не указать первичный ключ, этот прототип добавит все записи из исходного объекта DataTable в целевой объект DataTable.

Здесь есть одна дополнительная сложность. Поскольку, используя этот прототип, вы, возможно, заинтересованы в сравнении исходной и текущей версий значений полей, не забудьте, что с этим прототипом операции CopyDataTable поле не имеет исходной версии, если только не был вызван метод AcceptChanges. Попытка обратиться к исходной версии, когда она не существует, приведет к генерации исключения. Тем не менее, перед попыткой получить доступ к исходной версии на каждом объекте DataRow можно вызвать метод HasVersion, чтобы узнать, существует ли она, и предотвратить генерацию исключения этого типа.

В примере применения первого прототипа операции CopyToDataTable сначала модифицируется поле в DataTable, с помощью операции CopyToDataTable создается новый объект DataTable из модифицированного, после чего содержимое нового объекта DataTable отображается на консоль:

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("Исходный объект DataTable: \n");
            foreach (DataRow dr in seq1)
            {
                Console.WriteLine("Студент с Id = {0} - {1}", dr.Field<int>("Id"),
                    dr.Field<string>("Name"));
            }

            // Вносим изменения
            (from s in seq1
             where s.Field<string>("Name") == "Елена Волкова"
             select s).Single<DataRow>().SetField("Name", "Ольга Ивина");

            Console.WriteLine("\nНовый объект DataTable: \n");
            DataTable newTable = seq1.CopyToDataTable();
            foreach (DataRow dr in newTable.AsEnumerable())
            {
                Console.WriteLine("Студент с Id = {0} - {1}", dr.Field<int>("Id"),
                    dr.Field<string>("Name"));
            }

Итак, сначала создается объект DataTable из массива студентов, как обычно делалось в предыдущих примерах. Затем содержимое DataTable отображается на консоль. После этого модифицируется поле Name одного из объектов DataRow. Затем вызовом операции CopyToDataTable создается новый объект DataTable. И, наконец, содержимое вновь созданного объекта DataTable отображается на консоль. Ниже показаны результаты:

Вызов первого прототипа операции CopyToDataTable

Как видите, в новом объекте DataTable имеются данные в модифицированной версии, чего и следовало ожидать.

В следующем примере демонстрируется применение второго прототипа операции CopyToDataTable. Как упоминалось ранее, для правильной работы аргумента LoadOption в целевом объекте DataSet должен быть определен первичный ключ. В данном примере они не устанавливаются, чтобы можно было увидеть поведение без ключей. Поскольку этот пример несколько сложнее, приводятся дополнительные пояснения:

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);
            DataTable newTable = dt1.AsEnumerable().CopyToDataTable();

Пока ничего нового. Из массива students создан исходный объект DataTable. Целевой объект DataTable будет создан вызовом операции CopyToDataTable на исходном DataTable. Обратите внимание, что поскольку вызван первый прототип операции CopyToDataTable, незачем вызывать метод AcceptChanges на целевом DataTable. Это важно отметить, потому что в следующем сегменте кода производится ссылка на исходную версию поля Name. Если бы не тот факт, что первый прототип операции CopyToDataTable устанавливает исходные версии полей, в этой ситуации было бы сгенерировано исключение.

Console.WriteLine("Перед обновлением DataTable: ");
            foreach (DataRow dr in newTable.AsEnumerable())
            {
                Console.WriteLine("Студент с Id = {0} : исходное {1} : текущее {2}", dr.Field<int>("Id"),
                    dr.Field<string>("Name", DataRowVersion.Original)
                    ,dr.Field<string>("Name", DataRowVersion.Current));
            }

Здесь нет ничего особенного за исключением ссылки на исходную версию поля Name в записи и отсутствия генерации исключения при этом, поскольку данный прототип операции CopyToDataTable устанавливает исходную версию полей автоматически.

(from s in dt1.AsEnumerable()
             where s.Field<string>("Name") == "Елена Волкова"
             select s).Single<DataRow>().SetField("Name", "Alex Erohin");

            dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert);

Это важный сегмент кода в данном примере. Обратите внимание, что с использованием операции SetField<T> изменяется значение поля Name для одной записи в исходном объекте DataTable. Затем вызывается операция CopyToDataTable с указанием, что должно произойти копирование типа LoadOption.Upsert, имея в виду обновление только текущей версии. Это вызывает проблему. Из-за того, что вызван второй прототип операции CopyToDataTable, который не устанавливает исходные версии записей, вставляемых в базу данных, а также не вызван метод AcceptChanges, попытка обратиться к исходным версиям вставленных записей приводит к генерации исключения. Чтобы предотвратить это в случае вставки любой записи, понадобится применить метод HasVersion. Поскольку первичный ключ не указан, известно, что все записи исходной таблицы будут вставлены в целевую таблицу.

Console.WriteLine("\nПосле обновления DataTable: \n");
            foreach (DataRow dr in newTable.AsEnumerable())
            {
                Console.WriteLine("Студент с Id = {0} : исходное {1} : текущее {2}", dr.Field<int>("Id"),
                    dr.HasVersion(DataRowVersion.Original) ?
                    dr.Field<string>("Name", DataRowVersion.Original) : " не существует"
                    , dr.Field<string>("Name", DataRowVersion.Current));
            }

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

Также обратите внимание, что обращение к исходной версии данных поля происходит только если метод HasVersion возвращает true, указывая на наличие исходной версии. Вот результат:

Вызов второго прототипа операции CopyToDataTable

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

Может вызвать удивление, зачем возиться с вызовом метода HasVersion, если можно было просто вызвать метод AcceptChanges? Если это сделать, то все текущие значения полей станут исходными версиями значений, и невозможно будет узнать, какие записи были изменены. В рассматриваемых примерах нужно, чтобы при изменении записи значения исходной и текущей версий отличались.

Решение проблемы предыдущего примера заключается в указании первичного ключа целевого объекта DataTable. Ниже приведен пример, аналогичный предыдущему, но на этот раз первичный ключ установлен:

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);
            DataTable newTable = dt1.AsEnumerable().CopyToDataTable();
            newTable.PrimaryKey = new DataColumn[] { newTable.Columns[0] };

            Console.WriteLine("Перед обновлением DataTable: \n");
            foreach (DataRow dr in newTable.AsEnumerable())
            {
                Console.WriteLine("Студент с Id = {0} : исходное {1} : текущее {2}", dr.Field<int>("Id"),
                    dr.Field<string>("Name", DataRowVersion.Original)
                    ,dr.Field<string>("Name", DataRowVersion.Current));
            }

            (from s in dt1.AsEnumerable()
             where s.Field<string>("Name") == "Елена Волкова"
             select s).Single<DataRow>().SetField("Name", "Ольга Ивина");

            dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert);

            Console.WriteLine("\nПосле обновления DataTable: \n");
            foreach (DataRow dr in newTable.AsEnumerable())
            {
                Console.WriteLine("Студент с Id = {0} : исходное {1} : текущее {2}", dr.Field<int>("Id"),
                    dr.HasVersion(DataRowVersion.Original) ?
                    dr.Field<string>("Name", DataRowVersion.Original) : " не существует"
                    , dr.Field<string>("Name", DataRowVersion.Current));
            }

Единственное различие между этим примером и предыдущим в том, что добавлена строка, устанавливающая первичный ключ в DataTable по имени newTable. И вот результат:

 Вызов второго прототипа операции CopyToDataTable, когда первичный ключ установлен
Пройди тесты
Лучший чат для C# программистов