Метод SubmitChanges()
66LINQ --- LINQ to DataSet и SQL --- Метод SubmitChanges()
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
Объект DataContext будет кэшировать все изменения, проведенные в сущностных объектах, до тех пор, пока не будет вызван метод SubmitChanges. Метод SubmitChanges инициирует запуск процесса изменений, и эти изменения в сущностных объектах будут сохранены в базе данных.
Если объекту DataContext недоступна объемлющая транзакция, которую можно было бы задействовать при вызове метода SubmitChanges, транзакция будет создана, и все изменения будут проведены в ее рамках. В этом случае, если транзакция потерпит неудачу, все ее изменения в базе данных будут отменены.
В случае возникновения конфликтов параллельного доступа будет сгенерировано исключение ChangeConflictException, что даст возможность попытаться решить конфликты и повторить попытку фиксации изменений. И что действительно хорошо — DataContext содержит коллекцию ChangeConflicts, которая предоставляет метод ResolveAll, чтобы выполнить такое разрешение конфликтов автоматически.
Метод SubmitChanges имеет два прототипа, которые описаны ниже:
- Первый прототип SubmitChanges
-
void SubmitChanges()
Этот прототип метода не принимает аргументов и по умолчанию устанавливает значение ConflictMode в FailOnFirstConflict.
- Второй прототип SubmitChanges
-
void SubmitChanges(ConflictMode failureMode)
Этот прототип метода позволяет указывать ConflictMode. Его возможные значения: FailOnFirstConflict и ContinueOnConflict. Режим ConflictMode.FailOnFirstConflict вынуждает метод SubmitChanges генерировать исключение ChangeConflictException при самом первом обнаруженном конфликте. Режим ConflictMode.ContinueOnConflict пытается провести все обновления базы данных, чтобы, когда сгенерировано исключение ChangeConflictException, можно было получить отчет о них всех и разрешить за один прием.
Конфликты оцениваются в терминах количества конфликтующих записей, а не количества конфликтующих полей. Можно иметь два поля из одной записи, приведших к конфликту, но это вызовет только один конфликт.
Поскольку во многих примерах вызывался метод SubmitChanges, тривиальный пример этого метода, вероятно, покажется старым знакомым. Вместо еще одного базового примера вызова метода SubmitChanges для простого сохранения изменений в базе данных предлагается рассмотреть несколько более сложный пример.
В примере вызова первого прототипа SubmitChanges будет сначала доказано, что изменения не проводятся в базе данных до тех пор, пока не будет вызван метод SubmitChanges. Поскольку этот пример сложнее многих предыдущих, он сопровождается дополнительными пояснениями:
System.Data.SqlClient.SqlConnection sqlConn =
new System.Data.SqlClient.SqlConnection(
@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;Initial Catalog=C:\NORTHWIND.MDF;Integrated Security=True");
try
{
sqlConn.Open();
string sqlQuery = "select ContactTitle from Customers where CustomerID = 'LAZYK'";
string originalTitle = GetStringFromDb(sqlConn, sqlQuery);
string title = originalTitle;
Console.WriteLine("Заголовок из базы данных: {0}", title);
Northwind db = new Northwind(sqlConn);
Customer c = (from cust in db.Customers
where cust.CustomerID == "LAZYK"
select cust).
Single<Customer>();
Console.WriteLine("Заголовок из сущностного объекта: {0}", c.ContactTitle);
...
В приведенном коде создается и открывается подключение к базе данных ADO.NET. Затем в базе ContactTitle запрашивается заказчик LAZYK, используя общий метод из исходного кода GetStringFromDb, а результат выводится на консоль. Далее с использованием подключения к базе данных ADO.NET создается объект Northwind, запрашивается тот же самый заказчик с помощью LINQ to SQL и отображается информация ContactTitle. В этой точке два полученных ContactTitle должны совпадать.
...
Console.WriteLine(String.Format(
"\nЗаменить заголовок на 'Director of Marketing' в сущностном объекте..."));
c.ContactTitle = "Director of Marketing";
title = GetStringFromDb(sqlConn, sqlQuery);
Console.WriteLine("Загаловок из базы данных: {0}", title);
Customer c2 = (from cust in db.Customers
where cust.CustomerID == "LAZYK"
select cust).
Single<Customer>();
Console.WriteLine("Заголовок из сущностного объекта: {0}", c2.ContactTitle);
...
В этом фрагменте кода значение ContactTitle сущностного объекта LINQ to SQL, представляющего заказчика, изменяется. Затем значения ContactTitle запрашиваются из базы данных и из сущностного объекта, после чего отображаются на консоли. На этот раз значения ContactTitle не должны совпадать, потому что изменения еще не были сохранены в базе данных.
...
db.SubmitChanges();
Console.WriteLine(String.Format(
"\nВызван метод SubmitChanges()."));
title = GetStringFromDb(sqlConn, sqlQuery);
Console.WriteLine("Теперь заголовок из базы данных: {0}", title);
// Восстанвление исходного значения
c.ContactTitle = "Marketing Manager";
db.SubmitChanges();
}
finally
{
sqlConn.Close();
}
В предыдущем коде вызывается метод SubmitChanges и вновь извлекается ContactTitle, чтобы отобразить его снова. На этот раз значение в базе данных должно быть обновлено, потому что метод SubmitChanges сохранил изменения в базе.
И, наконец, ContactTitle устанавливается обратно в исходное значение и сохраняется в базе данных с использованием метода SubmitChanges. Это позволит вернуть базу данных в исходное состояние, чтобы пример можно было запускать несколько раз и это никак не повлияло на остальные примеры.
Этот код много чего делает, но его цель — доказать, что изменения, проведенные в сущностном объекте, не сохраняются в базе данных до тех пор, пока не будет вызван метод SubmitChanges. Когда вы видите вызов метода GetStringFromDb, это значит, что он извлекает ContactTitle непосредственно из базы данных с использованием ADO.NET. Ниже показаны результаты выполнения кода:
Как видите, значение ContactTitle не изменялось в базе данных до тех пор, пока не был вызван метод SubmitChanges.
В примере применения второго прототипа SubmitChanges намеренно вызываются ошибки параллельного доступа к двум записям за счет их обновления через ADO.NET между моментом запроса записей и моментом их обновления с помощью LINQ to SQL. Будут созданы две конфликтующих записи, чтобы продемонстрировать разницу между ConflictMode.FailOnFirstConflict и ConflictMode.ContinueOnConflict.
Также далее будет показан код, который вернет в базе данных значения ContactTitle в исходное состояние. Это позволит запускать пример многократно. Если вы прекратите выполнение кода в отладчике, то может понадобиться вручную сбросить эти значения.
В первом примере второго прототипа метода SubmitChanges для ConflictMode указывается ContinueOnConflict, чтобы сначала можно было увидеть, как он обрабатывает множественные конфликты. Поскольку пример довольно сложен, он сопровождается дополнительными пояснениями:
// Используйте свое подключение
Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
Initial Catalog=C:\NORTHWIND.MDF;
Integrated Security=True");
Console.WriteLine("Запрос заказчика LAZYK с помощью LINQ.");
Customer cust1 = (from c in db.Customers
where c.CustomerID == "LAZYK"
select c).Single<Customer>();
Console.WriteLine("Запрос заказчика LONEP с помощью LINQ.");
Customer cust2 = (from c in db.Customers
where c.CustomerID == "LONEP"
select c).Single<Customer>();
В этом коде создается объект Northwind типа DataContext и запрашиваются два заказчика - LAZYK и LONEP.
string cmd = @"update Customers set ContactTitle = 'Director of Marketing'
where CustomerID = 'LAZYK';
update Customers set ContactTitle = 'Director of Sales'
where CustomerID = 'LONEP'";
ExecuteStatementInDb(cmd);
Далее для обоих заказчиков обновляется значение ContactValue в базе данных с помощью общего метода ExecuteStatementInDb, который использует ADO.NET для проведения изменений. В этой точке создается потенциальная возможность конфликта параллельного доступа для каждой записи.
Console.WriteLine("Изменение ContactTitle в сущностных объектах для LAZYK и LONEP.");
cust1.ContactTitle = "Vice President of Marketing";
cust2.ContactTitle = "Vice President of Sales";
try
{
Console.WriteLine("Вызов SubmitChanges() ...");
db.SubmitChanges(ConflictMode.ContinueOnConflict);
Console.WriteLine("Метод SubmitChanges() вызван успешно.");
}
...
В этой части кода обновляется ContactTitle для каждого заказчика, так что при вызове метода SubmitChanges в следующей части процессор изменений объекта DataContext попытается сохранить изменения для этих двух заказчиков и обнаружить конфликты параллельного доступа.
Затем вызьшается метод SubmitChanges. Это заставит процессор изменений объекта DataContext попытаться сохранить этих двух заказчиков, но поскольку значение каждого ContactTitle каждого заказчика в базе данных отличается от того, что было изначально из нее загружено, будет обнаружен конфликт параллельного доступа.
catch (ChangeConflictException ex)
{
Console.WriteLine("Во время вызова SubmitChanges() обнаружен(ы) конфликт(ы): {0}",
ex.Message);
foreach (ObjectChangeConflict objectConflict in db.ChangeConflicts)
{
Console.WriteLine("Возник конфликт для {0}.",
((Customer)objectConflict.Object).CustomerID);
foreach (MemberChangeConflict memberConflict in objectConflict.MemberConflicts)
{
Console.WriteLine(" Значение LINQ = {0}\n Значение базы данных = {1}",
memberConflict.CurrentValue,
memberConflict.DatabaseValue);
}
}
}
...
В этом фрагменте перехватывается исключение ChangeConflictException. Именно здесь начинается самое интересное. Обратите внимание, что сначала выполняется перечисление коллекции ChangeConflicts объекта DataContext по имени db. Эта коллекция будет хранить объекты ObjectChangeConflict. Объекты ObjectChangeConflict имеют свойство по имени Object, ссылающееся на действительный сущностный объект, при сохранении которого случается конфликт параллельного доступа. В коде просто выполняется приведение члена Object к типу данных сущностного класса, чтобы обратиться к значениям свойств сущностного объекта. В данном случае производится обращение к свойству CustomerID.
Затем для каждого объекта ObjectChangeConflict производится перечисление коллекции объектов MemberChangeConflict с отображением информации для каждого из тех, которые интересуют. В данном случае на консоль выводится значение LINQ значение из базы данных.
...
Console.WriteLine("\nСброс данных к исходным значениям.");
cmd = @"update Customers set ContactTitle = 'Marketing Manager'
where CustomerID = 'LAZYK';
update Customers set ContactTitle = 'Sales Manager'
where CustomerID = 'LONEP'";
ExecuteStatementInDb(cmd);
В этом фрагменте кода база данных просто восстанавливается в ее исходное состояние, чтобы пример можно было запускать много раз.
Был приведен довольно большой объем кода. Имейте в виду, что ни одно из этих перечислений по различным коллекциям конфликтов не является необходимым. Здесь просто показано, как это можно делать, и предоставлена доступная информация о конфликтах, которую можно видеть.
Также обратите внимание, что в этом примере ничего не делается в плане разрешения конфликтов. О них просто сообщается.
Ниже показан результат выполнения этого кода:
Как видите, было обнаружено два конфликта — по одному для каждой из двух записей, для которых был создан конфликт. Это демонстрирует, что процессор изменений не прекратил попыток сохранить изменения в базе данных после того, как произошел первый конфликт. Это связано с тем, что при вызове SubmitChanges было передано значение ConflictMode, равное ContinueOnConflict.
Ниже содержится такой же код, за исключением того, что в нем при вызове SubmitChanges передается значение ConflictMode, равное FailOnFirstConflict:
...
try
{
Console.WriteLine("Вызов SubmitChanges() ...");
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
Console.WriteLine("Метод SubmitChanges() вызван успешно.");
}
...
На этот раз результат должен указывать на то, что обработка изменений в сущностных объектах останавливается, как только обнаружен первый конфликт параллельного доступа. Взглянем на результат:
Как видите, даже несмотря на то, что было инициировано два конфликта, процессор изменений прекращает попытки сохранить изменения в базе данных, как только возникает конфликт, о чем свидетельствует единственное сообщение о конфликте.