536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
766 lines
36 KiB
C#
766 lines
36 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Diagnostics;
|
|
|
|
namespace System.Data.Linq {
|
|
using System.Data.Linq.Mapping;
|
|
using System.Data.Linq.Provider;
|
|
|
|
/// <summary>
|
|
/// Describes the type of change the entity will undergo when submitted to the database.
|
|
/// </summary>
|
|
public enum ChangeAction {
|
|
/// <summary>
|
|
/// The entity will not be submitted.
|
|
/// </summary>
|
|
None = 0,
|
|
/// <summary>
|
|
/// The entity will be deleted.
|
|
/// </summary>
|
|
Delete,
|
|
/// <summary>
|
|
/// The entity will be inserted.
|
|
/// </summary>
|
|
Insert,
|
|
/// <summary>
|
|
/// The entity will be updated.
|
|
/// </summary>
|
|
Update
|
|
}
|
|
|
|
internal class ChangeProcessor {
|
|
CommonDataServices services;
|
|
DataContext context;
|
|
ChangeTracker tracker;
|
|
ChangeDirector changeDirector;
|
|
EdgeMap currentParentEdges;
|
|
EdgeMap originalChildEdges;
|
|
ReferenceMap originalChildReferences;
|
|
|
|
internal ChangeProcessor(CommonDataServices services, DataContext context) {
|
|
this.services = services;
|
|
this.context = context;
|
|
this.tracker = services.ChangeTracker;
|
|
this.changeDirector = services.ChangeDirector;
|
|
this.currentParentEdges = new EdgeMap();
|
|
this.originalChildEdges = new EdgeMap();
|
|
this.originalChildReferences = new ReferenceMap();
|
|
}
|
|
|
|
internal void SubmitChanges(ConflictMode failureMode) {
|
|
this.TrackUntrackedObjects();
|
|
// Must apply inferred deletions only after any untracked objects
|
|
// are tracked
|
|
this.ApplyInferredDeletions();
|
|
this.BuildEdgeMaps();
|
|
|
|
var list = this.GetOrderedList();
|
|
|
|
ValidateAll(list);
|
|
|
|
int numUpdatesAttempted = 0;
|
|
ChangeConflictSession conflictSession = new ChangeConflictSession(this.context);
|
|
List<ObjectChangeConflict> conflicts = new List<ObjectChangeConflict>();
|
|
List<TrackedObject> deletedItems = new List<TrackedObject>();
|
|
List<TrackedObject> insertedItems = new List<TrackedObject>();
|
|
List<TrackedObject> syncDependentItems = new List<TrackedObject>();
|
|
|
|
foreach (TrackedObject item in list) {
|
|
try {
|
|
if (item.IsNew) {
|
|
if (item.SynchDependentData()) {
|
|
syncDependentItems.Add(item);
|
|
}
|
|
changeDirector.Insert(item);
|
|
// store all inserted items for post processing
|
|
insertedItems.Add(item);
|
|
}
|
|
else if (item.IsDeleted) {
|
|
// Delete returns 1 if the delete was successfull, 0 if the row exists
|
|
// but wasn't deleted due to an OC conflict, or -1 if the row was
|
|
// deleted by another context (no OC conflict in this case)
|
|
numUpdatesAttempted++;
|
|
int ret = changeDirector.Delete(item);
|
|
if (ret == 0) {
|
|
conflicts.Add(new ObjectChangeConflict(conflictSession, item, false));
|
|
}
|
|
else {
|
|
// store all deleted items for post processing
|
|
deletedItems.Add(item);
|
|
}
|
|
}
|
|
else if (item.IsPossiblyModified) {
|
|
if (item.SynchDependentData()) {
|
|
syncDependentItems.Add(item);
|
|
}
|
|
if (item.IsModified) {
|
|
CheckForInvalidChanges(item);
|
|
numUpdatesAttempted++;
|
|
if (changeDirector.Update(item) <= 0) {
|
|
conflicts.Add(new ObjectChangeConflict(conflictSession, item));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (ChangeConflictException) {
|
|
conflicts.Add(new ObjectChangeConflict(conflictSession, item));
|
|
}
|
|
if (conflicts.Count > 0 && failureMode == ConflictMode.FailOnFirstConflict) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if we have accumulated any failed updates, throw the exception now
|
|
if (conflicts.Count > 0) {
|
|
// First we need to rollback any value that have already been auto-sync'd, since the values are no longer valid on the server
|
|
changeDirector.RollbackAutoSync();
|
|
// Also rollback any dependent items that were sync'd, since their parent values may have been rolled back
|
|
foreach (TrackedObject syncDependentItem in syncDependentItems) {
|
|
Debug.Assert(syncDependentItem.IsNew || syncDependentItem.IsPossiblyModified, "SynchDependent data should only be rolled back for new and modified objects.");
|
|
syncDependentItem.SynchDependentData();
|
|
}
|
|
this.context.ChangeConflicts.Fill(conflicts);
|
|
throw CreateChangeConflictException(numUpdatesAttempted, conflicts.Count);
|
|
}
|
|
else {
|
|
// No conflicts occurred, so we don't need to save the rollback values anymore
|
|
changeDirector.ClearAutoSyncRollback();
|
|
}
|
|
|
|
// Only after all updates have been sucessfully processed do we want to make
|
|
// post processing modifications to the objects and/or cache state.
|
|
PostProcessUpdates(insertedItems, deletedItems);
|
|
}
|
|
|
|
private void PostProcessUpdates(List<TrackedObject> insertedItems, List<TrackedObject> deletedItems) {
|
|
// perform post delete processing
|
|
foreach (TrackedObject deletedItem in deletedItems) {
|
|
// remove deleted item from identity cache
|
|
this.services.RemoveCachedObjectLike(deletedItem.Type, deletedItem.Original);
|
|
ClearForeignKeyReferences(deletedItem);
|
|
}
|
|
|
|
// perform post insert processing
|
|
foreach (TrackedObject insertedItem in insertedItems) {
|
|
object lookup = this.services.InsertLookupCachedObject(insertedItem.Type, insertedItem.Current);
|
|
if (lookup != insertedItem.Current) {
|
|
throw new DuplicateKeyException(insertedItem.Current, Strings.DatabaseGeneratedAlreadyExistingKey);
|
|
}
|
|
insertedItem.InitializeDeferredLoaders();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears out the foreign key values and parent object references for deleted objects on the child side of a relationship.
|
|
/// For bi-directional relationships, also performs the following fixup:
|
|
/// - for 1:N we remove the deleted entity from the opposite EntitySet or collection
|
|
/// - for 1:1 we null out the back reference
|
|
/// </summary>
|
|
private void ClearForeignKeyReferences(TrackedObject to) {
|
|
Debug.Assert(to.IsDeleted, "Foreign key reference cleanup should only happen on Deleted objects.");
|
|
foreach (MetaAssociation assoc in to.Type.Associations) {
|
|
if (assoc.IsForeignKey) {
|
|
// If there is a member on the other side referring back to us (i.e. this is a bi-directional relationship),
|
|
// we want to do a cache lookup to find the other side, then will remove ourselves from that collection.
|
|
// This cache lookup is only possible if the other key is the primary key, since that is the only way items can be found in the cache.
|
|
if (assoc.OtherMember != null && assoc.OtherKeyIsPrimaryKey) {
|
|
Debug.Assert(assoc.OtherMember.IsAssociation, "OtherMember of the association is expected to also be an association.");
|
|
// Search the cache for the target of the association, since
|
|
// it might not be loaded on the object being deleted, and we
|
|
// don't want to force a load.
|
|
object[] keyValues = CommonDataServices.GetForeignKeyValues(assoc, to.Current);
|
|
object cached = this.services.IdentityManager.Find(assoc.OtherType, keyValues);
|
|
|
|
if (cached != null) {
|
|
if (assoc.OtherMember.Association.IsMany) {
|
|
// Note that going through the IList interface handles
|
|
// EntitySet as well as POCO collections that implement IList
|
|
// and are not FixedSize.
|
|
System.Collections.IList collection = assoc.OtherMember.MemberAccessor.GetBoxedValue(cached) as System.Collections.IList;
|
|
if (collection != null && !collection.IsFixedSize) {
|
|
collection.Remove(to.Current);
|
|
// Explicitly clear the foreign key values and parent object reference
|
|
ClearForeignKeysHelper(assoc, to.Current);
|
|
}
|
|
}
|
|
else {
|
|
// Null out the other association. Since this is a 1:1 association,
|
|
// we're not concerned here with causing a deferred load, since the
|
|
// target is already cached (since we're deleting it).
|
|
assoc.OtherMember.MemberAccessor.SetBoxedValue(ref cached, null);
|
|
// Explicitly clear the foreign key values and parent object reference
|
|
ClearForeignKeysHelper(assoc, to.Current);
|
|
}
|
|
}
|
|
// else the item was not found in the cache, so there is no fixup that has to be done
|
|
// We are explicitly not calling ClearForeignKeysHelper because it breaks existing shipped behavior and we want to maintain backward compatibility
|
|
}
|
|
else {
|
|
// This is a unidirectional relationship or we have no way to look up the other side in the cache, so just clear our own side
|
|
ClearForeignKeysHelper(assoc, to.Current);
|
|
}
|
|
}
|
|
// else this is not the 1-side (foreign key) of the relationship, so there is nothing for us to do
|
|
}
|
|
}
|
|
|
|
// Ensure the the member and foreign keys are nulled so that after trackedInstance is deleted,
|
|
// the object does not appear to be associated with the other side anymore. This prevents the deleted object
|
|
// from referencing objects still in the cache, but also will prevent the related object from being implicitly loaded
|
|
private static void ClearForeignKeysHelper(MetaAssociation assoc, object trackedInstance) {
|
|
Debug.Assert(assoc.IsForeignKey, "Foreign key clearing should only happen on foreign key side of the association.");
|
|
Debug.Assert(assoc.ThisMember.IsAssociation, "Expected ThisMember of an association to always be an association.");
|
|
|
|
// If this member is one of our deferred loaders, and it does not already have a value, explicitly set the deferred source to
|
|
// null so that when we set the association member itself to null later, it doesn't trigger an implicit load.
|
|
// This is only necessary if the value has not already been assigned or set, because otherwise we won't implicitly load anyway when the member is accessed.
|
|
MetaDataMember thisMember = assoc.ThisMember;
|
|
|
|
if (thisMember.IsDeferred &&
|
|
!(thisMember.StorageAccessor.HasAssignedValue(trackedInstance) || thisMember.StorageAccessor.HasLoadedValue(trackedInstance)))
|
|
{
|
|
// If this is a deferred member, set the value directly in the deferred accessor instead of going
|
|
// through the normal member accessor, so that we don't trigger an implicit load.
|
|
thisMember.DeferredSourceAccessor.SetBoxedValue(ref trackedInstance, null);
|
|
}
|
|
|
|
// Notify the object that the relationship should be considered deleted.
|
|
// This allows the object to do its own fixup even when we can't do it automatically.
|
|
thisMember.MemberAccessor.SetBoxedValue(ref trackedInstance, null);
|
|
|
|
// Also set the foreign key values to null if possible
|
|
for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) {
|
|
MetaDataMember thisKey = assoc.ThisKey[i];
|
|
if (thisKey.CanBeNull) {
|
|
thisKey.StorageAccessor.SetBoxedValue(ref trackedInstance, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ValidateAll(IEnumerable<TrackedObject> list) {
|
|
foreach (var item in list) {
|
|
if (item.IsNew) {
|
|
item.SynchDependentData();
|
|
if (item.Type.HasAnyValidateMethod) {
|
|
SendOnValidate(item.Type, item, ChangeAction.Insert);
|
|
}
|
|
} else if (item.IsDeleted) {
|
|
if (item.Type.HasAnyValidateMethod) {
|
|
SendOnValidate(item.Type, item, ChangeAction.Delete);
|
|
}
|
|
} else if (item.IsPossiblyModified) {
|
|
item.SynchDependentData();
|
|
if (item.IsModified && item.Type.HasAnyValidateMethod) {
|
|
SendOnValidate(item.Type, item, ChangeAction.Update);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void SendOnValidate(MetaType type, TrackedObject item, ChangeAction changeAction) {
|
|
if (type != null) {
|
|
SendOnValidate(type.InheritanceBase, item, changeAction);
|
|
|
|
if (type.OnValidateMethod != null) {
|
|
try {
|
|
type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction });
|
|
} catch (TargetInvocationException tie) {
|
|
if (tie.InnerException != null) {
|
|
throw tie.InnerException;
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal string GetChangeText() {
|
|
this.ObserveUntrackedObjects();
|
|
// Must apply inferred deletions only after any untracked objects
|
|
// are tracked
|
|
this.ApplyInferredDeletions();
|
|
this.BuildEdgeMaps();
|
|
|
|
// append change text only
|
|
StringBuilder changeText = new StringBuilder();
|
|
foreach (TrackedObject item in this.GetOrderedList()) {
|
|
if (item.IsNew) {
|
|
item.SynchDependentData();
|
|
changeDirector.AppendInsertText(item, changeText);
|
|
}
|
|
else if (item.IsDeleted) {
|
|
changeDirector.AppendDeleteText(item, changeText);
|
|
}
|
|
else if (item.IsPossiblyModified) {
|
|
item.SynchDependentData();
|
|
if (item.IsModified) {
|
|
changeDirector.AppendUpdateText(item, changeText);
|
|
}
|
|
}
|
|
}
|
|
return changeText.ToString();
|
|
}
|
|
|
|
internal ChangeSet GetChangeSet() {
|
|
List<object> newEntities = new List<object>();
|
|
List<object> deletedEntities = new List<object>();
|
|
List<object> changedEntities = new List<object>();
|
|
|
|
this.ObserveUntrackedObjects();
|
|
// Must apply inferred deletions only after any untracked objects
|
|
// are tracked
|
|
this.ApplyInferredDeletions();
|
|
|
|
foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
|
|
if (item.IsNew) {
|
|
item.SynchDependentData();
|
|
newEntities.Add(item.Current);
|
|
}
|
|
else if (item.IsDeleted) {
|
|
deletedEntities.Add(item.Current);
|
|
}
|
|
else if (item.IsPossiblyModified) {
|
|
item.SynchDependentData();
|
|
if (item.IsModified) {
|
|
changedEntities.Add(item.Current);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new ChangeSet(newEntities.AsReadOnly(), deletedEntities.AsReadOnly(), changedEntities.AsReadOnly());
|
|
}
|
|
|
|
// verify that primary key and db-generated values have not changed
|
|
private static void CheckForInvalidChanges(TrackedObject tracked) {
|
|
foreach (MetaDataMember mem in tracked.Type.PersistentDataMembers) {
|
|
if (mem.IsPrimaryKey || mem.IsDbGenerated || mem.IsVersion) {
|
|
if (tracked.HasChangedValue(mem)) {
|
|
if (mem.IsPrimaryKey) {
|
|
throw Error.IdentityChangeNotAllowed(mem.Name, tracked.Type.Name);
|
|
}
|
|
else {
|
|
throw Error.DbGeneratedChangeNotAllowed(mem.Name, tracked.Type.Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an ChangeConflictException with the best message
|
|
/// </summary>
|
|
static private ChangeConflictException CreateChangeConflictException(int totalUpdatesAttempted, int failedUpdates) {
|
|
string msg = Strings.RowNotFoundOrChanged;
|
|
if (totalUpdatesAttempted > 1) {
|
|
msg = Strings.UpdatesFailedMessage(failedUpdates, totalUpdatesAttempted);
|
|
}
|
|
return new ChangeConflictException(msg);
|
|
}
|
|
|
|
internal void TrackUntrackedObjects() {
|
|
Dictionary<object, object> visited = new Dictionary<object, object>();
|
|
|
|
// search for untracked new objects
|
|
List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
|
|
foreach (TrackedObject item in items) {
|
|
this.TrackUntrackedObjects(item.Type, item.Current, visited);
|
|
}
|
|
}
|
|
|
|
internal void ApplyInferredDeletions() {
|
|
foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
|
|
if (item.CanInferDelete()) {
|
|
// based on DeleteOnNull specifications on the item's associations,
|
|
// a deletion can be inferred for this item. The actual state transition
|
|
// is dependent on the current item state.
|
|
if (item.IsNew) {
|
|
item.ConvertToRemoved();
|
|
}
|
|
else if (item.IsPossiblyModified || item.IsModified) {
|
|
item.ConvertToDeleted();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TrackUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
|
|
if (!visited.ContainsKey(item)) {
|
|
visited.Add(item, item);
|
|
TrackedObject tracked = this.tracker.GetTrackedObject(item);
|
|
if (tracked == null) {
|
|
tracked = this.tracker.Track(item);
|
|
tracked.ConvertToNew();
|
|
}
|
|
else if (tracked.IsDead || tracked.IsRemoved) {
|
|
// ignore
|
|
return;
|
|
}
|
|
|
|
// search parents (objects we are dependent on)
|
|
foreach (RelatedItem parent in this.services.GetParents(type, item)) {
|
|
this.TrackUntrackedObjects(parent.Type, parent.Item, visited);
|
|
}
|
|
|
|
// synch up primary key
|
|
if (tracked.IsNew) {
|
|
tracked.InitializeDeferredLoaders();
|
|
|
|
if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
|
|
tracked.SynchDependentData();
|
|
object cached = this.services.InsertLookupCachedObject(tracked.Type, item);
|
|
if (cached != item) {
|
|
TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
|
|
Debug.Assert(cachedTracked != null);
|
|
if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
|
|
// adding new object with same ID as object being deleted.. turn into modified
|
|
tracked.ConvertToPossiblyModified(cachedTracked.Original);
|
|
// turn deleted to dead...
|
|
cachedTracked.ConvertToDead();
|
|
|
|
this.services.RemoveCachedObjectLike(tracked.Type, item);
|
|
this.services.InsertLookupCachedObject(tracked.Type, item);
|
|
}
|
|
else if (!cachedTracked.IsDead) {
|
|
throw new DuplicateKeyException(item, Strings.CantAddAlreadyExistingKey);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// we may have a generated PK, however we set the PK on this new item to
|
|
// match a deleted item
|
|
object cached = this.services.GetCachedObjectLike(tracked.Type, item);
|
|
if (cached != null) {
|
|
TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
|
|
Debug.Assert(cachedTracked != null);
|
|
if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
|
|
// adding new object with same ID as object being deleted.. turn into modified
|
|
tracked.ConvertToPossiblyModified(cachedTracked.Original);
|
|
// turn deleted to dead...
|
|
cachedTracked.ConvertToDead();
|
|
|
|
this.services.RemoveCachedObjectLike(tracked.Type, item);
|
|
this.services.InsertLookupCachedObject(tracked.Type, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// search children (objects that are dependent on us)
|
|
foreach (RelatedItem child in this.services.GetChildren(type, item)) {
|
|
this.TrackUntrackedObjects(child.Type, child.Item, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ObserveUntrackedObjects() {
|
|
Dictionary<object, object> visited = new Dictionary<object, object>();
|
|
|
|
List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
|
|
foreach (TrackedObject item in items) {
|
|
this.ObserveUntrackedObjects(item.Type, item.Current, visited);
|
|
}
|
|
}
|
|
|
|
private void ObserveUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
|
|
if (!visited.ContainsKey(item)) {
|
|
visited.Add(item, item);
|
|
TrackedObject tracked = this.tracker.GetTrackedObject(item);
|
|
if (tracked == null) {
|
|
tracked = this.tracker.Track(item);
|
|
tracked.ConvertToNew();
|
|
} else if (tracked.IsDead || tracked.IsRemoved) {
|
|
// ignore
|
|
return;
|
|
}
|
|
|
|
// search parents (objects we are dependent on)
|
|
foreach (RelatedItem parent in this.services.GetParents(type, item)) {
|
|
this.ObserveUntrackedObjects(parent.Type, parent.Item, visited);
|
|
}
|
|
|
|
// synch up primary key unless its generated.
|
|
if (tracked.IsNew) {
|
|
if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
|
|
tracked.SynchDependentData();
|
|
}
|
|
}
|
|
|
|
// search children (objects that are dependent on us)
|
|
foreach (RelatedItem child in this.services.GetChildren(type, item)) {
|
|
this.ObserveUntrackedObjects(child.Type, child.Item, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
private TrackedObject GetOtherItem(MetaAssociation assoc, object instance) {
|
|
if (instance == null)
|
|
return null;
|
|
object other = null;
|
|
// Don't load unloaded references
|
|
if (assoc.ThisMember.StorageAccessor.HasAssignedValue(instance) ||
|
|
assoc.ThisMember.StorageAccessor.HasLoadedValue(instance)
|
|
) {
|
|
other = assoc.ThisMember.MemberAccessor.GetBoxedValue(instance);
|
|
}
|
|
else if (assoc.OtherKeyIsPrimaryKey) {
|
|
// Maybe it's in the cache, but not yet attached through reference.
|
|
object[] foreignKeys = CommonDataServices.GetForeignKeyValues(assoc, instance);
|
|
other = this.services.GetCachedObject(assoc.OtherType, foreignKeys);
|
|
}
|
|
// else the other key is not the primary key so there is no way to try to look it up
|
|
return (other != null) ? this.tracker.GetTrackedObject(other) : null;
|
|
}
|
|
|
|
private bool HasAssociationChanged(MetaAssociation assoc, TrackedObject item) {
|
|
if (item.Original != null && item.Current != null) {
|
|
if (assoc.ThisMember.StorageAccessor.HasAssignedValue(item.Current) ||
|
|
assoc.ThisMember.StorageAccessor.HasLoadedValue(item.Current)
|
|
) {
|
|
return this.GetOtherItem(assoc, item.Current) != this.GetOtherItem(assoc, item.Original);
|
|
}
|
|
else {
|
|
object[] currentFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Current);
|
|
object[] originaFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Original);
|
|
for (int i = 0, n = currentFKs.Length; i < n; i++) {
|
|
if (!object.Equals(currentFKs[i], originaFKs[i]))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void BuildEdgeMaps() {
|
|
this.currentParentEdges.Clear();
|
|
this.originalChildEdges.Clear();
|
|
this.originalChildReferences.Clear();
|
|
|
|
List<TrackedObject> list = new List<TrackedObject>(this.tracker.GetInterestingObjects());
|
|
foreach (TrackedObject item in list) {
|
|
bool isNew = item.IsNew;
|
|
MetaType mt = item.Type;
|
|
foreach (MetaAssociation assoc in mt.Associations) {
|
|
if (assoc.IsForeignKey) {
|
|
TrackedObject otherItem = this.GetOtherItem(assoc, item.Current);
|
|
TrackedObject dbOtherItem = this.GetOtherItem(assoc, item.Original);
|
|
bool pointsToDeleted = (otherItem != null && otherItem.IsDeleted) || (dbOtherItem != null && dbOtherItem.IsDeleted);
|
|
bool pointsToNew = (otherItem != null && otherItem.IsNew);
|
|
|
|
if (isNew || pointsToDeleted || pointsToNew || this.HasAssociationChanged(assoc, item)) {
|
|
if (otherItem != null) {
|
|
this.currentParentEdges.Add(assoc, item, otherItem);
|
|
}
|
|
if (dbOtherItem != null) {
|
|
if (assoc.IsUnique) {
|
|
this.originalChildEdges.Add(assoc, dbOtherItem, item);
|
|
}
|
|
this.originalChildReferences.Add(dbOtherItem, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum VisitState {
|
|
Before,
|
|
After
|
|
}
|
|
|
|
private List<TrackedObject> GetOrderedList() {
|
|
var objects = this.tracker.GetInterestingObjects().ToList();
|
|
|
|
// give list an initial order (most likely correct order) to avoid deadlocks in server
|
|
var range = Enumerable.Range(0, objects.Count).ToList();
|
|
range.Sort((int x, int y) => Compare(objects[x], x, objects[y], y));
|
|
var ordered = range.Select(i => objects[i]).ToList();
|
|
|
|
// permute order if constraint dependencies requires some changes to come before others
|
|
var visited = new Dictionary<TrackedObject, VisitState>();
|
|
var list = new List<TrackedObject>();
|
|
foreach (TrackedObject item in ordered) {
|
|
this.BuildDependencyOrderedList(item, list, visited);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static int Compare(TrackedObject x, int xOrdinal, TrackedObject y, int yOrdinal) {
|
|
// deal with possible nulls
|
|
if (x == y) {
|
|
return 0;
|
|
}
|
|
if (x == null) {
|
|
return -1;
|
|
}
|
|
else if (y == null) {
|
|
return 1;
|
|
}
|
|
// first order by action: Inserts first, Updates, Deletes last
|
|
int xAction = x.IsNew ? 0 : x.IsDeleted ? 2 : 1;
|
|
int yAction = y.IsNew ? 0 : y.IsDeleted ? 2 : 1;
|
|
if (xAction < yAction) {
|
|
return -1;
|
|
}
|
|
else if (xAction > yAction) {
|
|
return 1;
|
|
}
|
|
// no need to order inserts (PK's may not even exist)
|
|
if (x.IsNew) {
|
|
// keep original order
|
|
return xOrdinal.CompareTo(yOrdinal);
|
|
}
|
|
// second order by type
|
|
if (x.Type != y.Type) {
|
|
return string.CompareOrdinal(x.Type.Type.FullName, y.Type.Type.FullName);
|
|
}
|
|
// lastly, order by PK values
|
|
int result = 0;
|
|
foreach (MetaDataMember mm in x.Type.IdentityMembers) {
|
|
object xValue = mm.StorageAccessor.GetBoxedValue(x.Current);
|
|
object yValue = mm.StorageAccessor.GetBoxedValue(y.Current);
|
|
if (xValue == null) {
|
|
if (yValue != null) {
|
|
return -1;
|
|
}
|
|
}
|
|
else {
|
|
IComparable xc = xValue as IComparable;
|
|
if (xc != null) {
|
|
result = xc.CompareTo(yValue);
|
|
if (result != 0) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// they are the same? leave in original order
|
|
return xOrdinal.CompareTo(yOrdinal);
|
|
}
|
|
|
|
private void BuildDependencyOrderedList(TrackedObject item, List<TrackedObject> list, Dictionary<TrackedObject, VisitState> visited) {
|
|
VisitState state;
|
|
if (visited.TryGetValue(item, out state)) {
|
|
if (state == VisitState.Before) {
|
|
throw Error.CycleDetected();
|
|
}
|
|
return;
|
|
}
|
|
|
|
visited[item] = VisitState.Before;
|
|
|
|
if (item.IsInteresting) {
|
|
if (item.IsDeleted) {
|
|
// if 'item' is deleted
|
|
// all objects that used to refer to 'item' must be ordered before item
|
|
foreach (TrackedObject other in this.originalChildReferences[item]) {
|
|
if (other != item) {
|
|
this.BuildDependencyOrderedList(other, list, visited);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// if 'item' is new or changed
|
|
// for all objects 'other' that 'item' refers to along association 'assoc'
|
|
// if 'other' is new then 'other' must be ordered before 'item'
|
|
// if 'assoc' is pure one-to-one and some other item 'prevItem' used to refer to 'other'
|
|
// then 'prevItem' must be ordered before 'item'
|
|
foreach (MetaAssociation assoc in item.Type.Associations) {
|
|
if (assoc.IsForeignKey) {
|
|
TrackedObject other = this.currentParentEdges[assoc, item];
|
|
if (other != null) {
|
|
if (other.IsNew) {
|
|
// if other is new, visit other first (since item's FK depends on it)
|
|
if (other != item || item.Type.DBGeneratedIdentityMember != null) {
|
|
this.BuildDependencyOrderedList(other, list, visited);
|
|
}
|
|
}
|
|
else if ((assoc.IsUnique || assoc.ThisKeyIsPrimaryKey)) {
|
|
TrackedObject prevItem = this.originalChildEdges[assoc, other];
|
|
if (prevItem != null && other != item) {
|
|
this.BuildDependencyOrderedList(prevItem, list, visited);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
list.Add(item);
|
|
}
|
|
|
|
visited[item] = VisitState.After;
|
|
}
|
|
|
|
class EdgeMap {
|
|
Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>> associations;
|
|
|
|
internal EdgeMap() {
|
|
this.associations = new Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>>();
|
|
}
|
|
|
|
internal void Add(MetaAssociation assoc, TrackedObject from, TrackedObject to) {
|
|
Dictionary<TrackedObject, TrackedObject> pairs;
|
|
if (!associations.TryGetValue(assoc, out pairs)) {
|
|
pairs = new Dictionary<TrackedObject, TrackedObject>();
|
|
associations.Add(assoc, pairs);
|
|
}
|
|
pairs.Add(from, to);
|
|
}
|
|
|
|
internal TrackedObject this[MetaAssociation assoc, TrackedObject from] {
|
|
get {
|
|
Dictionary<TrackedObject, TrackedObject> pairs;
|
|
if (associations.TryGetValue(assoc, out pairs)) {
|
|
TrackedObject to;
|
|
if (pairs.TryGetValue(from, out to)) {
|
|
return to;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
internal void Clear() {
|
|
this.associations.Clear();
|
|
}
|
|
}
|
|
|
|
class ReferenceMap {
|
|
Dictionary<TrackedObject, List<TrackedObject>> references;
|
|
|
|
internal ReferenceMap() {
|
|
this.references = new Dictionary<TrackedObject, List<TrackedObject>>();
|
|
}
|
|
|
|
internal void Add(TrackedObject from, TrackedObject to) {
|
|
List<TrackedObject> refs;
|
|
if (!references.TryGetValue(from, out refs)) {
|
|
refs = new List<TrackedObject>();
|
|
references.Add(from, refs);
|
|
}
|
|
if (!refs.Contains(to))
|
|
refs.Add(to);
|
|
}
|
|
|
|
internal IEnumerable<TrackedObject> this[TrackedObject from] {
|
|
get {
|
|
List<TrackedObject> refs;
|
|
if (references.TryGetValue(from, out refs)) {
|
|
return refs;
|
|
}
|
|
return Empty;
|
|
}
|
|
}
|
|
|
|
internal void Clear() {
|
|
this.references.Clear();
|
|
}
|
|
|
|
private static TrackedObject[] Empty = new TrackedObject[] { };
|
|
}
|
|
}
|
|
}
|