При изучении LINQ меня почти поразила трудность — операция обновления базы данных, которую вы видите в заголовке. Теперь я шаг за шагом проведу вас в эту трясину. Пожалуйста, приготовьте кирпичи и слюну, следуйте за мной.
Начнем с самого простого случая. В качестве примера возьмем базу данных Northwind. Когда вам нужно изменить ProductName продукта, вы можете напрямую написать на клиенте следующий код:
// Список 0NorthwindDataContext db = новый NorthwindDataContext();
Продукт продукта = db.Products.Single(p => p.ProductID == 1);
Product.ProductName = "Чай изменен";
БД.SubmitChanges();
Проверьте это, и обновление пройдет успешно. Однако я считаю, что такой код не появится в ваших проектах, потому что его просто невозможно повторно использовать. Хорошо, давайте проведем рефакторинг и выделим его в метод. Какие должны быть параметры? — это новое имя продукта и идентификатор продукта, который необходимо обновить. Ну, похоже, это так.
public void UpdateProduct (int id, строка ProductName)
{
NorthwindDataContext db = новый NorthwindDataContext ();
Продукт продукта = db.Products.Single(p => p.ProductID == id);
Product.ProductName = ProductName;
БД.SubmitChanges();
}В реальных проектах мы не можем просто изменить название продукта. Другие поля Продукта также могут быть изменены. Тогда подпись метода UpdateProduct станет следующей:
public void UpdateProduct(int id,
строка ProductName,
интервал поставщика,
int категорияId,
строковое количество PerUnit,
десятичная единица Цена,
короткие единицыНа складе,
короткие единицыOnOrder,
короткий reorderLevel) Конечно, это всего лишь простая база данных. В реальных проектах нередко бывает двадцать, тридцать или даже сотни полей. Кто может терпеть такие методы? Если вы так напишете, что будет делать объект Product?
Правильно, используйте Product в качестве параметра метода и кидайте надоедливую операцию присваивания в клиентский код. При этом мы извлекли код получения экземпляра Product для формирования метода GetProduct, а методы, связанные с операциями с базой данных, поместили в класс ProductRepository, который конкретно отвечает за работу с базой данных. Ах да, СРП!
// Список 1
//Репозиторий продуктов
общедоступный продукт GetProduct (int id)
{
NorthwindDataContext db = новый NorthwindDataContext ();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct (продукт продукта)
{
NorthwindDataContext db = новый NorthwindDataContext ();
db.Products.Attach(продукт);
БД.SubmitChanges();
}
//Код клиента
Репозиторий ProductRepository = новый ProductRepository();
Продукт продукта = репозиторий.GetProduct(1);
Product.ProductName = "Чай изменен";
репозиторий.UpdateProduct(продукт);
Здесь я использую метод Attach, чтобы прикрепить экземпляр Product к другому DataContext. Для базы данных Northwind по умолчанию результатом этого является следующее исключение:
// Исключение 1 NotSupportException:
Попытка присоединения или добавления объекта. Объект не является новым и, возможно, был загружен из другого контекста данных. Эта операция не поддерживается.
Была предпринята попытка прикрепить или добавить объект, который не является новым,
Возможно, он был загружен из другого DataContext. Это не поддерживается. Глядя на MSDN, мы знаем, что при сериализации объектов клиенту эти объекты отделяются от исходного DataContext. DataContext больше не отслеживает изменения этих сущностей или их связей с другими объектами. Если вы хотите обновить или удалить данные в это время, вы должны использовать метод Attach, чтобы прикрепить сущность к новому DataContext перед вызовом SubmitChanges, в противном случае будет выдано вышеуказанное исключение.
В базе данных Northwind класс Product содержит три связанных класса (т. е. ассоциации внешних ключей): Order_Detail, Категория и Поставщик. В приведенном выше примере, хотя мы прикрепляем продукт, с ним не связан класс Attach, поэтому генерируется исключение NotSupportException.
Итак, как связать классы, связанные с продуктом? Это может показаться сложным даже для такой простой базы данных, как Northwind. Кажется, что мы должны сначала получить исходные классы Order_Detail, Category и Поставщик, относящиеся к исходному Продукту, а затем Присоединить их к текущему DataContext соответственно, но на самом деле, даже если мы это сделаем, будет выброшено исключение NotSupportException.
Итак, как реализовать операцию обновления? Для простоты мы удаляем другие классы сущностей в Northwind.dbml и оставляем только Product. Таким образом, мы можем начать анализ с самого простого случая.
После удаления других классов из-за проблем мы снова выполнили код из списка 1, но база данных не изменила название продукта. Посмотрев на перегруженную версию метода Attach, мы легко сможем обнаружить проблему.
Метод Attach(entity) по умолчанию вызывает перегрузку Attach(entity, false), которая прикрепляет соответствующую сущность в неизмененном состоянии. Если объект Product не был изменен, нам следует вызвать эту перегруженную версию, чтобы прикрепить объект Product к DataContext в неизмененном состоянии для последующих операций. В настоящее время статус объекта Product «изменён», и мы можем вызвать только метод Attach(entity, true).
Итак, мы изменили соответствующий код в списке 1 на Attach(product, true) и посмотрим, что произошло?
// Исключение 2 InvalidOperationException:
Если сущность объявляет член версии или не имеет политики проверки обновлений, ее можно присоединить только как измененную сущность без исходного состояния.
Объект можно прикрепить только как измененный без исходного состояния.
если он объявляет член версии или не имеет политики проверки обновлений.
LINQ to SQL использует столбец RowVersion для реализации проверки оптимистичного параллелизма по умолчанию, в противном случае описанная выше ошибка возникнет при присоединении сущностей к DataContext в измененном состоянии. Существует два способа реализации столбца RowVersion. Один — определить столбец типа метки времени для таблицы базы данных, а другой — определить атрибут IsVersion=true в атрибуте сущности, соответствующем первичному ключу таблицы. Обратите внимание, что вы не можете использовать столбец TimeStamp и атрибут IsVersion=true одновременно, иначе будет выдано исключение InvalidOprationException: оба элемента «System.Data.Linq.Binary TimeStamp» и «Int32 ProductID» помечены как версии строки. В этой статье мы используем столбец timestamp в качестве примера.
После создания столбца с именем TimeStamp и ввода отметки времени для таблицы Products перетащите его обратно в конструктор, а затем выполните код из списка 1. Слава Богу, наконец-то это сработало.
Теперь перетаскиваем таблицу «Категории» в дизайнер. На этот раз я усвоил урок и сначала добавил столбец временной метки в таблицу «Категории». После тестирования выяснилось, что это снова ошибка в Исключении 1! После удаления столбца временной метки категорий проблема остается. Боже мой, что именно делается в ужасном методе Attach?
Да, кстати, есть перегруженная версия метода Attach, давайте попробуем.
public void UpdateProduct (продукт продукта)
{
NorthwindDataContext db = новый NorthwindDataContext();
Product oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(продукт, старыйПродукт);
БД.SubmitChanges();
} Или ошибка исключения 1!
Я упаду! Прикрепи, Прикрепи, что с тобой случилось?
Чтобы изучить исходный код LINQ to SQL, мы используем подключаемый модуль FileDisassembler для Reflector, который декомпилирует System.Data.Linq.dll в код cs и генерирует файлы проекта, что помогает нам найти его в Visual Studio.
Когда выдается исключение 1?
Сначала мы находим информацию, описанную в Исключении 1, из System.Data.Linq.resx и получаем ключ «CannotAttachAddNonNewEntities», затем находим метод System.Data.Linq.Error.CannotAttachAddNonNewEntities(), находим все ссылки на этот метод и находим их два. Этот метод используется в трех местах, а именно в методе StandardChangeTracker.Track и методе InitializeDeferredLoader.
Мы снова открываем код Table.Attach(entity, bool) и, как и ожидалось, обнаруживаем, что он вызывает метод StandardChangeTracker.Track (то же самое справедливо и для метода Attach(entity,entity)):
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); В методе Track следующий код выдает исключение 1:
если (trackedObject.HasDeferredLoaders)
{
throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Итак, мы обращаем внимание на свойство StandardTrackedObject.HasDeferredLoaders:
внутреннее переопределение bool HasDeferredLoaders
{
получать
{
foreach (ассоциация MetaAssociation в this.Type.Associations)
{
если (this.HasDeferredLoader(association.ThisMember))
{
вернуть истину;
}
}
foreach (член MetaDataMember из p в this.Type.PersistentDataMembers
где p.IsDeferred && !p.IsAssociation
выберите п)
{
если (this.HasDeferredLoader(член))
{
вернуть истину;
}
}
вернуть ложь;
}
} Из этого можно примерно сделать вывод, что пока в объекте есть лениво загруженные элементы, операция Attach выдаст Исключение 1. Это точно соответствует сценарию, в котором возникает Исключение 1 — класс Product содержит лениво загружаемые элементы.
Тогда появился способ избежать этого исключения — удалить элементы, загрузку которых в Продукте необходимо отложить. Как его удалить? Вы можете использовать DataLoadOptions для немедленной загрузки или установить для элементов, требующих отложенной загрузки, значение NULL. Но первый способ не сработал, поэтому пришлось воспользоваться вторым.
// Список 2
класс ProductRepository
{
общедоступный продукт GetProduct (int id)
{
NorthwindDataContext db = новый NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
общедоступный продукт GetProductNoDeffered (int id)
{
NorthwindDataContext db = новый NorthwindDataContext();
//Параметры DataLoadOptions = новые DataLoadOptions();
//options.LoadWith<Product>(p => p.Category);
//db.LoadOptions = параметры;
var product = db.Products.SingleOrDefault(p => p.ProductID == id);
продукт.Категория = ноль;
возврат товара;
}
public void UpdateProduct (продукт продукта)
{
NorthwindDataContext db = новый NorthwindDataContext ();
db.Products.Attach(продукт, правда);
БД.SubmitChanges();
}
}
//Код клиента
Репозиторий ProductRepository = новый ProductRepository();
Продукт продукта = репозиторий.GetProductNoDeffered(1);
Product.ProductName = "Чай изменен";
репозиторий.UpdateProduct(продукт);
Когда выдается исключение 2?
Следуя методу из предыдущего раздела, мы быстро нашли код, вызвавший исключение 2. К счастью, во всем проекте было только оно:
if (asModified && ((inheritanceType.VersionMember == null) && HeritageType.HasUpdateCheck))
{
throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Как видите, если второй параметр asModified в Attach имеет значение true, не содержит столбца RowVersion (VersionMember=null) и содержит столбец проверки обновлений (HasUpdateCheck), будет выдано исключение 2. Код HasUpdateCheck выглядит следующим образом:
публичное переопределение bool HasUpdateCheck
{
получать
{
foreach (член MetaDataMember в this.PersistentDataMembers)
{
если (member.UpdateCheck != UpdateCheck.Never)
{
вернуть истину;
}
}
вернуть ложь;
}
}Это также соответствует нашему сценарию — в таблице Products нет столбца RowVersion, а в коде, автоматически сгенерированном дизайнером, свойства UpdateCheck всех полей имеют значение по умолчанию Always, то есть свойство HasUpdateCheck имеет значение true.
Способ избежать Исключения 2 еще проще: добавьте столбец TimeStamp во все таблицы или установите поле IsVersion=true в полях первичного ключа всех таблиц. Поскольку последний метод изменяет автоматически сгенерированные классы и может быть перезаписан новыми проектами в любое время, я рекомендую использовать первый метод.
Как использовать метод Attach?
После приведенного выше анализа мы можем выяснить два условия, связанные с методом Attach: существует ли столбец RowVersion и существует ли ассоциация внешнего ключа (то есть элементы, которые необходимо отложенно загружать). Эти два условия и использование нескольких перегрузок Attach я свел в таблицу. Глядя на таблицу ниже, вам нужно быть полностью морально готовым.
серийный номер
Прикрепить метод
Имеет ли столбец RowVersion связанное описание.
1 Присоединить(объект) Нет Нет Без изменений
2 Attach(entity) Нет Да NotSupportException: предпринята попытка присоединения или добавления объекта. Объект не является новым объектом и может быть загружен из других контекстов данных. Эта операция не поддерживается.
3 Attach(entity) Есть ли изменения
4 Attach(объект) не изменяется. То же, что и 2, если в подмножестве нет столбца RowVersion.
5 Attach(entity, true) Нет Нет InvalidOperationException: Если объект объявляет член версии или не имеет политики проверки обновлений, его можно присоединить только как измененный объект без исходного состояния.
6 Attach(entity, true) Нет Да NotSupportException: предпринята попытка присоединения или добавления объекта. Объект не является новым объектом и может быть загружен из других контекстов данных. Эта операция не поддерживается.
7 Attach(entity, true) Является ли изменение нормальным (принудительное изменение столбца RowVersion сообщит об ошибке)
8 Attach(entity, true) Да NotSupportException: предпринята попытка присоединения или добавления объекта. Объект не является новым объектом и может быть загружен из других контекстов данных. Эта операция не поддерживается.
9 Attach(entity,entity) Нет Нет DuplateKeyException: невозможно добавить объект, ключ которого уже используется.
10 Attach(entity,entity) Нет Да NotSupportException: предпринята попытка присоединения или добавления объекта. Объект не является новым объектом и может быть загружен из других контекстов данных. Эта операция не поддерживается.
11 Attach(entity,entity) DuplateKeyException: невозможно добавить объект, ключ которого уже используется.
12 Attach(entity,entity) Да NotSupportException: предпринята попытка присоединения или добавления объекта. Объект не является новым объектом и может быть загружен из других контекстов данных. Эта операция не поддерживается.
Вложение может быть нормально обновлено только в 7-й ситуации (включая столбец RowVersion и отсутствие ассоциации внешнего ключа)! Такая ситуация практически невозможна для системы, основанной на базе данных! Что это за API?
Резюме Давайте успокоимся и начнем подводить итоги.
Если вы напишете код LINQ to SQL непосредственно в пользовательском интерфейсе, как в списке 0, ничего страшного не произойдет. Но если вы попытаетесь абстрагировать отдельный уровень доступа к данным, произойдет катастрофа. Означает ли это, что LINQ to SQL не подходит для разработки многоуровневой архитектуры? Многие говорят, что LINQ to SQL подходит для разработки небольших систем, но небольшой размер не означает, что она не многоуровневая. Есть ли способ избежать такого количества исключений?
Эта статья действительно дала некоторые подсказки. В следующем эссе этой серии я постараюсь предложить несколько решений, из которых каждый сможет выбрать.