Update of /cvsroot/jcframework/dotnet In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv24463 Modified Files: AToMSFramework.vbproj CAssociationObject.vb CClassMap.vb CInjectedObject.vb CPersistenceBroker.vb CPersistentObject.vb Added Files: CAssociationState.vb Log Message: Many-to-Many associations now working (for Inherited objects and XML mapping only at this stage). Also includes a number of other minor fixes. Index: AToMSFramework.vbproj =================================================================== RCS file: /cvsroot/jcframework/dotnet/AToMSFramework.vbproj,v retrieving revision 1.23 retrieving revision 1.24 diff -u -d -r1.23 -r1.24 --- AToMSFramework.vbproj 9 Feb 2005 06:24:44 -0000 1.23 +++ AToMSFramework.vbproj 11 Feb 2005 02:51:35 -0000 1.24 @@ -119,6 +119,11 @@ BuildAction = "Compile" /> <File + RelPath = "CAssociationState.vb" + SubType = "Code" + BuildAction = "Compile" + /> + <File RelPath = "CAssociationTable.vb" SubType = "Code" BuildAction = "Compile" Index: CPersistenceBroker.vb =================================================================== RCS file: /cvsroot/jcframework/dotnet/CPersistenceBroker.vb,v retrieving revision 1.93 retrieving revision 1.94 diff -u -d -r1.93 -r1.94 --- CPersistenceBroker.vb 10 Feb 2005 04:22:39 -0000 1.93 +++ CPersistenceBroker.vb 11 Feb 2005 02:51:35 -0000 1.94 @@ -551,7 +551,7 @@ End If 'Check whether the object is of child type - If fromClass.ChildrenMaps.Count > 0 Then + If toClass.ChildrenMaps.Count > 0 Then targetobj = Me.createTargetObjectForMultipleInheritance(toClass, obj.GetObjectType, obj.GetObjectType.Namespace, rs.ResultSet.Tables(0).Rows(i), joins, conn) 'update classMapCount with the child count number classMapCount += Me.getChildCountForMultipleInheritance(toClass) @@ -1150,6 +1150,7 @@ deletePrivateObject(Value, conn, True) End If End If + cm = clMap End If obj.Persistent = False @@ -2446,6 +2447,7 @@ cm2 = classMap While Not cm1 Is Nothing classMap.populateObject(cm1, obj, dataRow, joins.GetTableAlias(cm1), True, classMap) + 'classMap.populateObject(cm1, obj, dataRow, joins.GetTableAlias(cm1), True, cm1) cm2 = cm1 cm1 = cm1.SuperClass End While @@ -2588,10 +2590,6 @@ End If End If - 'If includeBaseObject Then Stack.Push(obj) - ''Clear the dirty flag to prevent recursion in the class structure causing an infinite loop - 'obj.IsDirty = False - 'Now process the object associations to see what else needs saving cm = obj.GetClassMap() @@ -2628,6 +2626,10 @@ col = obj.GetCollectionByAttribute(udamap.ToClassTarget) End If End If + 'To ensure consistency of the association we need to get a list of records in the mapping table + 'and check wether the record is still valid. Removal of an object from a collection + 'should mean that we have to delete the associationtable record. + Dim state As New CAssociationState(obj, udamap) If Not col Is Nothing Then For k = 0 To col.Count() - 1 tmpObj = col.Item(k) @@ -2637,6 +2639,7 @@ value = tmpObj End If aObj = Nothing + state.ValidateObject(value) For Each qObj In getObjectsToSave(value, True, checkAssociationsRecursivly) queue.Enqueue(qObj) If aObj Is Nothing AndAlso value.Equals(qObj) Then @@ -2651,6 +2654,7 @@ End If Next Next k + state.DoCleanUp() End If End If End If @@ -3085,4 +3089,49 @@ vbCrLf & ex.Message) End Try End Sub + + Public Shared Sub CopyCollections(ByVal fromObject As IPersistableObject, ByRef toObject As IPersistableObject) + Dim t, iEnumerableType, iListType, iDicType As Type Dim coll, collItem As Object + Dim il As IList + Dim id As IDictionary + Dim f, fields() As FieldInfo + Dim value As Object + + t = fromObject.GetObjectType + fields = t.GetFields(BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Public) + For Each f In fields + iListType = f.FieldType.GetInterface("IList", True) + iDicType = f.FieldType.GetInterface("IDictionary", True) + If Not iListType Is Nothing OrElse Not iDicType Is Nothing Then + Dim ICloneType As Type = f.FieldType.GetInterface("ICloneable", True) + If Not ICloneType Is Nothing Then + 'Getting the ICloneable interface from the object. + If Not f.GetValue(fromObject.GetSourceObject) Is Nothing Then + Dim IClone As ICloneable = CType(f.GetValue(fromObject.GetSourceObject), ICloneable) + coll = IClone.Clone() + Else + coll = Nothing + End If + Else + If Not f.GetValue(fromObject.GetSourceObject) Is Nothing Then + 'If the field doesn't support the ICloneable interface then just set it. + coll = Activator.CreateInstance(f.FieldType) 'need to copy references one-by-one + If Not iListType Is Nothing Then + il = CType(coll, IList) + For Each collItem In f.GetValue(fromObject.GetSourceObject) + il.Add(collItem) + Next + Else + id = CType(coll, IDictionary) + For Each de As DictionaryEntry In f.GetValue(fromObject.GetSourceObject) + id.Add(de.Key, de.Value) + Next + End If + Else + coll = Nothing + End If + End If + f.SetValue(toObject.GetSourceObject, coll) + End If Next + End Sub End Class \ No newline at end of file --- NEW FILE: CAssociationState.vb --- 'Class to record the current state of many-to-many associations and to handle 'changes to that state (such as objects being removed from collections) 'To use this class, instantiate it with an object 'Pass in objects in the many-to-many collection via the ValidateObject method 'Call the collectionCleanup cause the orphaned association table records to be deleted Friend Class CAssociationState Private m_toClassKeysCollection As New Collection Private m_fromObject As IPersistableObject Private m_association As CUDAMap Private m_fromClass As CClassMap Private m_toClass As CClassMap Public Property FromObject() As IPersistableObject Get Return m_fromObject End Get Set(ByVal Value As IPersistableObject) m_fromObject = Value If Not m_association Is Nothing Then If m_fromObject.GetClassMap.Name = m_association.FromClass.Name Then m_fromClass = m_association.FromClass m_toClass = m_association.ToClass Else m_fromClass = m_association.ToClass m_toClass = m_association.FromClass End If End If End Set End Property Public Property Association() As CUDAMap Get Return m_association End Get Set(ByVal Value As CUDAMap) If Value.Cardinality <> CUDAMap.CardinalityEnum.MANY_TO_MANY Then m_association = Nothing Exit Property End If m_association = Value If Not m_fromObject Is Nothing Then If m_fromObject.GetClassMap.Name = m_association.FromClass.Name Then m_fromClass = m_association.FromClass m_toClass = m_association.ToClass Else m_fromClass = m_association.ToClass m_toClass = m_association.FromClass End If End If End Set End Property Public Sub LoadState() If m_fromObject Is Nothing Then Exit Sub If m_association Is Nothing Then Exit Sub Dim db As CRelationalDatabase Dim key As CAssociationKey Dim fromKeys As CAssociationKeys Dim toKeys As CAssociationKeys Dim paramCount As Integer Dim conn As _CConnection If m_fromObject.GetClassMap.Name = m_association.FromClass.Name Then fromKeys = m_association.AssocationTable.FromKeys toKeys = m_association.AssocationTable.ToKeys Else toKeys = m_association.AssocationTable.FromKeys fromKeys = m_association.AssocationTable.ToKeys End If m_toClassKeysCollection = New Collection db = m_fromObject.GetClassMap.RelationalDatabase conn = db.getConnection(Nothing) 'To load the state we need to hit the database to determine what the records are in the 'association table that match the from class key values 'We will then store the toClass values as a collection of objects in the keys collection 'STEP 1. Build SQL for retrieving the records Dim statement As New CSqlStatement statement.addSqlClause(db.getClauseStringSelect) statement.addSqlClause(" ") Dim isFirst As Boolean = True Dim i As Short For i = 0 To toKeys.Count - 1 If isFirst Then isFirst = False Else statement.addSqlClause(", ") End If key = toKeys(i) statement.addSqlClause(key.ColumnMap.getFullyQualifiedName) Next i 'Add the FROM clause to the statement statement.addSqlClause(" " & db.getClauseStringFrom & " ") statement.addSqlClause(m_association.AssocationTable.Name) statement.addSqlClause(" " & db.getClauseStringWhere & " ") 'Add the clause "WHERE key=?" to the selectStatement paramCount = 1 isFirst = True For i = 0 To fromKeys.Count - 1 key = fromKeys(i) If Not isFirst Then statement.addSqlClause(" " & db.getClauseStringAnd & " ") End If isFirst = False statement.addSqlClause(key.ColumnMap.getFullyQualifiedName & db.getClauseStringEqualTo("_" & paramCount.ToString & "_")) statement.addSqlParameter(paramCount, m_fromObject.GetValueByAttribute(key.AttributeMap.Name), key.ColumnMap) paramCount += 1 Next i Dim x As String = statement.SqlString Dim p As CSQLParameter For i = 1 To paramCount - 1 p = statement.Parameters.Item(i) If IsNullAlias(p.Value) Then If db.UseANSINulls = True Then x = x.Replace(" = _" & i.ToString & "_", " is NULL") p.Ignore = True Else x = x.Replace("_" & i.ToString & "_", db.getParamHolder(i)) End If Else x = x.Replace("_" & i.ToString & "_", db.getParamHolder(i)) End If Next statement.SqlString = x 'STEP 2. Execute Statement and Create CacheKeys Dim rs As CResultset Try getPersistenceBrokerInstance.PCSQLHits.Increment() Catch End Try rs = conn.processSelectStatement(statement) Debug.WriteLine("Select statement returned " & rs.ResultSet.Tables(0).Rows.Count & " row(s)") If rs.ResultSet.Tables(0).Rows.Count = 0 Then 'No records means nothing to do so lets leave Return End If For Each rw As DataRow In rs.ResultSet.Tables(0).Rows m_toClassKeysCollection.Add(createKey(rw, toKeys)) Next End Sub Private Function createKey(ByVal rw As DataRow, ByVal keys As CAssociationKeys) As Collection Dim ck As New Collection Dim tmpObj As Object Dim AttrMap As CAttributeMap Dim i As Integer For i = 0 To keys.Count - 1 'Attempt to load column via alias first, then table qualified name then column name tmpObj = Nothing Try If rw.Table.Columns.Contains(keys(i).ColumnMap.Name) Then tmpObj = rw.Item(keys(i).ColumnMap.Name) End If Catch ex As Exception End Try ck.Add(tmpObj) Next i Return ck End Function Private Function createKey(ByVal obj As IPersistableObject, ByVal keys As CAssociationKeys) As Collection Dim ck As New Collection Dim tmpObj As Object Dim AttrMap As CAttributeMap Dim i As Integer For i = 0 To keys.Count - 1 AttrMap = keys(i).AttributeMap tmpObj = Nothing Try tmpObj = obj.GetObjectByAttribute(AttrMap.Name) Catch ex As Exception End Try ck.Add(tmpObj) Next i Return ck End Function Public Sub ValidateObject(ByVal toObject As IPersistableObject) 'Now we want to find a collection entry which has a matching key signature to the 'passed object. 'This will indicate that the association is still valid, and that we can remove If m_association Is Nothing OrElse m_fromObject Is Nothing OrElse toObject Is Nothing Then Return End If Dim Keys As CAssociationKeys If m_fromObject.GetClassMap.Name = m_association.FromClass.Name Then Keys = m_association.AssocationTable.FromKeys Else Keys = m_association.AssocationTable.ToKeys End If Dim objKey As Collection = createKey(toObject, Keys) Dim col As Collection Dim indexToRemove As Integer Dim found As Boolean For n As Integer = 1 To m_toClassKeysCollection.Count col = m_toClassKeysCollection(n) found = True For i As Integer = 1 To col.Count If Not col(i).Equals(objKey(i)) Then found = False Exit For End If Next If found Then indexToRemove = n Exit For Next If found Then m_toClassKeysCollection.Remove(indexToRemove) End If End Sub Public Sub DoCleanUp() 'Remove records for anything left in the keys collection Dim col As Collection 'Need to build the main delete statement then call it multiple times 'changing the where clause each time. Dim isFirst As Boolean Dim db As _CRelationalDatabase Dim m_deleteStatement As New CSqlStatement Dim m_actualStatement As CSqlStatement Dim paramCount, fromOffset As Integer Dim AttrMap As CAttributeMap Dim Key As CAssociationKey Dim fromKeys, toKeys As CAssociationKeys Dim i As Short Dim conn As _CConnection If m_fromObject Is Nothing Then Return End If If m_toClassKeysCollection.Count = 0 Then Exit Sub End If db = m_fromObject.GetClassMap.RelationalDatabase conn = db.getConnection(Nothing) 'Determine which order classes are in If m_fromObject.GetClassMap.Name = m_association.FromClass.Name Then fromKeys = m_association.AssocationTable.FromKeys toKeys = m_association.AssocationTable.ToKeys Else toKeys = m_association.AssocationTable.FromKeys fromKeys = m_association.AssocationTable.ToKeys End If m_deleteStatement.addSqlClause(db.getClauseStringDelete & " " & db.getClauseStringFrom & " ") m_deleteStatement.addSqlClause(m_association.AssocationTable.Name & " ") m_deleteStatement.addSqlClause(db.getClauseStringWhere & " ") paramCount = 1 isFirst = True For i = 0 To fromKeys.Count - 1 Key = fromKeys(i) If Not isFirst Then m_deleteStatement.addSqlClause(" " & db.getClauseStringAnd & " ") End If isFirst = False m_deleteStatement.addSqlClause(Key.ColumnMap.getFullyQualifiedName & db.getClauseStringEqualTo("_" & paramCount.ToString & "_")) m_deleteStatement.addSqlParameter(paramCount, m_fromObject.GetValueByAttribute(Key.AttributeMap.Name), Key.ColumnMap) paramCount += 1 Next i fromOffset = paramCount - 1 For i = 0 To toKeys.Count - 1 Key = toKeys(i) If Not isFirst Then m_deleteStatement.addSqlClause(" " & db.getClauseStringAnd & " ") End If isFirst = False m_deleteStatement.addSqlClause(Key.ColumnMap.getFullyQualifiedName & db.getClauseStringEqualTo("_" & paramCount.ToString & "_")) paramCount += 1 Next i For n As Integer = 1 To m_toClassKeysCollection.Count m_actualStatement = m_deleteStatement col = m_toClassKeysCollection(n) For i = 1 To toKeys.Count m_actualStatement.addSqlParameter(fromOffset + i, col.Item(i), toKeys(i - 1).ColumnMap) Next Dim x As String = m_actualStatement.SqlString Dim p As CSQLParameter For i = 1 To paramCount - 1 p = m_actualStatement.Parameters.Item(i) If IsNullAlias(p.Value) Then If db.UseANSINulls = True Then x = x.Replace(" = _" & i.ToString & "_", " is NULL") p.Ignore = True Else x = x.Replace("_" & i.ToString & "_", db.getParamHolder(i)) End If Else x = x.Replace("_" & i.ToString & "_", db.getParamHolder(i)) End If Next m_actualStatement.SqlString = x Try 'May have already been deleted by other side of association conn.processStatement(m_actualStatement) Catch ex As Exception End Try Next End Sub Public Sub New(ByVal obj As IPersistableObject, ByVal udamap As CUDAMap) m_fromObject = obj m_association = udamap LoadState() End Sub End Class Index: CAssociationObject.vb =================================================================== RCS file: /cvsroot/jcframework/dotnet/CAssociationObject.vb,v retrieving revision 1.2 retrieving revision 1.3 diff -u -d -r1.2 -r1.3 --- CAssociationObject.vb 10 Feb 2005 04:22:39 -0000 1.2 +++ CAssociationObject.vb 11 Feb 2005 02:51:35 -0000 1.3 @@ -305,4 +305,5 @@ Next End If End Sub + End Class Index: CClassMap.vb =================================================================== RCS file: /cvsroot/jcframework/dotnet/CClassMap.vb,v retrieving revision 1.49 retrieving revision 1.50 diff -u -d -r1.49 -r1.50 --- CClassMap.vb 9 Feb 2005 06:24:44 -0000 1.49 +++ CClassMap.vb 11 Feb 2005 02:51:35 -0000 1.50 @@ -1406,7 +1406,8 @@ skipAttribute = False If checkForDuplicateAttributes Then Try - If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, False) Is Nothing Then + 'If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, False) Is Nothing Then + If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, True) Is Nothing Then skipAttribute = True End If Catch @@ -1500,7 +1501,8 @@ skipAttribute = False If checkForDuplicateAttributes Then Try - If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, False) Is Nothing Then + 'If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, False) Is Nothing Then + If Not classMapToCheck.getAttributeMapByString(AttrMap.Name, True) Is Nothing Then skipAttribute = True End If Catch Index: CInjectedObject.vb =================================================================== RCS file: /cvsroot/jcframework/dotnet/CInjectedObject.vb,v retrieving revision 1.11 retrieving revision 1.12 diff -u -d -r1.11 -r1.12 --- CInjectedObject.vb 9 Feb 2005 06:24:44 -0000 1.11 +++ CInjectedObject.vb 11 Feb 2005 02:51:35 -0000 1.12 @@ -550,12 +550,11 @@ End Function Public Function Copy() As IPersistableObject Implements IPersistableObject.Copy - 'Dim m_object2 As Object Dim injobj As CInjectedObject injobj = CType(Me.MemberwiseClone, CInjectedObject) - 'm_object2 = Activator.CreateInstance(m_object.GetType) - 'ReplaceValues(m_object, m_object2) - 'injobj = New CInjectedObject(m_object2) + 'Because memberwise clone only reference copies collections we should also + 'copy the collections as well, since failing to do so can corrupt the cache. + CPersistenceBroker.CopyCollections(Me, injobj) Return injobj End Function Index: CPersistentObject.vb =================================================================== RCS file: /cvsroot/jcframework/dotnet/CPersistentObject.vb,v retrieving revision 1.53 retrieving revision 1.54 diff -u -d -r1.53 -r1.54 --- CPersistentObject.vb 9 Feb 2005 06:24:44 -0000 1.53 +++ CPersistentObject.vb 11 Feb 2005 02:51:37 -0000 1.54 @@ -1237,7 +1237,11 @@ ''' </history> '''----------------------------------------------------------------------------- Public Overridable Function Copy() As IPersistableObject Implements IPersistentObject.Copy - Return Me.MemberwiseClone + Dim obj As CPersistentObject + obj = CType(Me.MemberwiseClone, CPersistentObject) + 'Because memberwise clone only reference copies collections we should also + 'copy the collections as well, since failing to do so can corrupt the cache. + CPersistenceBroker.CopyCollections(Me, obj) Return obj End Function '''----------------------------------------------------------------------------- |