//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner Microsoft // @backupOwner Microsoft //--------------------------------------------------------------------- namespace System.Data.Mapping.Update.Internal { using System.Collections.Generic; using System.Data.Common; using System.Data.Common.Utils; using System.Data.Entity; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Globalization; using System.Linq; internal partial class UpdateTranslator { /// /// Class validating relationship cardinality constraints. Only reasons about constraints that can be inferred /// by examining change requests from the store. /// (no attempt is made to ensure consistency of the store subsequently, since this would require pulling in all /// values from the store). /// private class RelationshipConstraintValidator { #region Constructor internal RelationshipConstraintValidator(UpdateTranslator updateTranslator) { m_existingRelationships = new Dictionary(EqualityComparer.Default); m_impliedRelationships = new Dictionary(EqualityComparer.Default); m_referencingRelationshipSets = new Dictionary>(EqualityComparer.Default); m_updateTranslator = updateTranslator; } #endregion #region Fields /// /// Relationships registered in the validator. /// private readonly Dictionary m_existingRelationships; /// /// Relationships the validator determines are required based on registered entities. /// private readonly Dictionary m_impliedRelationships; /// /// Cache used to store relationship sets with ends bound to entity sets. /// private readonly Dictionary> m_referencingRelationshipSets; /// /// Update translator containing session context. /// private readonly UpdateTranslator m_updateTranslator; #endregion #region Methods /// /// Add an entity to be tracked by the validator. Requires that the input describes an entity. /// /// State entry for the entity being tracked. internal void RegisterEntity(IEntityStateEntry stateEntry) { EntityUtil.CheckArgumentNull(stateEntry, "stateEntry"); if (EntityState.Added == stateEntry.State || EntityState.Deleted == stateEntry.State) { // We only track added and deleted entities because modifications to entities do not affect // cardinality constraints. Relationships are based on end keys, and it is not // possible to modify key values. Debug.Assert(null != (object)stateEntry.EntityKey, "entity state entry must have an entity key"); EntityKey entityKey = EntityUtil.CheckArgumentNull(stateEntry.EntityKey, "stateEntry.EntityKey"); EntitySet entitySet = (EntitySet)stateEntry.EntitySet; EntityType entityType = EntityState.Added == stateEntry.State ? GetEntityType(stateEntry.CurrentValues) : GetEntityType(stateEntry.OriginalValues); // figure out relationship set ends that are associated with this entity set foreach (AssociationSet associationSet in GetReferencingAssocationSets(entitySet)) { // describe unidirectional relationships in which the added entity is the "destination" var ends = associationSet.AssociationSetEnds; foreach (var fromEnd in ends) { foreach (var toEnd in ends) { // end to itself does not describe an interesting relationship subpart if (object.ReferenceEquals(toEnd.CorrespondingAssociationEndMember, fromEnd.CorrespondingAssociationEndMember)) { continue; } // skip ends that don't target the current entity set if (!toEnd.EntitySet.EdmEquals(entitySet)) { continue; } // skip ends that aren't required if (0 == MetadataHelper.GetLowerBoundOfMultiplicity( fromEnd.CorrespondingAssociationEndMember.RelationshipMultiplicity)) { continue; } // skip ends that don't target the current entity type if (!MetadataHelper.GetEntityTypeForEnd(toEnd.CorrespondingAssociationEndMember) .IsAssignableFrom(entityType)) { continue; } // register the relationship so that we know it's required DirectionalRelationship relationship = new DirectionalRelationship(entityKey, fromEnd.CorrespondingAssociationEndMember, toEnd.CorrespondingAssociationEndMember, associationSet, stateEntry); m_impliedRelationships.Add(relationship, stateEntry); } } } } } // requires: input is an IExtendedDataRecord representing an entity // returns: entity type for the given record private static EntityType GetEntityType(DbDataRecord dbDataRecord) { IExtendedDataRecord extendedRecord = dbDataRecord as IExtendedDataRecord; Debug.Assert(extendedRecord != null); Debug.Assert(BuiltInTypeKind.EntityType == extendedRecord.DataRecordInfo.RecordType.EdmType.BuiltInTypeKind); return (EntityType)extendedRecord.DataRecordInfo.RecordType.EdmType; } /// /// Add a relationship to be tracked by the validator. /// /// Relationship set to which the given record belongs. /// Relationship record. Must conform to the type of the relationship set. /// State entry for the relationship being tracked internal void RegisterAssociation(AssociationSet associationSet, IExtendedDataRecord record, IEntityStateEntry stateEntry) { EntityUtil.CheckArgumentNull(associationSet, "relationshipSet"); EntityUtil.CheckArgumentNull(record, "record"); EntityUtil.CheckArgumentNull(stateEntry, "stateEntry"); Debug.Assert(associationSet.ElementType.Equals(record.DataRecordInfo.RecordType.EdmType)); // retrieve the ends of the relationship Dictionary endNameToKeyMap = new Dictionary( StringComparer.Ordinal); foreach (FieldMetadata field in record.DataRecordInfo.FieldMetadata) { string endName = field.FieldType.Name; EntityKey entityKey = (EntityKey)record.GetValue(field.Ordinal); endNameToKeyMap.Add(endName, entityKey); } // register each unidirectional relationship subpart in the relationship instance var ends = associationSet.AssociationSetEnds; foreach (var fromEnd in ends) { foreach (var toEnd in ends) { // end to itself does not describe an interesting relationship subpart if (object.ReferenceEquals(toEnd.CorrespondingAssociationEndMember, fromEnd.CorrespondingAssociationEndMember)) { continue; } EntityKey toEntityKey = endNameToKeyMap[toEnd.CorrespondingAssociationEndMember.Name]; DirectionalRelationship relationship = new DirectionalRelationship(toEntityKey, fromEnd.CorrespondingAssociationEndMember, toEnd.CorrespondingAssociationEndMember, associationSet, stateEntry); AddExistingRelationship(relationship); } } } /// /// Validates cardinality constraints for all added entities/relationships. /// internal void ValidateConstraints() { // ensure all expected relationships exist foreach (KeyValuePair expected in m_impliedRelationships) { DirectionalRelationship expectedRelationship = expected.Key; IEntityStateEntry stateEntry = expected.Value; // determine actual end cardinality int count = GetDirectionalRelationshipCountDelta(expectedRelationship); if (EntityState.Deleted == stateEntry.State) { // our cardinality expectations are reversed for delete (cardinality of 1 indicates // we want -1 operation total) count = -count; } // determine expected cardinality int minimumCount = MetadataHelper.GetLowerBoundOfMultiplicity(expectedRelationship.FromEnd.RelationshipMultiplicity); int? maximumCountDeclared = MetadataHelper.GetUpperBoundOfMultiplicity(expectedRelationship.FromEnd.RelationshipMultiplicity); int maximumCount = maximumCountDeclared.HasValue ? maximumCountDeclared.Value : count; // negative value // indicates unlimited cardinality if (count < minimumCount || count > maximumCount) { // We could in theory "fix" the cardinality constraint violation by introducing surrogates, // but we risk doing work on behalf of the user they don't want performed (e.g., deleting an // entity or relationship the user has intentionally left untouched). throw EntityUtil.UpdateRelationshipCardinalityConstraintViolation( expectedRelationship.AssociationSet.Name, minimumCount, maximumCountDeclared, TypeHelpers.GetFullName(expectedRelationship.ToEntityKey.EntityContainerName, expectedRelationship.ToEntityKey.EntitySetName), count, expectedRelationship.FromEnd.Name, stateEntry); } } // ensure actual relationships have required ends foreach (DirectionalRelationship actualRelationship in m_existingRelationships.Keys) { int addedCount; int deletedCount; actualRelationship.GetCountsInEquivalenceSet(out addedCount, out deletedCount); int absoluteCount = Math.Abs(addedCount - deletedCount); int minimumCount = MetadataHelper.GetLowerBoundOfMultiplicity(actualRelationship.FromEnd.RelationshipMultiplicity); int? maximumCount = MetadataHelper.GetUpperBoundOfMultiplicity(actualRelationship.FromEnd.RelationshipMultiplicity); // Check that we haven't inserted or deleted too many relationships if (maximumCount.HasValue) { EntityState? violationType = default(EntityState?); int? violationCount = default(int?); if (addedCount > maximumCount.Value) { violationType = EntityState.Added; violationCount = addedCount; } else if (deletedCount > maximumCount.Value) { violationType = EntityState.Deleted; violationCount = deletedCount; } if (violationType.HasValue) { throw EntityUtil.Update(Strings.Update_RelationshipCardinalityViolation(maximumCount.Value, violationType.Value, actualRelationship.AssociationSet.ElementType.FullName, actualRelationship.FromEnd.Name, actualRelationship.ToEnd.Name, violationCount.Value), null, actualRelationship.GetEquivalenceSet().Select(reln => reln.StateEntry)); } } // We care about the case where there is a relationship but no entity when // the relationship and entity map to the same table. If there is a relationship // with 1..1 cardinality to the entity and the relationship is being added or deleted, // it is required that the entity is also added or deleted. if (1 == absoluteCount && 1 == minimumCount && 1 == maximumCount) // 1..1 relationship being added/deleted { bool isAdd = addedCount > deletedCount; // Ensure the entity is also being added or deleted IEntityStateEntry entityEntry; // Identify the following error conditions: // - the entity is not being modified at all // - the entity is being modified, but not in the way we expect (it's not being added or deleted) if (!m_impliedRelationships.TryGetValue(actualRelationship, out entityEntry) || (isAdd && EntityState.Added != entityEntry.State) || (!isAdd && EntityState.Deleted != entityEntry.State)) { throw EntityUtil.UpdateEntityMissingConstraintViolation(actualRelationship.AssociationSet.Name, actualRelationship.ToEnd.Name, actualRelationship.StateEntry); } } } } /// /// Determines the net change in relationship count. /// For instance, if the directional relationship is added 2 times and deleted 3, the return value is -1. /// private int GetDirectionalRelationshipCountDelta(DirectionalRelationship expectedRelationship) { // lookup up existing relationship from expected relationship DirectionalRelationship existingRelationship; if (m_existingRelationships.TryGetValue(expectedRelationship, out existingRelationship)) { int addedCount; int deletedCount; existingRelationship.GetCountsInEquivalenceSet(out addedCount, out deletedCount); return addedCount - deletedCount; } else { // no modifications to the relationship... return 0 (no net change) return 0; } } private void AddExistingRelationship(DirectionalRelationship relationship) { DirectionalRelationship existingRelationship; if (m_existingRelationships.TryGetValue(relationship, out existingRelationship)) { existingRelationship.AddToEquivalenceSet(relationship); } else { m_existingRelationships.Add(relationship, relationship); } } /// /// Determine which relationship sets reference the given entity set. /// /// Entity set for which to identify relationships /// Relationship sets referencing the given entity set private IEnumerable GetReferencingAssocationSets(EntitySet entitySet) { List relationshipSets; // check if this information is cached if (!m_referencingRelationshipSets.TryGetValue(entitySet, out relationshipSets)) { relationshipSets = new List(); // relationship sets must live in the same container as the entity sets they reference EntityContainer container = entitySet.EntityContainer; foreach (EntitySetBase extent in container.BaseEntitySets) { AssociationSet associationSet = extent as AssociationSet; if (null != associationSet && !associationSet.ElementType.IsForeignKey) { foreach (var end in associationSet.AssociationSetEnds) { if (end.EntitySet.Equals(entitySet)) { relationshipSets.Add(associationSet); break; } } } } // add referencing relationship information to the cache m_referencingRelationshipSets.Add(entitySet, relationshipSets); } return relationshipSets; } #endregion #region Nested types /// /// An instance of an actual or expected relationship. This class describes one direction /// of the relationship. /// private class DirectionalRelationship : IEquatable { /// /// Entity key for the entity being referenced by the relationship. /// internal readonly EntityKey ToEntityKey; /// /// Name of the end referencing the entity key. /// internal readonly AssociationEndMember FromEnd; /// /// Name of the end the entity key references. /// internal readonly AssociationEndMember ToEnd; /// /// State entry containing this relationship. /// internal readonly IEntityStateEntry StateEntry; /// /// Reference to the relationship set. /// internal readonly AssociationSet AssociationSet; /// /// Reference to next 'equivalent' relationship in circular linked list. /// private DirectionalRelationship _equivalenceSetLinkedListNext; private readonly int _hashCode; internal DirectionalRelationship(EntityKey toEntityKey, AssociationEndMember fromEnd, AssociationEndMember toEnd, AssociationSet associationSet, IEntityStateEntry stateEntry) { ToEntityKey = EntityUtil.CheckArgumentNull(toEntityKey, "toEntityKey"); FromEnd = EntityUtil.CheckArgumentNull(fromEnd, "fromEnd"); ToEnd = EntityUtil.CheckArgumentNull(toEnd, "toEnd"); AssociationSet = EntityUtil.CheckArgumentNull(associationSet, "associationSet"); StateEntry = EntityUtil.CheckArgumentNull(stateEntry, "stateEntry"); _equivalenceSetLinkedListNext = this; _hashCode = toEntityKey.GetHashCode() ^ fromEnd.GetHashCode() ^ toEnd.GetHashCode() ^ associationSet.GetHashCode(); } /// /// Requires: 'other' must refer to the same relationship metadata and the same target entity and /// must not already be a part of an equivalent set. /// Adds the given relationship to linked list containing all equivalent relationship instances /// for this relationship (e.g. all orders associated with a specific customer) /// internal void AddToEquivalenceSet(DirectionalRelationship other) { Debug.Assert(null != other, "other must not be null"); Debug.Assert(this.Equals(other), "other must be another instance of the same relationship target"); Debug.Assert(Object.ReferenceEquals(other._equivalenceSetLinkedListNext, other), "other must not be part of an equivalence set yet"); DirectionalRelationship currentSuccessor = this._equivalenceSetLinkedListNext; this._equivalenceSetLinkedListNext = other; other._equivalenceSetLinkedListNext = currentSuccessor; } /// /// Returns all relationships in equivalence set. /// internal IEnumerable GetEquivalenceSet() { // yield everything in circular linked list DirectionalRelationship current = this; do { yield return current; current = current._equivalenceSetLinkedListNext; } while (!object.ReferenceEquals(current, this)); } /// /// Determines the number of add and delete operations contained in this equivalence set. /// internal void GetCountsInEquivalenceSet(out int addedCount, out int deletedCount) { addedCount = 0; deletedCount = 0; // yield everything in circular linked list DirectionalRelationship current = this; do { if (current.StateEntry.State == EntityState.Added) { addedCount++; } else if (current.StateEntry.State == EntityState.Deleted) { deletedCount++; } current = current._equivalenceSetLinkedListNext; } while (!object.ReferenceEquals(current, this)); } public override int GetHashCode() { return _hashCode; } public bool Equals(DirectionalRelationship other) { if (object.ReferenceEquals(this, other)) { return true; } if (null == other) { return false; } if (ToEntityKey != other.ToEntityKey) { return false; } if (AssociationSet != other.AssociationSet) { return false; } if (ToEnd != other.ToEnd) { return false; } if (FromEnd != other.FromEnd) { return false; } return true; } public override bool Equals(object obj) { Debug.Fail("use only typed Equals method"); return Equals(obj as DirectionalRelationship); } public override string ToString() { return String.Format(CultureInfo.InvariantCulture, "{0}.{1}-->{2}: {3}", AssociationSet.Name, FromEnd.Name, ToEnd.Name, StringUtil.BuildDelimitedList(ToEntityKey.EntityKeyValues, null, null)); } } #endregion } } }