Gedankenknoten im Datenmodell auflösen, Schnittmenge aus zwei Datatables bilden

  • VB.NET

Es gibt 14 Antworten in diesem Thema. Der letzte Beitrag () ist von VaporiZed.

    Gedankenknoten im Datenmodell auflösen, Schnittmenge aus zwei Datatables bilden

    Hallo Forum,

    ich habe bisher überwiegend mit überschaubaren Projekten im Bereich von VBA (Excel) zu tun gehabt. Seit einigen Wochen befasse ich mit einer kleinen Personalverwaltung, in der man dann später auch z.B. Arbeitszeiten erfassen soll. Die Ausführungen und Video-Tutorials vom "Erfinder des Rades" waren für mich ein sehr hilfreicher Einstieg in die Materie, im Grunde genommen bauen meine Gehversuche komplett auf dem Beispiel-Layout/-Code auf.

    Grund dieses Posts ist, dass ich ungern spaghetticoden möchte, weswegen ich einfach mal ganz zu Beginn zwei Fragen habe:
    1. Erfüllt das von mir erstellte Datenmodell überhaupt meine Anforderungen, bzw. ist es die logischste Art mein Wunsch-Setup abzubilden?
    2. Sofern Punkt1 abgehakt werden kann: Wie kann ich aus zwei Datatables ohne Spaghetticode eine neue Datatable erhalten, welche eine definierte Schnittmenge enthält?

    # Setup / Meine Anforderungen
    - Es gibt viele Mitarbeiter, die jeweils einige Eigenschaften haben (dtMitarbeiter)
    - Es gibt mehrere Betriebe/Filialen (dtBetriebe)
    - Es gibt mehrere Arbeitsbereiche (dtArbeitsbereiche)
    Dabei soll (u.a.) gelten:
    - Ein Mitarbeiter gehört zu einem (Stamm)Betrieb und er gehört einem (Stamm)Arbeitsbereich an, aber insgesamt kann er außerdem in weiteren X Betrieben arbeiten, und auch je Betrieb in X Arbeitsbereichen eingesetzt werden.

    # Umsetzung
    Deswegen habe ich mir die beiden Datatables "dtMAArbeitsbereiche" und "dtMABetriebe" erzeugt und von diesen dann Relationen zu jeweils dtMitarbeiter und dtArbeitsbereiche bzw. dtBetriebe.

    Mir scheint, damit bin ich nicht auf dem Holzweg, denn in meiner Mitarbeiter-Detailansicht habe ich zwei Datagridviews die dtMAArbeitsbereicheBindingSource & dtMABetriebeBindingSource darstellen. Je Mitarbeiter erhalte ich somit die zugeordneten Betriebe und Arbeitsbereiche.

    # Fragestellung
    Jetzt benötige ich aber quasi den umgekehrten Fall: Ich benötige Auflistung aller Mitarbeiter die einem Betrieb X zugeordnet sind und außerdem in einem Arbeitsbereich Y arbeiten dürfen. Betrieb X und Arbeitsbereich Y sind dabei nicht Stammbetrieb und Stammarbeitsbereich, welches direkte Eigenschaften des Mitarbeiters wären. Betrieb X und Arbeitsbereich Y würde der Anwender z.B. über zwei Comboboxen als "Filter" auswählen, und dann als Ergebnis alle zutreffenden Mitarbeiter erhalten.

    Ich wäre sehr dankbar, wenn mir jemand ein kurzes Feedback zum Datenmodell und mir einen Denkanstoß bezüglich des zu bildenden Schnittmengendatensatzes geben könnte.
    Bilder
    • 01 Datenmodell.JPG

      64 kB, 807×383, 58 mal angesehen
    • 02 Mitarbeiterübersicht.JPG

      74,31 kB, 1.113×687, 68 mal angesehen
    • 03 Mitarbeiterdetailansicht.JPG

      78,39 kB, 1.113×691, 63 mal angesehen

    spaghetticoder schrieb:

    Ein Mitarbeiter gehört zu einem (Stamm)Betrieb und er gehört einem (Stamm)Arbeitsbereich an, aber insgesamt kann er außerdem in weiteren X Betrieben arbeiten, und auch je Betrieb in X Arbeitsbereichen eingesetzt werden.
    nun, das sind 4 Zuordnungen, deren letzte beiden du mit den m:n-Relation bereits richtig modelliert hast.
    Fehlen nur noch die beiden 1:n:
    • MA -> Arbeitsbereich
    • MA -> Betrieb
    Die sind ja auch einfach, dafür brauchste nicht extra eine ZwischenTabelle einzuführen - je ein zusätzlicher FK in Mitarbeiter reicht, um die Relationen zu setzen.



    spaghetticoder schrieb:

    Ich benötige Auflistung aller Mitarbeiter die einem Betrieb X zugeordnet sind und außerdem in einem Arbeitsbereich Y arbeiten dürfen. Betrieb X und Arbeitsbereich Y sind dabei nicht Stammbetrieb und Stammarbeitsbereich
    Das wird komplizierter, allein in Designern ist das nicht zu lösen.
    Sondern da muss, wenn Betrieb und Arbeitsbereich gewählt sind, schlauer Code ausgeführt werden, und die betroffenen MAs zusammensuchen.
    Vielleicht ist sinnvoll, diese Anforderung in einem eigenen UserControl anzusiedeln, welches du dann auf ein TabControl-Tab setzst.
    (dazu sollteste aber meine DatasetHelpers einbinden, denn die unterstützen Databinding auch in UserControls hinein (was andernfalls ein Theater ist)).

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

    vielen Dank schon einmal für die Rückmeldung bzgl. des Datenmodells. An die 1:n habe ich gar nicht gedacht, habe sie bisher einfach über eine Combobox mit Databinding im DGV abgebildet. Es schadet aber bestimmt nicht, hier auch die entsprechende Relation zu erstellen.

    ErfinderDesRades schrieb:

    Das wird komplizierter, allein in Designern ist das nicht zu lösen.
    Sondern da muss, wenn Betrieb und Arbeitsbereich gewählt sind, schlauer Code ausgeführt werden, und die betroffenen MAs zusammensuchen.
    Vielleicht ist sinnvoll, diese Anforderung in einem eigenen UserControl anzusiedeln, welches du dann auf ein TabControl-Tab setzst.
    (dazu sollteste aber meine DatasetHelpers einbinden, denn die unterstützen Databinding auch in UserControls hinein (was andernfalls ein Theater ist)).


    Bzgl. UserControl / TabControl: Werde ich so umsetzen. Wo finde ich die DatasetHelpers? Einfach Forum durchsuchen?

    @"schlauer Code":
    Ich habe gestern Abend noch mit linq experimentiert. Herausgekommen ist dieser Codeschnipsel, der zumindest als Ergebnis das von mir gesuchte liefert:

    VB.NET-Quellcode

    1. Dim dtBetriebe = DsMADly.dtBetriebe
    2. Dim dtArbeitsbereiche = DsMADly.dtArbeitsbereiche
    3. Dim dtMABetriebe = DsMADly.dtMABetriebe
    4. Dim dtMAArbeitsbereiche = DsMADly.dtMAArbeitsbereiche
    5. Dim dtMitarbeiter = DsMADly.dtMitarbeiter
    6. 'Die Gedanken hinter diesem Beispiel:
    7. 'Ich will alle Mitarbeiter "filtern", die im Betrieb "X"
    8. '(ID des Betriebes=Value einer Combobox mit Databinding auf dtBetriebe) und
    9. 'im Arbeitsbereich "Y" (ID des Arbeitsbereiches=Value einer
    10. 'ComboBox mit Databinding auf dtArbeitsbereiche) eingesetzt werden können.
    11. 'Die gedanklichen Bedingungen dazu lauten:
    12. '1. Mitarbeiter-ID in dtMitarbeiter und dtMAArbeitsbereiche muss übereinstimmen
    13. '2. in dtMAArbeitsbereiche muss die ID=cbBereich.selectedValue sein
    14. '3. Mitarbeiter-ID in dtMitarbeiter und dtMABetriebe muss übereinstimmen
    15. '4. in dtMABetriebe muss die ID=cbBetrieb.selectedValue sein
    16. '5. die Betrieb-ID in dtBetriebe und dtMABetriebe muss übereinstimmen
    17. '6. die Bereich-ID in dtArbeitsbereiche und dtMAArbeitsbereiche muss übereinstimmen
    18. Dim dieauserwaehlten = From
    19. mitarbeiter In dtMitarbeiter.AsEnumerable,
    20. ma_arbeitsbereiche In dtMAArbeitsbereiche.AsEnumerable,
    21. ma_betriebe In dtMABetriebe.AsEnumerable,
    22. betrieb In dtBetriebe.AsEnumerable,
    23. bereich In dtArbeitsbereiche.AsEnumerable
    24. Where
    25. mitarbeiter.ID = ma_arbeitsbereiche.IDMitarbeiter _
    26. And ma_arbeitsbereiche.IDArbeitsbereiche = cbBereichFilter.SelectedValue _
    27. And mitarbeiter.ID = ma_betriebe.IDMitarbeiter _
    28. And ma_betriebe.IDBetriebe = cbBetriebfilter.SelectedValue _
    29. And betrieb.ID = ma_betriebe.IDBetriebe _
    30. And bereich.ID = ma_arbeitsbereiche.IDArbeitsbereiche
    31. Select
    32. mitarbeiter.Name,
    33. vorname = mitarbeiter.Vorname,
    34. betrieb = betrieb.Betrieb,
    35. bereich = bereich.Arbeitsbereich_Kurz
    36. For Each element In dieauserwaehlten
    37. MsgBox(element.Name & ", " & element.vorname & ": " & element.betrieb & "/" & element.bereich)
    38. Next


    1. Kann man es so machen oder gibt es Probleme auf die ich auf diese Art und Weise stoßen werde?
    2. Kann man es besser/schöner machen?
    DatasetHelpers

    spaghetticoder schrieb:

    Auflistung aller Mitarbeiter die einem Betrieb X zugeordnet sind und außerdem in einem Arbeitsbereich Y arbeiten dürfen

    VB.NET-Quellcode

    1. dim choosenMAs = dtMitarbeiter.Where(Function(ma) betriebX.GetMitarbeiterRows.Contains(ma) AndAlso bereichY.GetMitarbeiterRows.Contains(ma))
    2. dgv.DataSource=choosenMAs.AsDataView
    Siehe hierzu auch: LinqToDataset

    Beachte, dass die Methoden betriebX.GetMitarbeiterRows(), ArbeitsbereichY.GetMitarbeiterRows() nur dann generiert werden, wenn du die Relation eingebaut hast.

    ah - oder vlt. einfacher:

    VB.NET-Quellcode

    1. dim choosenMAs = dtMitarbeiter.Where(Function(ma) ma.BetriebRow is betriebX AndAlso ma.BereichRow is bereichY)
    2. dgv.DataSource=choosenMAs.AsDataView

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

    ErfinderDesRades schrieb:

    ah - oder vlt. einfacher:


    Also den einfachen Vorschlag habe ich umgesetzt bekommen. Aber: Nur mit Modifikationen ("=" statt "is". "ma.Stammbetrieb" statt "ma.StammbetriebRow"). Mache ich methodisch etwas falsch, oder warum muss ich meine Syntax ändern? Und: Mit dieser Abfrage kann ich doch nur "Stammbetrieb" und "Stammarbeitsbereich", also direkte Eigenschaften des MAs filtern? Die "Schnittmenge" aus dtMitarbeiter + dtMAArbeitsbereich + dtMABetriebe bekomme ich so nicht umgesetzt, oder verstehe ich es nicht?

    VB.NET-Quellcode

    1. Dim choosenMAs = dtMitarbeiter.Where(Function(ma) ma.Stammbetrieb = cbBetriebfilter.SelectedValue AndAlso ma.Stammarbeitsbereich = cbBereichFilter.SelectedValue)
    2. dgvUebersicht.DataSource = choosenMAs.AsDataView
    Kurzer Querulanten-Einwurf: Bevor Du weitermachst, bitte die empfohlenen VS-Einstellungen verwenden. Stichwort MsgBox (VB6) und ma.Stammbetrieb = cbBetriebfilter.SelectedValue (Option Strict Off), da SelectedValue vom Typ Object ist. Aber da hast Du recht, es muss Stammbetrieb (also die Objektinstanz) stehen, nicht StammbetriebRow (Objektdatentyp).
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    spaghetticoder schrieb:

    oder verstehe ich es nicht?
    zunächstmal: Die Vergleiche müssen mit Is funktionieren, und auch mit den gezeigten Properties, keinen anderen.
    Ich hab das an einem typDataset bei mir überprüft, und was ich gezeigt habe entspricht dem Schema, wie ein DatasetDesigner die typisierten DataRow-Klassen generiert - so und nicht anders.
    Wenn das also so nicht durch deinen Compiler geht, dann ist das Dataset allerhöchstwahrscheinlich anders gebaut als es sollte. Insbesondere dürften die Relationen fehlen oder falsch konfiguriert. Ich empfehle immer alle 3 Einstellungen:
    Beziehung + ForeignkeyConstraint + Löschweitergabe.
    siehe auch
    vier Views-Videos

    Ansonsten haste leider recht - meine einfache Variante ist zu einfach.
    Aber jetzt keine Zeit das zu korrigieren.

    VaporiZed schrieb:

    .. Bevor Du weitermachst, bitte die empfohlenen VS-Einstellungen verwenden.

    Danke für den querulatorischen Einwurf und das Feedback. Lektüre ist vorgemerkt.

    ErfinderDesRades schrieb:

    zunächstmal: Die Vergleiche müssen mit Is funktionieren, und auch mit den gezeigten Properties, keinen anderen...
    ..dann ist das Dataset allerhöchstwahrscheinlich anders gebaut als es sollte. Insbesondere dürften die Relationen fehlen oder falsch konfiguriert. I..


    Habe ich mein Datenmodell vielleicht falsch vorgestellt? Mein Aufbau ist dieser:

    VB.NET-Quellcode

    1. 'Meine Dataset-Struktur:
    2. '<Dataset = dsMadly>
    3. ' im Dataset diverse Datatables, u.a.
    4. ' <dtMitarbeiter>
    5. ' <dtBetriebe>
    6. ' <dtArbeitsbereiche>
    7. ' <dtMABetriebe>
    8. ' <dtMAArbeitsbereiche>
    9. '</dsMadly>
    10. 'Hier dieser Fehler: "StammbetriebRow ist kein Member von dsMADly.dtMitarbeiterRow":
    11. Dim chosenMAs = DsMADly.dtMitarbeiter.Where(Function(ma) ma.StammbetriebRow Is "1")
    12. 'Hier dieser Fehler: "Der Is-Operator akzeptiert keine Operanden vom Typ Integer":
    13. Dim chosenMAs2 = DsMADly.dtMitarbeiter.Where(Function(ma) ma.Stammbetrieb Is "1")


    Die Profis hier erkennen doch anhand der Fehlermeldung sofort was ich da verbockt habe, oder?
    "StammbetriebRow ist kein Member von dsMADly.dtMitarbeiterRow"
    "Der Is-Operator akzeptiert keine Operanden vom Typ Integer"
    Is wird für Objektinstanzvergleiche hergenommen, um zu schauen, ob die beiden Objekte links und rechts vom Is dasselbe Objekt sind (nicht das gleiche!). "1" ist aber ein Wert vom Typ String. Für Wertevergleiche wird = hergenommen. Aber wie kann eine DataRow einen Wert haben? Eine Row ist ein Objekt. Vielleicht täusche ich mich und der Screenshot aus Post#1 ist nicht mehr aktuell. Aber welche Werte kann denn ma.Stammbetrieb haben? Von welchem Datentyp ist Stammbetrieb? String? Integer? Was anderes?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Es gehört zum Dilemma des Amateurs, dass er ein Problem damit hat, sein Problem begrifflich korrekt zu beschreiben. :whistling:
    Ich versuche es mal:
    • Stammbetrieb sowie Stammarbeitsbereich sind jeweils vom Typ Integer.
    • Zwischen dtMitarbeiter (welche die StammbetriebColumn sowie StammarbeitsbereichColumn beinhaltet) und dtArbeitsbereiche und dtBetriebe besteht eine 1:n-Beziehung.
    • Die für mich aber interessantere Beziehung ist die m:n-Beziehung (1 Mitarbeiter kann in x Betrieben arbeiten), z.B. dtMABetriebe
    ​Du antwortest ja bereits weiter oben "Aber da hast Du recht, es muss Stammbetrieb (also die Objektinstanz) stehen, nicht StammbetriebRow (Objektdatentyp).". Mich verwirrt nur die gegenteilige Aussage vom ErfinderDesRades.

    Der weiter oben von mir gepostete Code bildet ja bereits ab, was ich erhalten möchte. Nur möchte ich gern mal die spaghetticoderei auf ein Minimum reduzieren.
    Ok, wenn Stammbetrieb vom Typ Integer ist, dann musst Du mit Option Strict On schreiben: .Where(Function(ma) ma.Stammbetrieb = 1). Aber: Wie kann ein Betrieb eine 1 sein? Einen Namen/String würde ich ja noch verstehen. Näher würde eine BetriebsID liegen. Aber Stammbetrieb ein Integer? Das verstehe wer will ...
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    VaporiZed schrieb:

    Aber: Wie kann ein Betrieb eine 1 sein? Einen Namen/String würde ich ja noch verstehen. Näher würde eine BetriebsID liegen. Aber Stammbetrieb ein Integer? Das verstehe wer will ...


    Es ist doch ganz einfach! ;)
    Die Betriebe sind alle in meiner dtBetriebe erfasst. Da hat natürlich jeder Betrieb neben der ID auch z.B. einen Namen.
    Die dtMitarbeiter, dargestellt in einem DGV, stellt den zugeordneten Betrieb über eine Combobox dar (Databinding zu dtBetriebe, DisplayMember = BetriebName, ValueMember = BetriebID). Zumindest in meiner Vorstellung ergab diese Herangehensweise Sinn.
    Ja, vielleicht ackerst du mal dieses Tut durch: codeproject.com/Articles/10331…eginners#Model-Guidelines
    da werden ausführliche und durchdachte Empfehlungen (auch) für BenamungsRichtlinien gegeben.
    Ich vermute, dass dtMitarbeiterRow.StammBetrieb eine ForeignKey-Spalte auf dtBetriebe.ID ist.
    Daraus würde das typDataset dann die Property

    VB.NET-Quellcode

    1. dtMitarbeiterRow.dtBetriebeRow
    generieren - wenn da tatsächlich eine Relation eingerichtet ist.
    In meinem Beispiel habich einfach eine verbesserte Benamung vorrausgesetzt - ohne Prefixe, und singular flexiert, und dann kommt eben sowas raus:

    VB.NET-Quellcode

    1. dim choosenMAs = dtMitarbeiter.Where(Function(ma) ma.BetriebRow is betriebX AndAlso ma.ArbeitsbereichRow is bereichY)
    wobei betriebX vom Typ BetriebRow ist, und bereichY vom Typ ArbeitsBereichRow.

    Mit deiner suboptimalen Benamsung müsste man schreiben:

    VB.NET-Quellcode

    1. dim choosenMAs = dtMitarbeiter.Where(Function(ma) ma.dtBetriebeRow is betriebX AndAlso ma.dtArbeitsbereicheRow is bereichY)
    Wie gesagt: Wenn die angenommene Relation existiert, muss das so kompilieren.
    Wie ebenfalls gesagt täte ich eine optimale Benamung empfehlen, weil von dtArbeitsbereicheRow und solchen Dingen bekommt man Augenkrebs.

    Und ein ForeignKey sollte benamt sein mit ZielTabelle+'ID'.
    Also wenn deine BetriebTabelle schlicht Betrieb heisst, und deine MitarbeiterTabelle schlicht 'MItarbeiter', und der ForeignKey von klar und deutlich 'BetriebID', dann funzt mein Snippet so schlank und schön wie gedacht:

    VB.NET-Quellcode

    1. dim choosenMAs = dtMitarbeiter.Where(Function(ma) ma.BetriebRow is betriebX AndAlso ma.ArbeitsbereichRow is bereichY)
    Und es kann keine Verwechslung eines Integer-ForeignKeys mit einer typisierten Datarow auftreten.
    So! Ich muss nochmal sagen, dass mir all die Tipps der Profis, die Videos und die verlinkten Tutorials sehr helfen. Da dieses Projekt mal möglichst ordentlich werden soll, habe ich in den letzten Tagen dies getan:
    - Zuerst habe ich mal mit der Eliminierung der "Deppeneinstellungen" begonnen, zumindest die "Option Strict On" ist aktiv und mein Code entsprechend angepasst. Bis auf an einer Stelle (s.u.) bin ich auch ohne Konvertierungen hingekommen.
    - Dann habe ich mich um eine augenkrebsige Benamsung gekümmert: Es gibt nun das Dataset MADlyDts, sowie die Datatables "Mitarbeiter", "Arbeitsbereich", "Betrieb" sowie "MAArbeitsbereich" und "MABetrieb". ForeignKeys (z.B. in Mitarbeiter auf Betrieb) heißen nun schön "BetriebID", "ArbeitsbereichID" etc.
    - meinen Modellierungsfehler (fehlende Relation) habe ich angepasst, nun heißt es knackig:

    VB.NET-Quellcode

    1. Dim chosenMAs = MADlyDts.Mitarbeiter.Where(Function(ma) ma.BetriebID = CInt(cbBetriebfilter.SelectedValue))
    2. dgvUebersicht.DataSource = chosenMAs.AsDataView


    Aber: Mal wieder wusste ich mir nicht anders zu helfen, als wie dargestellt. (wieso muss ich überhaupt den SelectedValue konvertieren? Ich verwende doch ein Databinding und der ValueMember "ID" ist im Designer als Integer definiert worden. Aber der Compiler meckert sonst).

    wobei betriebX vom Typ BetriebRow ist,..

    Wie gehe ich denn richtig vor, wenn ich eine ComboBox habe (cbBetriebfilter, Databinding zu BetriebBindingSource, DisplayMember=Betrieb("Name"), ValueMember = Betrieb("ID"))

    Denn die vom ErfinderDesRades vorgeschlagene Syntax funktioniert mit meinem korrigierten Datenmodell nun, aber was wäre "BetriebX" (Typ BetriebRow)?

    VB.NET-Quellcode

    1. Dim chosenMAs = MADlyDts.Mitarbeiter.Where(Function(ma) ma.BetriebRow Is cbBetriebfilter.SelectedItem)


    wenn ihr mir noch einmal mit etwas Input aushelfen könntet, schaffe ich den nächsten halben Schritt bestimmt auch wieder selbst! :P

    spaghetticoder schrieb:

    wieso muss ich überhaupt den SelectedValue konvertieren?
    SelectedValue ist nunmal zur Design-/Programmierzeit vom Typ Object, weil es von Mikrosaft so festgelegt wurde. Dass es bei Dir später, wenn das Programm läuft, mit einem Integer besetzt wird, wird für den Compiler erst zur Laufzeit klar. Das muss nämlich kein Integer sein, das kann auch ein String, Date, EigeneKlasse sein. Und da der Compiler das eben zur Designzeit nicht weiß, musst Du ihm das per Konvertierung mitteilen, sonst meckert er eben. Und mit der BetriebRow: Meinst du den Vergleich? mit Is? Dann musst Du mit DirectCast arbeiten. Such mal im Forum nach dem berühmt-berüchtigten Doppelcast.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.