Laden...

Wie können ohne "SELECT *" die Properties eines Objekts initialisiert sein?

Erstellt von WarLorD_XaN vor 3 Jahren Letzter Beitrag vor 3 Jahren 2.560 Views
W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 3 Jahren
Wie können ohne "SELECT *" die Properties eines Objekts initialisiert sein?

Hallo zusammen,

vor ein paar Monaten gab es in einem Thread hier im Forum die Empfehlung dass "SELECT * " eine schlechte Idee ist:

Es wurde dort auch auf StackOverflow verwiesen warum genau das eine schlechte Idee ist.

Ich verstehe soweit die auf StackOverflow genannten Gründe und finde sie klingen auch nachvollziehbar.

Was ich nicht verstehe, ist jedoch wie das zusammen mit einem Object-Relational-Mapping (ORM) Framework verträgt.
Angenommen ich habe eine Klasse die mir eine Tabelle abbildet.
Wenn jetzt beim SELECT aus der Datenbank nicht alle Spalten abgefragt werden, dann ist ja auch mein Objekt nur zum Teil mit den Daten initialisiert.

Woher soll das ORM Framework wissen welche Spalten es abfragen soll?
Dann müsste ja die Logik, welche mit den Datenobjekten arbeitet (oder mit Objekten die daraus aufgebaut sind) der Datenschicht mitteilen welche Spalten sie benötigt.
Da kann ich mir das ORM Framework ja komplett sparen.

Habe ich da etwas falsch verstanden?
Oder ist das ein Problem, dass ich je nach Tabelle anders lösen muss.

Schöne Grüße,
Xan

verwendetes Datenbanksystem: SQL Server

F
10.010 Beiträge seit 2004
vor 3 Jahren

Ja, da hast du etwas vergessen in deinen Überlegungen.

Die Datenobjekte beinhalten genau die Spalten, die Du haben willst.
Nicht mehr und nicht weniger, und die holt der ORM dann auch ab.

W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 3 Jahren

Ah ich denke ich verstehe.

Wenn ich explizit "alle" Spalten abrufe dann ist das besser als * weil ich (bzw. der ORM) die Kontrolle habe was abgefragt wird.

Der ORM könnte ja nicht von allen Spalten wissen.

Die SELECT * Problematik ist also nicht die dass alle Spalten einer Tabelle abgefragt werden, sondern wo die Kontrolle liegt welche Spalten abgefagt werden.

Könnte man das so stehen lassen?

F
10.010 Beiträge seit 2004
vor 3 Jahren

Jain.

Du siehst es jetzt nur aus der Sicht der Anwendung.

Select * bedeutet für den SqlServer aber auch etwas anderes.
Es müssen ggf andere Ausführungspläne erstellt werden, mehr Daten übertragen werden usw.

Auch ist dadurch die Reihenfolge der Spalten nicht im vorhinein bekannt, was dazu führt dass das Materialisieren ggf langsamer wird usw.

Wenn Du z.b. Binary oder NVarChar(max) Spalten in einer Tabelle hast, stehen die nicht im Datensatz selber, sondern physikalisch getrennt.
Wenn Du die abholst, obwohl sie nicht gebraucht werden, erhöht das sowohl die Zugriffszeiten, als auch den Datenverkehr und den Speicherverbrauch auf dem Server.

Gibt noch einiges anderes, aber das ist das offensichtlichste.

16.806 Beiträge seit 2008
vor 3 Jahren

Wenn jetzt beim SELECT aus der Datenbank nicht alle Spalten abgefragt werden, dann ist ja auch mein Objekt nur zum Teil mit den Daten initialisiert.

Sowas nennt sich Projektion; und für eine Projektion nimmt man nicht die Entitäts-Klassen, sondern extra Projektionsklassen.
Entity Framework hat dafür einen eingebauten Mechanismus im Rahmen der Keyless Entity Types.

Schöner ist jedoch der ProjectTo Mechanismus von AutoMapper. Ich verwende diesen Mechanismus bei allen EF Core Projekten (inkl. dem neuen Code von mycsharp.de).

W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 3 Jahren

@FZelle
Ok, das klingt für mich nachvollziehbar.

@Abt
Keyless Entity Types kannte ich noch gar nicht, danke für den Hinweis. Damit kann ich in meinem privaten Projekt wo ich EF Core verwende sicher einige Query-Konstrukte optimieren.

Vielleicht noch zum Hintergrund:
Wir haben in der Firma eine Art "ORM" Framework im Einsatz, das aktuell so konfiguriert ist, dass man die Spalten die man haben möchte immer explizit angeben muss. Die Objekte auf die das gemappt wird sind jedoch immer die selben, egal ob ich eine oder alle Spalten einer Tabelle haben will.
Da es in diesem Framework keine Zentrale Stelle gibt wo Daten aus der DB geladen werden, sondern das quer über die komplette Anwendung verstreut ist, bedeutet das einen riesigen Wartungsaufwand.

Daher disktutieren wir aktuell ob wir das Framework nicht umkonfigurieren sodass immer alle Spalten geladen werden.
Darum habe ich hier diesen Thread eröffnet, da ich mir unsicher war ob das generell eine schlechte Idee ist.
Unser "ORM" arbeitet so dass die Spalten im resultierenden SQL explizit angegeben werden. Also denke ich sollten wir das einfach mal testen wie hier die Performance aussieht.

16.806 Beiträge seit 2008
vor 3 Jahren

Ich habe bei meinen Anwendungen - glaube ich - nie irgendeine Stelle, wo ich wirklich eine gesamte, einzelne Entität benötige.
Das kostet ja auch alles Zeit, Performance, Load, Traffic...

Ich benötige eigentlich immer einen Ausschnitt einer Entität, oder die Summe aus einer gejointen Abfrage.
Der Vorteil einer Projektion ist ja auch, dass Du - in der Regel - immer nur eine Abfrage gegen die DB hast und keinen Data Merge Aufwand in der Logik hast.

Beispiel: Die Ansicht eines Profils (ex Abt) hier im Forum ist eine Summe aus mehreren Tabellen:

  • Forum Threads
  • Forum Posts
  • Users
  • User Profiles
  • User Avatars

Die Abfrage einer Profile View sieht so aus:

namespace MyCSharp.Portal.Engine.QueryHandlers
{
    public class GetUserProfileViewProjectionHandler : IQueryHandler<GetUserProfileViewProjection, UserProfileProjection>
    {
        private readonly IMyCSharpDbContext _dbContext;
        private readonly IMapper _mapper;

        public GetUserProfileViewProjectionHandler(IMyCSharpDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext;
            _mapper = mapper;
        }

        public async Task<UserProfileProjection> Handle(GetUserProfileViewProjection request, CancellationToken cancellationToken)
        {
            // inputs
            int userId = request.UserId;

            // query
            var query = _dbContext.UserAccounts.AsNoTracking()
                .Where(DbUserEx.HasId(userId))
                .ProjectTo<UserProfileProjection>(_mapper.ConfigurationProvider);

            // load
            var result = await query.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false);

            return result;
        }
    }
}

D.h. alle Informationen werden über einen relativ großen aber sehr performanten Join geladen. Eine einzige Abfrage.

Das wichtige ist folgende Zeile:

.ProjectTo<UserProfileProjection>(_mapper.ConfigurationProvider);

ProjectTo ermittelt anhand von Reflection, welche Linq Expression aufgebaut werden muss, damit nur das geladen wird, was benötigt wird.
Die Linq Expression ist in EF Core die Basis für das, was nachher den SQL Query String erzeugt.

Der einzige Wartungsaufwand, den man hier noch hat:

  • Man muss eine Projektionsklasse erstellen, die die geladenen Daten erzeugt
  • Man muss ein Proktionsprofile erzeugen, sodass nachher die Expression aufgebaut werden kann

Wenn man eine Änderung hat, dann muss man nur die Projektion und dessen Profile anpassen - mehr nicht.

Die Projektion sieht in diesem Fall bei uns so aus:

namespace MyCSharp.Portal.Data.Projections
{
    public class UserProfileProjection
    {
        public UserProfilePropertyProjection Profile { get; set; } = null!;
        public UserAvatarProjection? Avatar { get; set; }
        public UserForumStatsProjection ForumStats { get; set; } = null!;
        public UserLatestActivityProjection? LatestActivity { get; set; }
        public ForumPostHeadProjection? LatestPost { get; set; }
    }
}

Und so das Profile:

namespace MyCSharp.Portal.Data.Profiles
{
    public class UserProfileProjectionProfile : AutoMapper.Profile
    {
        public UserProfileProjectionProfile()
        {
            CreateMap<UserAccountEntity, UserProfileProjection>()

                .ForMember(p => p.Profile,
                    opt => opt.MapFrom(e => e))

                .ForMember(p => p.Avatar,
                    opt=> opt.MapFrom(e => e.Avatar))

                .ForMember(p => p.ForumStats,
                    opt=> opt.MapFrom(e => e))

                .ForMember(p => p.LatestActivity,
                    opt => opt.MapFrom(e => e))

                .ForMember(p => p.LatestActivity,
                    opt => opt.MapFrom(e => e));
        }
    }
}

Das hier ist viel komfortabler und einfacher umzusetzen als die Keyless Types in EF Core - wobei das Ziel das selbe ist.

Im Endeffekt ist das eine sehr moderne Darstellung einer mehrschichtigen Architektur, da es keinerlei Abhängigkeiten zu den Daten-Modellen gibt, sondern nur zu Projektionen.

PS noch zum Naming:

  • Views sind Resultate aus multiplen DB Queries
  • Projections das Resultat eines einzelnen Queries (auch über Joins)
W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 3 Jahren

Wow!

Danke Abt, für diese tolle Einführung in das Thema Projections!

Das ist ja eine super Sache, ich wünschte ich hätte mal wieder etwas mehr Zeit mich um mein privates EF Core Projekt zu kümmern.

Nur damit ich das auch richtig verstanden habe:

Ich mache mir also für die verschiedensten Anwendungsfälle (Logik oder UI) einfach meine Projections und das dafür notwendige Profile. Und in der Logik bzw. UI verweise ich dann nur noch auf die Projections. In der UI habe ich dann eventuell noch eine Art ViewModel darum.

Könnte man sagen eine Entity wäre ein Objekt auf Datenbank Ebene und eine Projection wäre dann ein Business Object?

Edit:
Wie wäre das dann wenn ich Daten verändern möchte?
Projections oder auch die KeylessEntity werden ja nicht getrackt.
Wobei ich ja bei Änderungen sowieso im Zweifelsfall mehrer Queries machen muss.

16.806 Beiträge seit 2008
vor 3 Jahren

Könnte man sagen eine Entity wäre ein Objekt auf Datenbank Ebene und eine Projection wäre dann ein Business Object?

Eine Projektion ist prinzipiell ein unabhängiges Darstellungobjekt; gehört aber aus der Sicht der Architektur zum DAL.

Wie wäre das dann wenn ich Daten verändern möchte?
Projections oder auch die KeylessEntity werden ja nicht getrackt.
Wobei ich ja bei Änderungen sowieso im Zweifelsfall mehrer Queries machen muss.

Projektionen wie der Name schon sagt - projizieren nur.
Da wird nichts getrackt; das ist nur für Lesen gedacht.

Schreiben wird Dir hier null abgenommen.

Für das Schreiben gibt es im Rahmen von CQRS eben Commands; auf das Profilbeispiel hier im Forum bezogen:
Der User möchte seine Signatur ändern:

    public class UserProfileUpdateSignatureCommandHandler : ICommandHandler<UserProfileUpdateSignatureCommand, bool>
    {
        private readonly IMyCSharpDbContext _dbContext;

        public UserProfileUpdateSignatureCommandHandler(IMyCSharpDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<bool> Handle(UserProfileUpdateSignatureCommand command, CancellationToken cancellationToken)
        {
            // input
            int userId = command.UserId;
            string? signature = command.Content;
            TextContentOptions signatureOptions = command.TextContentOptions;

            // query
            var user = await _dbContext.UserAccounts
                .Where(DbUserEx.HasId(userId))
                .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false);

            if (user is null) return false;

            // act
            user.Profile.Signature = signature;
            user.Profile.SignatureContentOptions = signatureOptions;

            // commit
            await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

            return true;
        }
    }

Das ist nun natürlich aufwändiger als ein simples Tracking, aber:

  • Viel einfacher zu warten
  • Viel einfacher zu testen
  • Viel performanter
W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 3 Jahren

Ok ich denke ich verstehe.

Das mit den Commands sieht auch ganz interessant aus, damit muss ich mich mal näher beschäftigen.
Hast du da vielleicht noch weiterführende Lektüre bei der Hand?

Danke nochmal für deine ausführlichen Erklärungen 👍

T
2.219 Beiträge seit 2008
vor 3 Jahren

Das Command Pattern wirst du in der heutigen Zeit öfters antreffen.
Technologie wie WPF nutzt dieses Pattern zur Interaktion in der UI.

Wikipedia

Anbei kann ich dir den Schinken für Entwurfsmuster empfehlen.
Habe ich hier bei mir liegen und deckt wirklich verdammt viel ab und ist gut geschrieben.

Entwurfsmuster - Das umfassende Handbuch

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.806 Beiträge seit 2008
vor 3 Jahren

Das Command Pattern wirst du in der heutigen Zeit öfters antreffen.
Technologie wie WPF nutzt dieses Pattern zur Interaktion in der UI.

Ne ne ne ne ne ne ne ne ne ne ne ne ne ne ne.

Der Command Pattern mit Referenz auf WPF hat mit Commands aus CQS/CQRS nichts zutun.
Auch wenn sie ähnlich heissen: die Funktionsweise, die Implementierung und die Idee dahinter sind grundlegend unterschiedlich.

Die gezeigte Implementierung hier basiert auf MediatR und respektive einer Mediator Implementierung.
Genauer gesagt ist MediatR nach dem CQRS implementiert.

T
2.219 Beiträge seit 2008
vor 3 Jahren

@Abt
Oh, dass wusste ich nicht.
Dann muss ich mich mal in das Thema einarbeiten. 😃
Bin leider nicht voll drin in WPF, habe damit beruflich nur kurz zu tun gehabt und wusste ich das nicht.
Werd ich mir selbst mal zu gemühte führen.

Ich plane eigentlich schon länger mal mit WPF was umzusetzen, scheitere aber immer weider am eigenen Interesse.
Nach 8 Std. Arbeit ist man dann doch etwas geschlaucht 😮
Aber danke für die Korrektur.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.806 Beiträge seit 2008
vor 3 Jahren

Das hat mit WPF nichts zutun.

Der MediatR Pattern kann überall eingesetzt werden; die Samples hier stammen aus dem neuen Forencode.
Und der basiert auf .NET 5 und ASP.NET Core.

Ob ich aber Commands und Queries aus ASP.NET aufrufe, aus WPF mit MVVM oder WPF zB mit Reactive Extensions: spielt keine Rolle.
Ist jedoch eine Gesamtentscheidung der Software Architektur.

T
2.219 Beiträge seit 2008
vor 3 Jahren

Klingt interessant.
Werde ich mir die Tage mal genauer anschauen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.