Как сохранить объект Linq, если у вас нет контекста данных?
У меня есть объект Linq, и я хочу внести в него изменения и сохранить его, например, так:
public void DoSomething(MyClass obj) {
obj.MyProperty = "Changed!";
MyDataContext dc = new MyDataContext();
dc.GetTable<MyClass>().Attach(dc, true); // throws exception
dc.SubmitChanges();
}
Исключение составляет:
System.InvalidOperationException: An entity can only be attached as modified without original state if it declares a version member or does not have an update check policy.
Похоже, у меня есть несколько вариантов:
- поместите член версии в каждый из моих классов и таблиц Linq (более 100), которые мне нужно использовать таким образом.
- найдите контекст данных, в котором изначально был создан объект, и используйте его для отправки изменений.
- реализовать OnLoaded в каждом классе и сохранить копию этого объекта, которую я могу передать Attach() в качестве базового объекта.
- К черту проверку параллелизма; загрузить версию БД непосредственно перед подключением и использовать ее в качестве базового объекта (НЕ!!!)
Вариант (2) кажется наиболее элегантным методом, особенно если я могу найти способ хранения ссылки на контекст данных при создании объекта. Но как?
Есть другие идеи?
РЕДАКТИРОВАТЬ
Я попытался последовать совету Джейсона Пуньона и создать поле параллелизма на столе в качестве контрольного примера. Я установил все нужные свойства (Time Stamp = true и т. Д.) Для поля в файле dbml, и теперь у меня есть поле параллелизма... и другая ошибка:
System.NotSupportedException: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported.
Так какого черта я должен присоединить, если не существующее существо? Если бы я хотел новую запись, я бы сделал InsertOnSubmit()! Так как же вы должны использовать Attach()?
Edit - полное раскрытие
Хорошо, я вижу, что пришло время полностью раскрыть, почему все стандартные шаблоны не работают для меня.
Я пытался быть умным и сделать свои интерфейсы намного чище, скрывая DataContext от "потребительских" разработчиков. Это я сделал, создав базовый класс
public class LinqedTable<T> where T : LinqedTable<T> {
...
}
... и каждая из моих таблиц имеет "другую половину" своей сгенерированной версии, объявленной так:
public partial class MyClass : LinqedTable<MyClass> {
}
Сейчас LinqedTable
имеет множество служебных методов, в частности такие вещи, как:
public static T Get(long ID) {
// code to load the record with the given ID
// so you can write things like:
// MyClass obj = MyClass.Get(myID);
// instead of:
// MyClass obj = myDataContext.GetTable<MyClass>().Where(o => o.ID == myID).SingleOrDefault();
}
public static Table<T> GetTable() {
// so you can write queries like:
// var q = MyClass.GetTable();
// instead of:
// var q = myDataContext.GetTable<MyClass>();
}
Конечно, как вы можете себе представить, это означает, что LinqedTable
должен каким-то образом иметь доступ к DataContext. До недавнего времени я достигал этого путем кэширования DataContext в статическом контексте. Да, "до недавнего времени", потому что это "недавно" - это когда я обнаружил, что на самом деле вы не должны держаться за DataContext дольше единицы работы, иначе все виды гремлинов начнут выходить из работы по дереву. Урок выучен.
Итак, теперь я знаю, что не могу слишком долго держаться за этот контекст данных... вот почему я начал экспериментировать с созданием DataContext по требованию, кэшируемым только на текущем LinqedTable
пример. Затем это привело к проблеме, когда вновь созданный DataContext не хочет иметь ничего общего с моим объектом, потому что он "знает", что он неверен к DataContext, который его создал.
Есть ли способ вставить информацию DataContext в LinqedTable во время создания или загрузки?
Это действительно проблемка. Я определенно не хочу идти на компромисс по всем этим удобным функциям, которые я положил в LinqedTable
базовый класс, и я должен быть в состоянии отпустить DataContext, когда это необходимо, и держаться за него, пока он еще нужен.
Есть другие идеи?
6 ответов
Ошибка "Объект может быть присоединен как измененный без исходного состояния, если он объявляет член версии", когда присоединение права, имеющего член метки времени, произойдет (должно) только в том случае, если объект не прошел "по проводам" (читай: был сериализован и снова десериализован). Если вы тестируете с помощью локального тестового приложения, которое не использует WCF или что-то еще, что приведет к сериализации и десериализации сущностей, они все равно сохранят ссылки на исходный текстовый код через entitysets / entityrefs (association / nav. Properties).
Если это так, вы можете обойти его, сериализовав и десериализовав его локально, прежде чем вызывать метод datacontext.Attach. Например:
internal static T CloneEntity<T>(T originalEntity)
{
Type entityType = typeof(T);
DataContractSerializer ser =
new DataContractSerializer(entityType);
using (MemoryStream ms = new MemoryStream())
{
ser.WriteObject(ms, originalEntity);
ms.Position = 0;
return (T)ser.ReadObject(ms);
}
}
В качестве альтернативы вы можете отсоединить его, установив для всех наборов сущностей / entityrefs значение null, но это более подвержено ошибкам, поэтому, хотя и немного дороже, я просто использую описанный выше метод DataContractSerializer всякий раз, когда хочу локально смоделировать поведение n-уровня...
(связанная ветка: http://social.msdn.microsoft.com/Forums/en-US/linqtosql/thread/eeeee9ae-fafb-4627-aa2e-e30570f637ba)
Обновление с LINQ to SQL, ну, интересно.
Если контекст данных пропал (что в большинстве случаев так и должно быть), вам потребуется получить новый контекст данных и выполнить запрос для получения объекта, который вы хотите обновить. Это абсолютное правило в LINQ to SQL, что вы должны получить объект для его удаления, и это почти так же железно, что вы должны получить объект, чтобы обновить его. Есть обходные пути, но они безобразны и, как правило, имеют гораздо больше способов доставить вам неприятности. Так что просто иди возьми запись и покончи с этим.
После того, как вы получите повторно выбранный объект, обновите его содержимым существующего объекта, в котором есть изменения. Затем выполните SubmitChanges() для нового контекста данных. Это оно! LINQ to SQL сгенерирует довольно жесткую версию оптимистичного параллелизма, сравнивая каждое значение в записи с исходной (в повторно выбранной) записи. Если какое-либо значение изменилось, пока у вас были данные, LINQ to SQL выдаст исключение параллелизма. (Так что вам не нужно менять все свои таблицы для контроля версий или временных меток.)
Если у вас есть какие-либо вопросы по поводу сгенерированных операторов обновлений, вам придется отключить SQL Profiler и посмотреть, как обновления попадают в базу данных. Что на самом деле хорошая идея, пока вы не получите уверенность в сгенерированном SQL.
Последнее замечание о транзакциях - контекст данных будет генерировать транзакцию для каждого вызова SubmitChanges(), если нет внешней транзакции. Если у вас есть несколько элементов для обновления и вы хотите запустить их как одну транзакцию, убедитесь, что вы используете один и тот же контекст данных для всех них, и дождитесь вызова SubmitChanges(), пока вы не обновите все содержимое объекта.
Если такой подход к транзакциям неосуществим, ищите объект TransactionScope. Это будет твой друг.
Я думаю, что 2 не лучший вариант. Звучит так, будто вы собираетесь создать один DataContext и поддерживать его в течение всего времени жизни вашей программы, что является плохой идеей. DataContexts - это легкие объекты, предназначенные для ускорения, когда они вам нужны. Попытка сохранить ссылки также, вероятно, приведет к тесному объединению областей вашей программы, которые вы предпочитаете хранить отдельно.
Пробег сто ALTER TABLE
Однократные заявления, воссоздание контекста и сохранение архитектуры простой и отделенной - элегантный ответ...
найти контекст данных, в котором изначально был создан объект, и использовать его для отправки изменений
Куда делся ваш текстовый текст? Почему так трудно найти? Вы используете только один в любой момент времени, верно?
Так какого черта я должен присоединить, если не существующее существо? Если бы я хотел новую запись, я бы сделал InsertOnSubmit()! Так как же вы должны использовать Attach()?
Вы должны прикрепить экземпляр, который представляет существующую запись... но не был загружен другим datacontext - не может иметь два контекста, отслеживающие состояние записи в одном и том же экземпляре. Если вы создадите новый экземпляр (например, клон), вам будет хорошо.
Возможно, вы захотите проверить эту статью и ее шаблоны параллелизма для раздела обновления и удаления.
Вы можете присоединиться к новому DataContext. Единственное, что мешает вам сделать это при нормальных обстоятельствах, - это регистрация событий, измененных собственностью, которые происходят в пределах EntitySet<T>
а также EntityRef<T>
классы. Чтобы разрешить перенос сущности между контекстами, сначала необходимо отсоединить сущность от DataContext, удалив эти регистрации событий, а затем снова присоединить к новому контексту, используя DataContext.Attach()
метод.
Вот хороший пример.
Когда вы извлекаете данные в первую очередь, отключите отслеживание объектов в контексте, который выполняет поиск. Это предотвратит отслеживание состояния объекта в исходном контексте. Затем, когда пришло время сохранить значения, присоединиться к новому контексту, обновить, чтобы установить исходные значения для объекта из базы данных, а затем отправить изменения. Следующее сработало для меня, когда я проверил это.
MyClass obj = null;
using (DataContext context = new DataContext())
{
context.ObjectTrackingEnabled = false;
obj = (from p in context.MyClasses
where p.ID == someId
select p).FirstOrDefault();
}
obj.Name += "test";
using (DataContext context2 = new ())
{
context2.MyClasses.Attach(obj);
context2.Refresh(System.Data.Linq.RefreshMode.KeepCurrentValues, obj);
context2.SubmitChanges();
}