Linq to SQL: порядок выполнения при вызове SubmitChanges()
У меня есть две связанные таблицы базы данных, которые в упрощенной форме выглядят так:
Product(
product_id,
name
)
ProductSpecs(
spec_id,
product_id,
name,
value
)
Внешний ключ устанавливается через поле product_id, а таблица ProductSpecs имеет уникальное ограничение на пару (product_id, name).
Теперь в моем приложении ASP.NET MVC, когда пользователь редактирует спецификации продукта и сохраняет данные, я удаляю старые спецификации и вставляю их как новые.
Я делаю это, сначала вызывая DataContext.DeleteAllOnSubmit() и предоставляя текущий (старый) ProductSpecs в качестве параметра, а затем добавляю новые спецификации в коллекцию Product.ProductSpecs.
Затем я вызываю DataContext.SubmitChanges() и получаю сообщение о том, что мое уникальное ограничение было нарушено.
Изучая инструкции SQL, возвращаемые DataContenxt.GetChangeText(), я вижу, что INSERT выполняются до DELETE (даже если я назвал DeleteAllOnSubmit() перед добавлением).
В чем причина этого поведения и как его исправить или обмануть?
Спасибо.
Ответы
Ответ 1
Да, по какой-то причине Linq to SQL выполняет все удаления как последнее. И нет способа изменить это.
Или, может быть, есть. Я не смотрел в codegen DataContext, чтобы увидеть, можем ли мы что-то переопределить там.
Вы можете вызвать SubmitChanges() столько раз, сколько хотите. Мое обходное решение, когда мне нужно удалить в качестве первого, отметить мои удаления, вызвать SubmitChanges(), затем выполнить вставки и обновления и снова вызвать SubmitChanges.
Вы можете обернуть все внутри TransactionScope:
var deletables =
from toDelete in db.NamedValues
where toDelete.Name == knownValue.Name
select toDelete;
using (var scope = new TransactionScope())
{
db.NamedValues.DeleteAllOnSubmit(deletables);
db.SubmitChanges();
db.NamedValues.InsertOnSubmit(knownValue);
db.SubmitChanges();
scope.Complete();
}
Ответ 2
У меня была такая же проблема. Я придумал хак, чтобы обойти его, что, похоже, работает для меня: я вручную выполняю SQL для каждого потенциально проблемного удаления в наборе изменений перед вызовом функции SubmitChanges():
foreach (object o in m_Db.GetChangeSet().Deletes)
{
LibraryMember m = o as LibraryMember;
if (m != null)
{
m_Db.ExecuteCommand("DELETE FROM LibraryMembers WHERE LibraryMemberId={0}", m.LibraryMemberId);
}
}
m_Db.SubmitChanges()
Ответ 3
Лучшее решение, которое не требует заранее знать, какие сущности были изменены, заключается в использовании частичных методов DataContext для перехвата вставок и нажатия на удаление сначала.
public partial class YourDataContext
{
List<EntityX> _deletedEntities = new List<EntityX>();
partial void InsertEntityX(EntityX instance)
{
// Run deletes before inserts so that we don't run into any index violations
var deletes =
this.GetChangeSet().Deletes
.Where(d => d.GetType().Equals(instance.GetType()))
.Cast<EntityX>().ToList();
var replaced = deletes.SingleOrDefault(d => d.UniqueKeyId == instance.UniqueKeyId);
if (replaced != null)
{
DeleteEntityX(replaced);
_deletedEntities.Add(replaced);
}
this.ExecuteDynamicInsert(instance);
}
partial void DeleteEntityX(EntityX instance)
{
if (_deletedEntities.Contains(instance))
{
_deletedEntities.Remove(instance);
return;
}
this.ExecuteDynamicDelete(instance);
}
}
Ответ 4
Другим решением является привязка изменений к другому контексту данных в правильном порядке. Однако недостатком этого решения является то, что любые объекты, все еще ссылающиеся на остальную часть вашего приложения, которые были из исходного контекста данных, теперь будут недействительными: (
Я пробовал небольшую вариацию, создавая второй временный контекст данных для фиксации. Но я не могу понять, как получить исходный контекст данных в чистое состояние.
Это очень расстраивает: (
public void Save()
{
string connectionString = m_Db.Connection.ConnectionString;
IList<object> deletes = m_Db.GetChangeSet().Deletes;
IList<object> inserts = m_Db.GetChangeSet().Inserts;
IList<object> updates = m_Db.GetChangeSet().Updates;
m_Db.Dispose();
m_Db = new MyDataContext(connectionString);
Attach(m_Db, deletes);
m_Db.SubmitChanges();
Attach(m_Db, updates);
m_Db.SubmitChanges();
Attach(m_Db, inserts);
m_Db.SubmitChanges();
}
void Attach(DataContext context, IList<object> items)
{
foreach (object o in items)
{
context.GetTable(o.GetType()).Attach(Clone(o));
}
}
object Clone(object o)
{
object result = o.GetType().GetConstructor(Type.EmptyTypes).Invoke(null);
foreach (PropertyInfo property in o.GetType().GetProperties())
{
if (property.PropertyType.IsValueType)
{
property.SetValue(result, property.GetValue(o, null), null);
}
}
return result;
}
Ответ 5
Вместо того, чтобы просто выбросить всех детей и воссоздать их, подумайте о том, чтобы приложить немного усилий и сделать это в два этапа: добавьте элементы, которые еще не добавлены, и удалите те, которые ранее добавлены, но больше не нужны, Например, на сайте ThinqLinq.com у меня есть сообщения, которые могут быть назначены категориям. На экране редактирования я предоставляю список категорий, которые можно выбрать и не выбрать. Когда я получаю сообщение, содержащее список выбранных категорий, я использую следующее, чтобы удалить и обновить соответствующие записи:
Dim selectedCats() As String = CType(ValueProvider("categories").RawValue, String())
For Each catId In selectedCats.Except(From cp In currentPost.CategoryPosts _
Select id = cp.CategoryID.ToString())
'Add new categories
currentPost.CategoryPosts.Add(New CategoryPost With {.CategoryID = CInt(catId)})
Next
'Delete removed categories
dc.CategoryPosts.DeleteAllOnSubmit(From cp In currentPost.CategoryPosts _
Where Not selectedCats.Contains(cp.CategoryID.ToString))