ClearBeforeFill bei DB-Extensions

  • VB.NET
  • .NET (FX) 4.0

Es gibt 18 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    ClearBeforeFill bei DB-Extensions

    Hallo Community und @ErfinderDesRades

    Ich möchte bei der ParentTable einige Datensätze, die eine WHERE-Klausel erfüllen, von der DB laden.
    Dabei sollen alle ChildTables die mit der ParentTable verknüpft sind ihre Daten geladen bekommen.
    Wenn nur ein Parent-Datensatz geladen wird, sind auch die ChildTables ordentlich geladen.
    Aber bei Ladung mehrerer Parent-Datensätze sind in allen ChildTable nur mehr die zugeordneten Datensätze vom zuletzt geladenen Parent-Datensatz vorhanden.
    Hier funkt mir immer die ClearBeforeFill-Eigenschaft hinein, die intern auf True vor eingestellt ist und vor dem Füllvorgang die ChildTable leert...
    Gibt es hierzu eine andere Möglichkeit, damit alle geladenen Childdatensätze erhalten bleiben?
    Ich habe im DBExtensions-Framework dazu keine andere Methode finden können?

    VB.NET-Quellcode

    1. Public Sub LoadDatas(P1 As Integer, P2 As Integer, P3 As Integer)
    2. Dim ChildTables() As DataTable = {myDts.ChildTable1, myDts.ChildTable2, myDts.ChildTable3}
    3. With myDts
    4. .Save(Me, True)
    5. .ParentTable.Fill("WHERE P1=? AND P2=? AND P3=?", {P1, P2, P3}) 'hier ist das Leeren ok
    6. For Each r In .ParentTable
    7. r.FillChildTables(ChildTables) ' aber hier sollten die Daten in der Tabelle bleiben...
    8. Next
    9. End With
    10. End Sub


    Lg VB1963

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VB1963“ ()

    Das wäre ja auch nicht schön, für jede ParentRow ihre ChildRows abzurufen eine eigene Query an die DB zu schicken.
    Hier sollen 2 Tabellen abgefragt werden, also solls auch nicht mehr als 2 Queries geben.
    Dazu muss man listenreiches Sql schreiben, für die ParentTable mit einem Where-Abschnitt, und für die Child-Table muss man einen einen InnerJoin zur ParentTable bauen, und zusätzlich denselben Where-Abschnitt hinzufügen.
    Dann kann man diese Befüllung mit 2 Queries abhandeln.
    Oooh - du hast gleich 3 ChildTables mit-zu füttern!
    Da lohnt es sich glaub, ein Joiner-Objekt zu erzeugen, was dir dabei hilft, den richtigen InnerJoin-Sql-Code zu generieren. Das müsste glaub so laufen:

    VB.NET-Quellcode

    1. Public Sub LoadDatas(P1 As Integer, P2 As Integer, P3 As Integer)
    2. With myDts
    3. .Save(Me, True)
    4. Dim filter = "WHERE ParentTable.P1=? AND ParentTable.P2=? AND ParentTable.P3=?"
    5. .ParentTable.Fill(filter, P1, P2, P3)
    6. Dim joiner As New Joiner
    7. joiner.Add(.ChildTable1.ParentTableIDColumn)
    8. .ChildTable1.Fill(joiner.Sql & filter, P1, P2, P3)
    9. joiner.Clear()
    10. joiner.Add(.ChildTable2.ParentTableIDColumn)
    11. .ChildTable2.Fill(joiner.Sql & filter, P1, P2, P3)
    12. joiner.Clear()
    13. joiner.Add(.ChildTable3.ParentTableIDColumn)
    14. .ChildTable3.Fill(joiner.Sql & filter, P1, P2, P3)
    15. End With
    16. End Sub
    Also so ein Joiner kann beliebig viele Tabellen zusammenjoinen, wenn zw. diesen Tabellen DataRelations bestehen.
    Einzige Info, die er braucht ist die Fremdschlüsselspalte. Die zugehörige DataRelation findet er selbst, und dann hatter auch die PrimkeySpalte der ParentTable, und alle Infos zusammen, um den InnerJoin korrekt zu formulieren.
    Aber hier hängen ja jeweils nur 2 Tabellen zusammen, daher zwischendrin immer wieder clear().

    na - bin ich mal gespannt, ob du klarkommst :thumbup:

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Ich wusste es doch, das in den Tiefen der DBExtensions nocht etwas in diese Richtung schlummern wird - darum meine Frage. :)
    Dieses Joiner-Objekt habe ich mir jetzt genau angeschaut und es funzt! :thumbsup:
    Ich danke wieder für deine Hilfe. :thumbup:

    Eine Frage habe ich jetzt dazu:
    Was würde eigentlich dagegensprechen, wenn man die ClearBeforeFill-Eigenschaft bei der FillChildTables-Methode bis zur FillByParentRow-Function mit False einfach durchreicht (ist jetzt natürlich nicht mehr notwendig)?
    Ich habe das probiert (3 Überladungen) - das Datenfüllen bei den ChildTables funktionierte dann auch...

    Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „VB1963“ ()

    ich glaub, ich habs nur deshalb nicht gemacht, weil ich selbst es noch nie benötigt habe.
    Generell besteht bei Nicht-ClearbeforeFill die Gefahr, dass ein Primkey zum 2. Mal geladen wird und Peng.
    Kann man vlt. auch anners lösen - erst neulich hab ich zB DataTable.Merge für mich entdeckt.

    Der Joiner hat übrigens noch ein Schmankerl: Du kannst den Filter auch integrieren, statt ihn als Where-Klausel anhängen:

    VB.NET-Quellcode

    1. Public Sub LoadDatas(P1 As Integer, P2 As Integer, P3 As Integer)
    2. With myDts
    3. .Save(Me, True)
    4. Dim filter = "ParentTable.P1=? AND ParentTable.P2=? AND ParentTable.P3=?"
    5. .ParentTable.Fill("WHERE " & filter, P1, P2, P3)
    6. Dim joiner As New Joiner
    7. joiner.Add(filter, .ChildTable1.ParentTableIDColumn)
    8. .ChildTable1.Fill(joiner.Sql, P1, P2, P3)
    9. joiner.Clear()
    10. joiner.Add(filter, .ChildTable2.ParentTableIDColumn)
    11. .ChildTable2.Fill(joiner.Sql, P1, P2, P3)
    12. joiner.Clear()
    13. joiner.Add(filter, .ChildTable3.ParentTableIDColumn)
    14. .ChildTable3.Fill(joiner.Sql, P1, P2, P3)
    15. End With
    16. End Sub
    Das generiert effizienteres Sql, denn so werden nicht mehr alle Datensätze der ParentTable gejoint und hinterher gefiltert, sondern Filtern und Joinen sind eines, und wirken wie wenn die ParentTable erst gefiltert wird, und nur das Filtrat verjoint. Debug dir mal joiner.Sql.
    Man kann auch Filter der ChildTable mitgeben, muss man dann **nach** der Fremdschlüsselspalte als 2. Parameter angeben. Aber das geht in diesem Beispiel ja nicht auf.
    Hast recht, da muss man schon wissen, was man laden will und was geladen ist. Besser man leert all die Tables und füllt sie wieder definiert.
    Danke dir für diese weiteren Hinweise, was man mit dem Joiner-Objekt noch alles anstellen kann. Tolle Sache...
    Mir ist nur aufgefallen, dass man bei der integrierter Filtermethode, dann auf die Angabe der ParentTable verzichten muss!

    VB.NET-Quellcode

    1. Dim filter = "P1=? AND P2=? AND P3=?"
    Sonst generiert der Tableadapter beim Füllvorgang folgende OleDBException:
    'Für mindestens einen erforderlichen Parameter wurde kein Wert angegeben.'

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VB1963“ ()

    mir spuckt er da so etwas aus:
    1. Methode 'normal
    ([Parent] INNER JOIN [Child] ON ([Parent].[ID]=[Child].[ID])) diese verstehe ich...
    2. Methode 'integriert
    ([Parent] INNER JOIN [Child] ON ([Parent].P1=? AND P2=? AND P3=? AND [Parent].[ID]=[Child].[ID])) hier wird auch gleich mit den Parametern gejoint
    diese funzt - aber sollte da bei P2 und P3 nicht richtigerweise auch die ParentTable vorangestellt sein?
    Der Vollständigkeit halber:
    Die Joiner.Sql spuckt bei der 2. Methode (integriert) und mit der oben genannten OleDBException (also mit Angabe der ParentTable im Filter) dann folgendes aus
    ([Parent] INNER JOIN [Child] ON ([Parent].Parent.P1=? AND Parent.P2=? AND Parent.P3=? AND [Parent].[ID]=[Child].[ID]))
    jo - ein Bug.
    Entwickelt habich das nur anhand einfacher Vergleiche. Dass jmd auf die Idee kommt, gleich 3 Parameter abgleichen zu wollen ging über meinen Horizont.

    Mist :cursing:

    also zurechtwursteln kann man das vmtl. mit dem Filter

    VB.NET-Quellcode

    1. Dim filter = "P1=? AND ParentTable.P2=? AND ParentTable.P3=?"
    weil er den TabellenNamen nur einmalig voranstellt, und dabei nichtmal guckt, ob der Tabellenname nicht vlt. schon gegeben ist.
    so gehts glaub.
    Also die ColumnInfo-Klasse ist intellenter geworden, die macht nun eine richtige Analyse der angelieferten Filter:

    VB.NET-Quellcode

    1. Imports System.Text.RegularExpressions
    2. Namespace System.Data
    3. ''' <summary>
    4. ''' speichert eine Datacolumn, und ihre gequoteten Schreibweisen, und auch eine Expression: das kann eine in Sql formulierte Bedingung für diese Column sein.
    5. ''' </summary>
    6. Public Class ColumnInfo
    7. Public Col As DataColumn
    8. Public ColName, TableName, JoinExtension As String
    9. Private Shared _rgxIdentifier As New Regex("([`\[]?)\b[a-zA-Z]\w*[`\]]?(\.?)"), _knownColnames As New HashSet(Of String), _Quote As Func(Of String, String)
    10. Public Function Table() As DataTable
    11. Return Col.Table
    12. End Function
    13. Public Function RenameTable(ByVal newName As String) As String
    14. RenameTable = TableName.And(" AS ", newName)
    15. TableName = newName
    16. End Function
    17. Public Function Fullname() As String
    18. Return ".".Between(TableName, ColName)
    19. End Function
    20. Public Sub New(ByVal col As DataColumn, Optional ByVal quote As Func(Of String, String) = Nothing, Optional joinExtension As String = "")
    21. Me.Col = col
    22. _Quote = If(quote, col.Table.DataSet.Adapter.Quote)
    23. ColName = _Quote(col.ColumnName)
    24. TableName = _Quote(col.Table.TableName)
    25. If joinExtension.Length > 0 Then
    26. 'bei Schlüsselspalten kann für den Joiner ein zusätzliches Kriterium mitgegeben werden, was er auf dise Table anwendet.
    27. 'die SpaltenNamen innerhalb der JoinExtension werden identifiziert, mit Tabellen-Angabe vervollständigt und gequoted
    28. _knownColnames.Clear()
    29. col.Table.Columns.Cast(Of DataColumn).Select(Function(clm) _Quote(clm.ColumnName)).ForEach(AddressOf _knownColnames.Add)
    30. joinExtension = _rgxIdentifier.Replace(joinExtension, AddressOf QualifyIdentifier)
    31. End If
    32. Me.JoinExtension = joinExtension
    33. End Sub
    34. Private Function QualifyIdentifier(ByVal mt As Match) As String
    35. If mt.Groups(2).Length > 0 Then Return "" ' ggfs. bereits vorhandenen TableName-Qualifier weghauen
    36. Dim colName = If(mt.Groups(1).Length > 0, mt.Value, _Quote(mt.Value))
    37. If _knownColnames.Contains(colName) Then Return TableName.And(".", colName) ' TableName-Qualifier davorsetzen
    38. Return mt.Value
    39. End Function
    40. End Class
    41. End Namespace

    Joiner.Sql() hat nur eine winzige Änderung:

    VB.NET-Quellcode

    1. Public Function Sql() As String
    2. If Count = 0 Then Return ""
    3. 'joinedTables enthält die Tables, auf die ein InnerJoin sich beziehen kann, und dient der Erkennung von Self-Joins, bei denen eine Table umbenannt wern muss.
    4. Dim joinedTables = New HashSet(Of DataTable) From {Me(0)(0).Table}
    5. Dim EnsureJoinableColumnInfoPair As Action(Of Integer) = _
    6. Sub(iCurrent As Integer)
    7. 'ggfs. durch Swap gewährleisten, dass an iCurrent ein ColumnInfo-Pair ist, dessen erste Table in joinedTables aufgeführt ist. Dieses weil die Sql-InnerJoin-Klausel sich auf eine zuvor genannte Tabelle beziehen muss.
    8. For iTest = iCurrent To Count - 1
    9. Dim ColumnInfoPair = Me(iTest)
    10. For i = 0 To 1
    11. If joinedTables.Contains(ColumnInfoPair(0).Table) Then
    12. If iTest > iCurrent Then Swap(iTest, iCurrent)
    13. Return
    14. End If
    15. ColumnInfoPair.Swap(0, 1)
    16. Next
    17. Next
    18. Throw GetType(Joiner).Exception("can't find joinable dataRelation")
    19. End Sub
    20. Dim sqlParts = {New String("("c, Count), Me(0)(0).TableName}.ToList
    21. Dim counter As Counter = 0
    22. For i = 0 To Count - 1
    23. EnsureJoinableColumnInfoPair(i)
    24. Dim ColumnInfoPair = Me(i), toJoin = ColumnInfoPair(1)
    25. Dim joinedTableName = If(joinedTables.Add(toJoin.Table), toJoin.TableName, toJoin.RenameTable("X" & counter.Up))
    26. sqlParts.Add(" INNER JOIN ", joinedTableName, " ON (")
    27. For Each colInf In ColumnInfoPair.Where(Function(ci) ci.JoinExtension.HasValue)
    28. sqlParts.Add(colInf.JoinExtension.Trim, " AND ")
    29. Next
    30. sqlParts.Add(ColumnInfoPair(0).Fullname, "=", toJoin.Fullname, "))") 'Columns joinen
    31. Next
    32. Return String.Concat(sqlParts)
    33. End Function
    Aber wennde das einkopierst, haste noch paar Folgefehler, denn bei ColumnInfo.New hab ich die Reihenfolge der Parameter umgestellt, und die Filter-Angabe optional gemacht.
    Denn ColumnInfo ist für alle Columns da, aber nur bei Schlüsselspalten ists ggfs sinnvoll, Filter anzugeben.
    Aber die entsprechenden Aufrufe lassen sich leicht korrigieren.
    ups - da hab ich vereinfacht.
    Das ist eine Extension der List(Of String) - Klasse, die ein ParamArray items As Object() entgegennimmt, und die mit String.Concat verkettet.

    VB.NET-Quellcode

    1. <Extension()> _
    2. Public Sub Add(ByVal lst As List(Of String), ByVal ParamArray items As Object())
    3. lst.Add(String.Concat(items))
    4. End Sub
    sehr praktisch
    @ErfinderDesRades
    Jetzt kann man bei der Filterdefinition die ParentTable weglassen:

    VB.NET-Quellcode

    1. Dim filter = "P1=? AND P2=? AND P3=?"
    ...sieht übersichtlicher aus und funktioniert.
    Joiner.Sql generiert mit oder ohne der Angabe der ParentTable immer den gleichen Ausdruck...
    stimmt. Ist eine der Verbesserungen.
    Steht der Filterausdruck links der angegebenen ChildKeyColumn, so bezieht er sich auf die ParentTable, recht davon bezieht der Filter sich auf die ChildTable.
    Da das so ist, entfernt der Joiner evtl. im Filter angegebene TabellenNamen, und verwendet die Tabellennamen, die sich direkt aus dem Dataset ergeben.
    Genaugenommen ists das ColumnInfo, was die joinExtension verarbeitet und auf diese Weise in eine einheitliche und leidlich wasserdichte Form bringt.

    Ups - dabei nun glaub Bug der Rename-Funktion: Die muss jetzt auch einen StringReplace der JoiningExtension durchführen

    VB.NET-Quellcode

    1. Public Function RenameTable(ByVal newName As String) As String
    2. RenameTable = TableName.And(" AS ", newName)
    3. TableName = newName
    4. JoiningExtension = JoiningExtension.Replace(TableName &".", newName &".")
    5. End Function

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Diese Funktion wird bei meinem Fall gar nicht angesprungen.
    Das liegt, soweit ich den Joiner jetzt durchforstet habe, daran, dass ich nur ein einfaches Join vornehme.
    Ich fülle den Joiner nur einmal mit einer ColumnInfo und lösche den Eintrag nach dem Fill wieder...