Laden...

EFCore - Fragen zu Fluent API und Navigation Property

Erstellt von GeneVorph vor 3 Jahren Letzter Beitrag vor 3 Jahren 522 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 3 Jahren
EFCore - Fragen zu Fluent API und Navigation Property

verwendetes Datenbanksystem: SQLite
betrifft: EntityFrameworkCore

Hallo,

ich versuche gerade komplett von DataAnnotations wegzukommen und meine Datenbank mit Hilfe von fluent Api zu konfigurieren.

Ich habe mich jetzt hauptsächlich auf entityframeworkcore.com, www.entityframeworktutorial.net und www.learnentityframeworkcore.com gestürzt.

Am besten ich zeige euch erst mal mein Data-Model und dann meine Fragen.

Ich kürze es ein wenig, weil es im Wesentlichen um das Verständnis geht (die Beispiele findet man so auch in etwa auf den vorgenannten Sites):

nehmen wir mal eine Schulklasse und einen Schüler:


public class SchoolClass
{
public int SchoolClassId {get;set;}

// ... noch ein paar Properties

public List<Students> Students {get;set;}
}

public class Student
{
public int StudentId {get;set;}
public Person PersonData {get;set;}
public Address AddressData {get;set;}
public Education EducationData {get;set,}
}

public class Person
{
public int PersonId {get;set;}

//...Properties wie Vorname, Nachname, Geburtsdatum, Geburtsort...etc.
}

public class Address
{
public int AddressId {get;set;}

//...Properties wie Straße, Hausnummer, Wohnort, Postleitzahl, etc.
}

public class Education
{
public int EducationId {get;set;}

//...Properties wie Schulform, Datum Schuleintritt, Klassenstufe, etc.
}

Nichts spannendes soweit. Wenn ich nun meinen DataContext so belasse...


public class DataBaseContext : DbContext
    {
        public DbSet<SchoolClass> SchoolClasses { get; set; }        
        public DbSet<Student> Students { get; set; }
        public DbSet<Person> Persons { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Education> Educations { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //optionsBuilder.UseSqlite("Data Source=Test_lite.sqlite");
            optionsBuilder.UseSqlite(connectionString: "FileName =./Test_DataBase.sqlite");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {                   
                        
        }
    }

...dann erstellt mir Entitiframeworkcore eine Datenbank mit den Tables SchoolClasses, Students, Persons, Addresses und Educations. In jedem Table werden außerdem Columns für die ForeignKeys erstellt, um die Objekt-Relationen abzubilden. Soweit so gut.

Nun möchte ich wie gesagt einige Eigenschaften konkretisieren. Ich möchte z. B. meiner Student-Klasse noch drei Properties vom Typ int hinzufügen:
public int PersonKey
public int AddressKey
public int EducationKey
Diese möchte ich im Code auslesen können, um z. B. ganz gezielt Informationen aus der Datenbank abfragen zu können - dazu müsste ich doch im ModelBuilder deklarieren, dass der ForeignKey z. B. PersonKey sein soll, oder? Nur


 protected override void OnModelCreating(ModelBuilder modelBuilder)
        {                   
            modelBuilder.Entity<Student>()
                              .HasForeignKey()  //<-- funktioniert bei mir nicht, diesen Eintrag bekomme ich                    
                                                                durch Intellisense nicht vorgeschlagen, bzw. wir als Fehler 
                                                                 angezeigt.

        }

Ich bin mir nicht sicher, warum das nicht funktioniert - wenn ich mir die Beispiele auf den erwähnten Sites so ansehe, kommt mir allerdings der Verdacht, dass ich ein Navigation-Property benötige.
Dessen Funktion ist mir im Prinzip klar, allerdings verstehe ich nicht, wie sich das auf meinen Code auswirkt, denn:



public class Student
{
public int StudentId {get;set;}

public int SchoolClassId {get;set;}
public SchoolClass AssignedSchoolClass {get;set;}
}

//Beispiel hier anhand der Person-Klasse
public class Person
{
public int PersonId {get;set;}

public int StudentId{get;set;}
public Student AssignedStudent {get;set;}
}

  • jetzt hätte meine Student-Klasse ein SchoolClass-Objekt (Navigation-Property) um eine One-to-Many-Relation herzustellen (Eine Klass kann viele Schüler enthalten <-> ein Schüler gehört immer nur einer Klasse an), bzw meine Person-Klasse ein Student-Objekt und nach den EFC-Konventionen hätten wir eine One-to-One-Relationship (zumindest möchte ich das abbilden: Ein Schüler hat genau ein ihm zugeordnetes Personen-Objekt mit seinen Stammdaten <->ein Personen-Objekt mit seinen spezifischen Stammdaten gehört zu genau einem Schüler)

Was ich jetzt nicht so ganz kapiere: in meinem Daten-Model ist jetzt in der Person-Klasse plötzlich ein Objekt vom Typ Schüler - und dieses besitzt ja wiederum ein Person-Property und dieses ... immer so weiter. Ebenso in der Student-Klasse: ein SchoolClass-Objekt, das ja wiederum eine List<Student> enthält. Soll das so sein?

Im Prinzip müsste es doch so sein, dass mein Navigation-Property im Code gar nicht auftaucht (denn dort wird es ja auch nicht gebraucht) - aber andererseits muss es ja da sein, damit ich es als Navigation-Property nutzen kann (denn für den DataContext wird es ja gebraucht).
So ganz bin ich mir noch nicht sicher, wie das läuft - ich hoffe, ihr könnt mir das etwas einfacher erklären 😃

Gruß
Vorph

16.806 Beiträge seit 2008
vor 3 Jahren

...dann erstellt mir Entitiframeworkcore eine Datenbank

Ganz wichtig: EF erstellt keine Datenbank. EF arbeitet mit einem Schema.
Du kannst daraus mit Hilfe der Migrations eine Datenbank erzeugen / pflegen - musst es aber nicht.

dazu müsste ich doch im ModelBuilder deklarieren, dass der ForeignKey z. B. PersonKey sein soll, oder?

Nein. EF ist der FK hier im Endeffekt egal; der Datenbank aber nicht, weil Du durch das Schema Regeln erzeugt hast, dass der FK gesetzt sein muss.
Das prüft hier EF aber nicht automatisch und auch bei Queries spielt das für EF keine Rolle.

durch Intellisense nicht vorgeschlagen, bzw. wir als Fehler angezeigt.

Weil es auch falsch ist. Du wirst die Methode auch nirgends in der Doku finden.

dass ich ein Navigation-Property benötige.

Richtig. Relationen funktionieren nur über Navigation Properties.

Inhaltlich sagt Dein Code: "Ich habe einen FK". Du verrätst aber nicht welchen.
Und die Datenbank kann auch nicht hellsehen, was Du willst. Du musst das vollständig angeben.

Im Prinzip müsste es doch so sein, dass mein Navigation-Property im Code gar nicht auftaucht (denn dort wird es ja auch nicht gebraucht)

Das Navigation Object wird immer gebraucht, ansonsten kann der Context intern nicht hergestellt werden.
Zusätzlich wird das Navigation Field, also die Property, in der die Relation gesetzt wird (hier nen Int) immer benötigt. Immer.
Gibst Du es nicht selbst an, dann erzeugt das EF automatisch. EF ist aber halt auch nur 60% intelligent und erzeugt es manchmal an der falschen Stelle.
Zusätzlich kann man die Id mit einem entsprechenden FK Feld (auch wenn es eine Eigenschaft ist) direkt setzen, ohne das Objekt laden zu müssen.
Das macht die Sache bei vielen Szenarien einfacher und schneller (letzteres hier eher nicht, weil SQlite).


Anbei Dein korrigierter Code mit Kommentaren an den notwendigen Stellen.
Hab natürlich nicht alles gefixt, sondern nur das relevante. Fertig machen musst selbst.

 public class SchoolClassEntity
    {
        public int SchoolClassId { get; set; }

        // EF arbeitet intern mit einem HashSet und nicht mit List.
        // Daher ist es ratsam, HashSet zu verwenden
        public HashSet<StudentEntity> Students { get; set; } = null!;
    }

    public class StudentEntity
    {
        public int StudentId { get; set; }

        // EF Core unterstützt C#9 nullables, die in diesem Kontext auch genutzt werden sollten
        // Damit ersparst Du Dir auch Annotations wie Required etc.
        public Person PersonData { get; set; } = null!;
        public int PersonDataId { get; set; }

        // FKs liegen meist in der Tabelle, in der die Relationen zusammen finden
        // In Deinem Fall ist das hier in dieser Klasse
        // Daher solltest hier auch die IDs hinterlegen.
        // Legst Du sie nicht fix fest erzeugt sie EF automatisch; aber nicht immer automatisch in der richtigen Tabelle.
        // Daher: FK Eigenschaften immer fix selbst setzen


        public Address AddressData { get; set; } = null!;
        public int AddressDataId { get; set; }

        public Education EducationData { get; set; } = null!;
        public int EducationDataId { get; set; }
    }

    public class MyDb : DbContext
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new StudentEntityTypeConfig());
        }
    }

    public class StudentEntityTypeConfig : IEntityTypeConfiguration<StudentEntity>
    {
        public void Configure(EntityTypeBuilder<StudentEntity> builder)
        {
            // EF-Klassen sind keine Business Modelle
            // Daher tragen die Klassen üblicherweise das Suffix "Entity"
            // Gespeichert werden soll aber in die Tabelle Students
            builder.ToTable("Students");

            // PK
            builder.HasKey(e => e.StudentId);
            
            // Students hält nun die FKs, die explizit deklariert werden müssen.
            
            // Zunächst die Relation aus Sicht des Students
            builder.HasOne<Person>(s => s.PersonData)
                // danach das Mapping von Person auf Student (Optional, kann man weglassen, wenn nur von Student aus zugegriffen werden soll)
                // Das gilt aber nur für den Code; für die DB hat das keine Relevanz
                .WithOne(p => p.Student)
                // Wichtig aber nun die Angabe, wo der FK zu finden ist
                // Die Angabe des Typs ist deswegen notwendig, damit EF weiß, in welcher der beiden Klassen dieser Relation
                //   der FK liegt
            
                // EF gibt keine Warnung aus, wenn Du hier die falsche Int-Eigenschaft wählst (zB AdressId statt PersonData Id)
                // Das knallt erst beim Zugriff oder gibt unerwartete Relations-Einträge zurück
                .HasForeignKey<StudentEntity>(s => s.PersonDataId);
            
        }
    }

    public class Person
    {
        public int PersonId { get; set; }

        //...Properties wie Vorname, Nachname, Geburtsdatum, Geburtsort...etc.

        // Wenn man will kann man hier das Relationsmapping hinterlegen; muss aber nicht.
        // Null deshalb, da ich das Schema hier so verstehe, dass nicht jede Person ein Student sein muss
        public StudentEntity? Student { get; set; }
    }

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 3 Jahren

Hallo Abt,

erstmal vielen Dank für die ausführlichen Erklärungen, und das du so viel Mühe in meinen Code gesteckt hast!

Bis ins kleinste Detail ist mir noch nicht klar, was da passiert, aber hier vlt. eine Frage, mit der ich etwas Licht ins Dunkel bekomme:

sind die Daten-Models etwas anderes als die Entity-Models? Oder blöd gefragt: in meiner Anwendung stelle ich z. B. Informationen in Textboxen bereit, die übergebe ich an die entsprechenden Properties der entsprechenden Data-Models (z. B. SchoolClass in meinem Fall), dieses übergibt dan diese Daten mit Hilfe eines DataService an die Datenbank weiter. Aber dieses Data-Model ist nochmal getrennt von den Models für die Entities, oder?

Weil es auch falsch ist. Du wirst die Methode auch nirgends in der Doku finden.

Das will ich dir auch gerne glauben, aber zumindest hier wird die Methode beschrieben/gezeigt. Und auch in msnd.

Nein. EF ist der FK hier im Endeffekt egal; der Datenbank aber nicht, weil Du durch das Schema Regeln erzeugt hast, dass der FK gesetzt sein muss.
Das prüft hier EF aber nicht automatisch und auch bei Queries spielt das für EF keine Rolle.

Ah - Danke! Auch das hab ich noch nirgendwo so gelesen, aber es holt einige Fragezeichen von meiner Liste! Jetzt verstehe ich auch, warum es EFC bis zur Migration völlig egal ist (und eigentlich auch darüber hinaus), außer beim PK. Ich dachte mir ja schon, dass da keine Magie im Spiel ist 😉

16.806 Beiträge seit 2008
vor 3 Jahren

Aber dieses Data-Model ist nochmal getrennt von den Models für die Entities, oder?

Entitites sind Datenmodelle. Aber Datenmodelle sind nicht gleich Entities:
Entities sind (bei relationalen Datenbanken) die genaue Darstellung einer Tabellenstruktur.
Projektionsklassen sind ebenfalls Datenmodelle; stellen aber das Resultat einer Abfrage dar (Query Models).

Datenmodelle ist also der Überbegriff im Sinne der Architektur für die Modelle an/mit einer Datenbank/Datenhalde (XML, Json, Whatever).

In einer traditionellen Anwendung hat man meist drei Modellschichten:

  • Die Datenbankschicht hat Entitäten aka Datenmodelle: repräsentiert nur die Daten gespeichert werden
  • Die Logikschicht hat Business Modelle: repräsentiert die Logik an/mit Modellen
  • Die Anzeigeschicht hat nur ViewModels: Modelle zum reinen Anzeigen von Daten
    => [Artikel] Drei-Schichten-Architektur

Ja, man kann ein Datenmodell für die Anzeige verwenden; macht man meist nur in sehr kleinen Anwendungen.
Man kommt hier sehr schnell an Limits / Probleme bzg. Speichern/Anzeigen.

In modern(re)en Software Architekturen findet man meist keine direkten Logikmodelle mehr; man spart sich oft den Overhead und arbeitet mit CQRS.

Das will ich dir auch gerne glauben, aber zumindest
>
wird die Methode beschrieben/gezeigt.

Da hab ich mich zu undeutlich ausgesprochen: Nein, die Methode existiert nicht ohne Parameter wie Du es hast.
Ja, sie existiert natürlich mit Parametern; siehe mein Beispiel (und die Begründung).

Edit: jetzt kapier ich glaube ich erst, was Du mit der Methode meinst.
Vermutlich fehlt Dir das NuGet Paket Microsoft.EntityFrameworkCore.Relational , mit dem die Methode HasForeignKey mitkommt.
Die Methode kommt natürlich bei EFCore nich mit dem Core-NuGet mit, weil es ja auch nicht-relationale Datenbanken/Provider gibt.

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 3 Jahren

So, ich konnte mich jetzt die letzten Tage noch einmal eingehender mit der Thematik beschäftigen. Vielen Dank, Abt, ich kann mittlerweile tatsächlich Erfolge verweisen - die Datenbank (das DB-Schema, das erstellt wird) sieht wirklich so aus, wie ich es konfiguriert habe und verhält sich auch wunschgemäß.

Super vielen Dank hierfür 😉

Eine letzte Frage sei mir vielleicht noch gestattet: es ist das erste Mal, dass ich bewusst auf DataAnnotations verzichtet habe und mich ganz bewusst für die sauberere Implementierung und das "mehr" an Power der Fluent Api entschieden habe. Leider bekomme ich zu 99,9% nur meinen eigenen Code zu Gesicht und so fällt es schwer, vergleiche zu ziehen. Daher muss ich jetzt bei einer Sache noch einmal nachhaken:

Entitites sind Datenmodelle. Aber Datenmodelle sind nicht gleich Entities:
Entities sind (bei relationalen Datenbanken) die genaue Darstellung einer Tabellenstruktur.

Ich glaube, das habe ich verstanden - aber ich möchte sichergehen, dass ich nicht völlig auf dem Holzweg bin!

Mein ViewModel --> geschenkt!
Meine Business-Logik --> geschenkt! Ich habe eine Klasse Student, eine Klasse SchoolClass, eine Klasse Person...usw., die Properties und Methoden beinhalten, die die jeweilige Klasse definieren (i. Sinne von: welche Eigenschaften das Objekt haben soll) und Funktionalität bereitstellen (Methoden).

Dann habe ich noch das, was ich mein Data-Model nenne: Klassen, die nur die Properties der Models implementieren, also als kurzes Beispiel:


public class Student
{
    public int StudentId {get;set;}
    public string FullName {get;set;\
    //...noch ein paar Properties

    private int CalculateAlge() //Methode zum Berechnen des Alters
    private void ChangeStudentType () // Methode, die den Typ des Schülers ändert
}


public class StudentEntity
{
    public int StudentId {get;set;}
    public string FullName {get;set;}
    //...und die restlichen Properties
}

Soweit, so gut. (Übrigens: ich hatte meine Klassen in <MeineKlasse>Entity umbenannt - auch das hat vieles übersichtlicher gemacht.)

Um das Datenbank-Schema zu erstellen (erinnere dich kurz an meinen ersten Post) war es notwendig, dass die One-To-Many-Relation zw. Student und SchoolClass deklariert wird: Eine Klasse kann viele Schüler haben, ein Schüler immer nur eine Klasse.

Im Code oben hatte ich ja eine Liste List<Students> Students in der SchoolClass-Klasse (ist jetzt ein HashSet). Gleichzeitig habe ich jetzt meine StudentEntity so abgeändert:


public class StudentEntity
{
    public int StudentId {get;set;}
    public string FullName {get;set;}
    //...und die restlichen Properties, ergänzt um:
    
    public int SchoolClassId {get;set;}
    public SchoolClass StudentClass {get;set;}

}

Dadurch ließ sich nun im DataContext die Relation zwischen SchoolClass und Student darstellen (nur vorweg - ich hab's im Code mit einer Klasse, die IEntityTypeConfiguration<StudentEntity> implementiert gelöst, hier also nur der kürze wegen so:)


  protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SchoolClassEntity>()
                              .HasKey(s => s.SchoolClassId);

           modelBuilder.Entity<SchoolClassEntity>()
                             .HasMany<StudentEntity>(s => s.Students)
                             .WithOne(s => s.StudentClass)
                             .HasForeignKey(s => s.StudentId);

Was mich verunsichert ist folgendes:

Im Code, übergebe ich in einer Methode Daten an mein Daten-Objekt (StudentEntity), in etwa so:


       newStudentEntity.Person.FirstName = FirstName; 
       newStudentEntity.Person.LastName = LastName;
//...

Jedesmal, wenn ich die Instanz newStudentEntity von StudentEntity eintippe, zeigt mir Intellisense nun natürlich auch das StudentClass Property vom Typ SchoolClass. Theoretisch könnte ich hier also versehentlich richtig Murks machen, denn im Code (in der BusinessLogik?) sollte Student überhaupt nicht auf SchoolClass zugreifen können - im Prinzip braucht Student von SchoolClass gar nichts zu wissen! Allerdings brauche ich das Property ja hier, um es im DataContext als Navigation-Property nutzen zu können.

Wie gesagt: vielleicht mache ich mir jetzt Gedanken um Sachen, die völlig irrelevant sind.

Oder ich habe ebend doch noch etwas nicht verstanden?

Gruß
Vorph

EDIT: NNNGH! Jetzt, wo ich gerade zum x-ten Mal die Klassen durchgehe und Code optimiere, fällt mir ein, dass ich ja in meiner ModelBuilder-Klasse (z. B. StudentEntity) einen Type T definiere! Damit hätte sich ja die Frage geklärt:

  • ich schreibe Entities, die sich nur darum "kümmern", wie das Datanbankschema aussehen wird
  • Meine Daten übergebe ich nich an die Entities - sondern an DataModels (was bei mir mal StudentData war und ich nun in StudentEntity umbenannt habe)
    Ich hatte einfach den Begriff DataModels falsch verstanden (OK, zumindest zu eng gefasst), weil ich dachte, dass sind nur die Klassen, die Daten für die Datenbank entgegennehmen.

Wenn das Edit stimmt, verstehe ich auch völlig was

Entitites sind Datenmodelle. Aber Datenmodelle sind nicht gleich Entities

bedeuten soll. (Ich kreuze mal die Finger...)

16.806 Beiträge seit 2008
vor 3 Jahren

ich kann mittlerweile tatsächlich Erfolge verweisen

👍

Eine letzte Frage sei mir vielleicht noch gestattet

Ausnahmsweise sogar zwei.

"mehr" an Power der Fluent Api entschieden habe.

Es hat nicht "mehr" Power.
Die Fluent API ermöglicht Dir halt die Definition der Regeln an einer Stelle. Die Annotations sind - bedingt durch die Attribute - eben verteilt. Und dann hast durch die Generics eben noch Architektur-Möglichkeiten, die eben mit Annotations nicht umsetzbar sind.
Das ist eigentlich der große Unterschied.

Leider bekomme ich zu 99,9% nur meinen eigenen Code zu Gesicht und so fällt es schwer, vergleiche zu ziehen.

Werde Berater wie ich; siehst viel Code, der Dir Kopfschmerzen macht und Du eigentlich lieber Schafshirte geworden wärst 🙂

Oder ich habe ebend doch noch etwas nicht verstanden?

Ne, Grundlegend hast das richtig verstanden.
An der Stelle kannst Du es ohne einen Architekturumbau auch wenig ändern - außer Du versteckst halt die Eigenschaft im IntelliSense.

Wie wir das im neuen Forum machen:
Die Datenbank-Schicht gibt bei Aktionen (also Schreiben von Entities) niemals das Entity zurück, sondern nur die Werte.
So hat die "Business Schicht" immer nur die Inhalte, die tatsächlich benötigt werden und die Entität verlässt nie den DAL.

Das ist aber wirklich ein Paradigma, dass Overhead mit sich bringt und ohne effiziente "Grundstruktur" viel Code für nichts.
Hat außer eine härtere Trennung kaum Vorteile.

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 3 Jahren

Werde Berater wie ich; siehst viel Code, der Dir Kopfschmerzen macht und Du eigentlich lieber Schafshirte geworden wärst 😃

Hehe - der ist gut 😉 Auf 'nem T-Shirt würde ich den Spruch sofort kaufen 😄