Back to "Πρόσβαση δεδομένων στο .NET: Συγκρίνοντας ORMs και στρατηγικές χαρτογράφησης (Μέρος 1 - Κεντρικό πλαίσιο οντοτήτων)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

.NET Database EF Core Performance PostgreSQL

Πρόσβαση δεδομένων στο .NET: Συγκρίνοντας ORMs και στρατηγικές χαρτογράφησης (Μέρος 1 - Κεντρικό πλαίσιο οντοτήτων)

Wednesday, 03 December 2025

Όταν χτίζετε .NET εφαρμογές, μια από τις σημαντικότερες αρχιτεκτονικές αποφάσεις που θα κάνετε είναι πώς να χειριστείτε την πρόσβαση των δεδομένων και τη χαρτογράφηση των αντικειμένων. Το οικοσύστημα .NET προσφέρει μια πλούσια ποικιλία προσεγγίσεων, από πλήρως εξοπλισμένο ORMs έως γυμνό μέταλλο εκτέλεση SQL. Κάθε προσέγγιση έρχεται με τις δικές του συναλλαγές όσον αφορά την απόδοση, την παραγωγικότητα του προγραμματιστή, την ασφάλεια του τύπου και τη διατήρηση.

Σε αυτό το περιεκτικό οδηγό δύο μερών, θα εξερευνήσουμε τα πιο δημοφιλή πρότυπα πρόσβασης δεδομένων σε .NET. Ενώ χρησιμοποιούμε PostgreSQL με Npgsql στα παραδείγματα μας (αφού αυτό είναι ό, τι εξουσία αυτό το blog), οι έννοιες, τα πρότυπα, και τις συναλλαγές ισχύουν εξίσου για SQL Server, MySQL, SQLite, και άλλες σχετικές βάσεις δεδομένων. Οι αρχές παραμένουν οι ίδιες - μόνο η διάλεκτος SQL και ορισμένα συγκεκριμένα χαρακτηριστικά διαφέρουν.

Μέρος 1 (το παρόν άρθρο) εστιάζει σε πυρήνα πλαισίου οντοτήτων, γενιά SQL και κοινές παγίδες. Μέρος 2 θα καλύψει Dapper, ωμό ADO.NET, βιβλιοθήκες χαρτογράφησης αντικειμένων, και υβριδικές προσεγγίσεις.

Σχετικά άρθρα

Αν ενδιαφέρεστε για πρακτικές εφαρμογές EF Core, ελέγξτε τα άλλα άρθρα μου:

Πίνακας Περιεχομένων

Το φάσμα των προσεγγίσεων πρόσβασης δεδομένων

Το τοπίο πρόσβασης δεδομένων NET μπορεί να απεικονιστεί ως ένα φάσμα:

Full Abstraction                                      Full Control
     ↓                                                      ↓
[EF Core] → [EF Core Raw SQL] → [Dapper] → [Npgsql ADO.NET]

Καθώς κινείστε από αριστερά προς τα δεξιά, αποκτάτε απόδοση και έλεγχο αλλά χάνετε την ευκολία και τα αυτόματα χαρακτηριστικά.

Σύγκριση ροής πρόσβασης δεδομένων

Εδώ είναι μια οπτική σύγκριση του πώς κάθε προσέγγιση χειρίζεται ένα τυπικό ερώτημα:

graph TB
    subgraph "EF Core Flow"
        A1[LINQ Query] -->|Compile| B1[Expression Tree]
        B1 -->|Translate| C1[SQL Query]
        C1 -->|Execute| D1[PostgreSQL]
        D1 -->|Results| E1[DbDataReader]
        E1 -->|Materialize| F1[Tracked Entities]
        F1 -->|Return| G1[Application]
    end

    subgraph "Dapper Flow"
        A2[SQL String] -->|Parameterize| B2[DbCommand]
        B2 -->|Execute| C2[PostgreSQL]
        C2 -->|Results| D2[DbDataReader]
        D2 -->|Map| E2[POCOs]
        E2 -->|Return| F2[Application]
    end

    subgraph "Raw Npgsql Flow"
        A3[SQL + Parameters] -->|Build Command| B3[NpgsqlCommand]
        B3 -->|Execute| C3[PostgreSQL]
        C3 -->|Results| D3[NpgsqlDataReader]
        D3 -->|Manual Mapping| E3[Objects]
        E3 -->|Return| F3[Application]
    end

    style A1 stroke:#2563eb,stroke-width:2px
    style B1 stroke:#2563eb,stroke-width:2px
    style C1 stroke:#2563eb,stroke-width:2px
    style D1 stroke:#2563eb,stroke-width:2px
    style E1 stroke:#2563eb,stroke-width:2px
    style F1 stroke:#2563eb,stroke-width:2px
    style G1 stroke:#2563eb,stroke-width:2px

    style A2 stroke:#059669,stroke-width:2px
    style B2 stroke:#059669,stroke-width:2px
    style C2 stroke:#059669,stroke-width:2px
    style D2 stroke:#059669,stroke-width:2px
    style E2 stroke:#059669,stroke-width:2px
    style F2 stroke:#059669,stroke-width:2px

    style A3 stroke:#dc2626,stroke-width:2px
    style B3 stroke:#dc2626,stroke-width:2px
    style C3 stroke:#dc2626,stroke-width:2px
    style D3 stroke:#dc2626,stroke-width:2px
    style E3 stroke:#dc2626,stroke-width:2px
    style F3 stroke:#dc2626,stroke-width:2px

Επιδόσεις εναντίον Παραγωγικότητας Προγραμματιστή

graph LR
    A[High Productivity<br/>Low Performance] --> B[EF Core<br/>Full Tracking]
    B --> C[EF Core<br/>No Tracking]
    C --> D[EF Core<br/>Raw SQL]
    D --> E[Dapper]
    E --> F[Raw Npgsql]
    F --> G[Low Productivity<br/>High Performance]

    style A stroke:#2563eb,stroke-width:2px
    style B stroke:#2563eb,stroke-width:2px
    style C stroke:#3b82f6,stroke-width:2px
    style D stroke:#059669,stroke-width:2px
    style E stroke:#059669,stroke-width:2px
    style F stroke:#dc2626,stroke-width:2px
    style G stroke:#dc2626,stroke-width:2px

Κεντρικό πλαίσιο οντοτήτων: Το πλήρως εξοπλισμένο ORM

Κεντρικό πλαίσιο οντότητας είναι η ναυαρχίδα ORM της Microsoft, παρέχοντας μια πλήρη αφαίρεση πάνω από τη βάση δεδομένων σας. Npgsql.Entity frameworkCore.PostgreSQL Προμηθευτής.

Για πρακτική καθοδήγηση σχετικά με τη δημιουργία πυρήνα EF στο έργο σας, δείτε το άρθρο μου σχετικά με Προσθήκη πλαισίου οντοτήτων για δημοσιεύσεις blog.

Βασικά χαρακτηριστικά

  • Αλλαγή εντοπισμού: Αυτόματα παρακολουθεί τις αλλαγές της οντότητας και παράγει τα κατάλληλα SQL
  • Μετανάστες: Code-first schema management and version control (see Μεταναστεύσεις EF Ο Σωστός Τρόπος)
  • Προμηθευτής LINQ: Type-safe requests using C# language manufacturers
  • Τεμπέλης/Φόρτωση Φαλακρού: Ευέλικτες στρατηγικές φόρτωσης για συνδεδεμένες οντότητες
  • Προηγμένες λειτουργίες PostgreSQL: Αναζήτηση πλήρους κειμένου (δείτε το άρθρο μου), στήλες JSON, συστοιχίες, τύποι εύρους
  • Αναχαιτιστικά και Γεγονότα: Extensability points for cross-creening confections

Παράδειγμα: Βασικό CRUD με πυρήνα EF

public class BlogDbContext : DbContext
{
    public DbSet<BlogPost> BlogPosts { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=blog;Username=postgres;Password=secret");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // PostgreSQL-specific: Full-text search
        modelBuilder.Entity<BlogPost>()
            .HasGeneratedTsVectorColumn(
                p => p.SearchVector,
                "english",
                p => new { p.Title, p.Content })
            .HasIndex(p => p.SearchVector)
            .HasMethod("GIN");

        // PostgreSQL array type
        modelBuilder.Entity<BlogPost>()
            .Property(p => p.Tags)
            .HasPostgresArrayConversion(
                tag => tag.ToLowerInvariant(),
                tag => tag);
    }
}

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string[] Tags { get; set; }
    public NpgsqlTsVector SearchVector { get; set; }
    public List<Comment> Comments { get; set; }
    public DateTime PublishedDate { get; set; }
}

// Usage
public class BlogService
{
    private readonly BlogDbContext _context;

    public async Task<List<BlogPost>> GetRecentPostsAsync(int count)
    {
        return await _context.BlogPosts
            .Include(p => p.Comments)
            .OrderByDescending(p => p.PublishedDate)
            .Take(count)
            .ToListAsync();
    }

    public async Task<List<BlogPost>> SearchPostsAsync(string searchTerm)
    {
        return await _context.BlogPosts
            .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", searchTerm)))
            .ToListAsync();
    }

    public async Task AddPostAsync(BlogPost post)
    {
        _context.BlogPosts.Add(post);
        await _context.SaveChangesAsync();
    }
}

EF πυρήνας με ωμό SQL

EF Core υποστηρίζει επίσης ακατέργαστα ερωτήματα SQL όταν χρειάζεστε περισσότερο έλεγχο:

public async Task<List<BlogPost>> GetPostsByComplexCriteriaAsync()
{
    var searchTerm = "postgresql";

    return await _context.BlogPosts
        .FromSqlInterpolated($@"
            SELECT * FROM ""BlogPosts""
            WHERE ""SearchVector"" @@ to_tsquery('english', {searchTerm})
            AND array_length(""Tags"", 1) > 3
            ORDER BY ts_rank(""SearchVector"", to_tsquery('english', {searchTerm})) DESC
        ")
        .ToListAsync();
}

// Or with DbDataReader for maximum control
public async Task<List<PostStatistics>> GetPostStatisticsAsync()
{
    using var command = _context.Database.GetDbConnection().CreateCommand();
    command.CommandText = @"
        SELECT
            DATE_TRUNC('month', ""PublishedDate"") as Month,
            COUNT(*) as PostCount,
            AVG(ARRAY_LENGTH(""Tags"", 1)) as AvgTags
        FROM ""BlogPosts""
        GROUP BY DATE_TRUNC('month', ""PublishedDate"")
        ORDER BY Month DESC";

    await _context.Database.OpenConnectionAsync();

    var results = new List<PostStatistics>();
    using var reader = await command.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        results.Add(new PostStatistics
        {
            Month = reader.GetDateTime(0),
            PostCount = reader.GetInt32(1),
            AverageTags = reader.GetDouble(2)
        });
    }

    return results;
}

Πότε να χρησιμοποιήσετε τον πυρήνα EF

Χρησιμοποιήστε τον πυρήνα EF όταν:

  • Κατασκευή νέας εφαρμογής με εξελισσόμενες απαιτήσεις σχεδίασης
  • Χρειάζεστε ισχυρή δακτυλογράφηση και κατάρτιση-χρόνου επικύρωση ερωτημάτων
  • Οι μεταναστευτικές ροές και η εκδοχή σχημάτων είναι σημαντικές (βλ. Ο οδηγός μου για τη μετανάστευση)
  • Η ομάδα σας προτιμά να δουλεύει με αντικείμενα πάνω από SQL
  • Χρησιμοποιείτε σύνθετα μοντέλα τομέα με σχέσεις
  • Η ταχύτητα ανάπτυξης είναι πιο κρίσιμη από τις ωφέλιμες επιδόσεις
  • Χρειάζεστε τη φορητότητα cross-database (αν και τα ειδικά χαρακτηριστικά PostgreSQL σας κλειδώνουν)

Αποφύγετε τον πυρήνα EF όταν:

  • Η μέγιστη απόδοση είναι κρίσιμης σημασίας (π.χ. υψηλής απόδοσης APIs, επεξεργασία παρτίδων)
  • Έχετε περίπλοκες, χειροποίητες ερωτήσεις SQL
  • Οι ερωτήσεις σας δεν έχουν καλό χάρτη για να αντιτεθούν γραφήματα
  • Χρειάζεσαι έλεγχο σε κάθε δήλωση SQL.
  • Η χρήση μνήμης είναι ένας κρίσιμος περιορισμός (αλλαγή εντοπισμού από πάνω)
  • Δουλεύεις με κληροδοτημένα σχέδια που δεν χαρτογράφουν σε συνέδρια.

EF Core SQL Generation: Κατανόηση Τι Εκτελέστηκε

Μια από τις πιο σημαντικές πτυχές της χρήσης EF Core αποτελεσματικά είναι η κατανόηση τι SQL που παράγει. EF Core έχει βελτιώσει σημαντικά την γενιά SQL όλα αυτά τα χρόνια, αλλά είναι κρίσιμης σημασίας για την επαλήθευση των ερωτημάτων που αποστέλλονται στο PostgreSQL.

Προβολή δημιουργημένου SQL

// Enable sensitive data logging and detailed errors (development only!)
optionsBuilder
    .UseNpgsql(connectionString)
    .EnableSensitiveDataLogging()
    .EnableDetailedErrors()
    .LogTo(Console.WriteLine, LogLevel.Information);

// Or use logging to see SQL
public class BlogService
{
    private readonly BlogDbContext _context;
    private readonly ILogger<BlogService> _logger;

    public async Task<List<BlogPost>> GetPostsAsync()
    {
        var query = _context.BlogPosts
            .Where(p => p.PublishedDate > DateTime.UtcNow.AddDays(-30))
            .OrderByDescending(p => p.PublishedDate);

        // View the SQL before execution
        var sql = query.ToQueryString();
        _logger.LogInformation("Executing query: {Sql}", sql);

        return await query.ToListAsync();
    }
}

Παράδειγμα: Απλό ερώτημα

C# LINQ:

var recentPosts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .OrderByDescending(p => p.PublishedDate)
    .Take(10)
    .ToListAsync();

Δημιουργία SQL (EF Core 8+):

SELECT b."Id", b."Title", b."Content", b."CategoryId", b."PublishedDate"
FROM "BlogPosts" AS b
WHERE b."CategoryId" = @__categoryId_0
ORDER BY b."PublishedDate" DESC
LIMIT @__p_1

Παρατηρήστε πώς το EF Core 8+ παράγει καθαρό, αποδοτικό SQL με σωστή παραμετροποίηση. EF πυρήνας 10 συνεχίζει αυτή την τάση με ακόμη περισσότερες βελτιώσεις.

Παράδειγμα: Ενωθείτε με Συμπεριλάβετε (Πριν από τον πυρήνα EF 5)

Κωδικός C#:

var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();

Old SQL (EF Core 3.1 - Cartesian Explosion):

SELECT b."Id", b."Title", c."Id", c."Name", cm."Id", cm."Content"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"
LEFT JOIN "Comments" AS cm ON b."Id" = cm."BlogPostId"
ORDER BY b."Id", c."Id"

Αυτό δημιουργεί ένα Καρτεσιανό προϊόν - εάν μια δημοσίευση έχει 10 σχόλια, αυτή η σειρά επαναλαμβάνεται 10 φορές!

Παράδειγμα: Διαχωρισμός ερωτημάτων (EF Core 5+)

Κωδικός C# με το Split Query:

var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()  // ← This is the key!
    .ToListAsync();

Δημιουργία SQL (Πολλαπλές ερωτήσεις):

-- Query 1: Get posts and categories
SELECT b."Id", b."Title", b."Content", c."Id", c."Name"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"

-- Query 2: Get comments for those posts
SELECT cm."Id", cm."Content", cm."BlogPostId"
FROM "Comments" AS cm
INNER JOIN (
    SELECT b."Id"
    FROM "BlogPosts" AS b
) AS t ON cm."BlogPostId" = t."Id"
ORDER BY t."Id"

Αυτό εξαλείφει το Καρτεσιανό προϊόν και είναι συχνά Πολύ πιο γρήγορα. για τις συλλογές!

Παράδειγμα: Φιλτραρισμένος πίνακας (EF Core 5+)

Κωδικός C#:

var posts = await _context.BlogPosts
    .Include(p => p.Comments.Where(c => c.IsApproved))
    .ToListAsync();

Δημιουργία SQL:

SELECT b."Id", b."Title", b."Content", t."Id", t."Content", t."IsApproved"
FROM "BlogPosts" AS b
LEFT JOIN (
    SELECT c."Id", c."Content", c."IsApproved", c."BlogPostId"
    FROM "Comments" AS c
    WHERE c."IsApproved" = TRUE
) AS t ON b."Id" = t."BlogPostId"
ORDER BY b."Id"

Παράδειγμα: Ερωτήματα στήλης JSON (EF Core 7+)

Κωδικός C#:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public PostMetadata Metadata { get; set; }  // Stored as JSONB
}

public class PostMetadata
{
    public bool IsFeatured { get; set; }
    public int ViewCount { get; set; }
    public List<string> RelatedTags { get; set; }
}

// Query JSON properties
var featuredPosts = await _context.BlogPosts
    .Where(p => p.Metadata.IsFeatured)
    .ToListAsync();

Δημιουργία SQL:

SELECT b."Id", b."Title", b."Metadata"
FROM "BlogPosts" AS b
WHERE b."Metadata" ->> 'IsFeatured' = 'true'

EF Core 7+ μπορεί να μεταφράσει JSON πρόσβαση σε ιδιοκτησία στους φορείς PostgreSQL JSON!

Παράδειγμα: Μαζική Ενημέρωση (EF Core 7+ ExecuteUpdate)

Παλαιός Τρόπος (Δεν είναι αποτελεσματικός):

var posts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ToListAsync();

foreach (var post in posts)
{
    post.IsArchived = true;
}

await _context.SaveChangesAsync();  // Generates N UPDATE statements!

Νέος τρόπος (EF Core 7+):

await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));

Δημιουργία SQL (Single Query!):

UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5

Αυτό είναι συμπαγές βελτίωση - μια δήλωση SQL αντί για N!

Παράδειγμα: Μαζική Διαγραφή (EF Core 7+)

Old Way:

var oldPosts = await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ToListAsync();

_context.BlogPosts.RemoveRange(oldPosts);
await _context.SaveChangesAsync();  // N DELETE statements

Νέος τρόπος:

await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ExecuteDeleteAsync();

Δημιουργία SQL:

DELETE FROM "BlogPosts" AS b
WHERE b."PublishedDate" < @__p_0

Παράδειγμα: Συγκρότημα

Κωδικός C#:

var categoryStats = await _context.Categories
    .Select(c => new CategoryStats
    {
        CategoryName = c.Name,
        PostCount = c.BlogPosts.Count(),
        LatestPostDate = c.BlogPosts.Max(p => p.PublishedDate),
        AverageComments = c.BlogPosts.Average(p => p.Comments.Count)
    })
    .ToListAsync();

Δημιουργία SQL (EF Core 8+/10):

SELECT c."Name" AS "CategoryName",
       COUNT(*)::int AS "PostCount",
       MAX(b."PublishedDate") AS "LatestPostDate",
       COALESCE(AVG((
           SELECT COUNT(*)::int
           FROM "Comments" AS c0
           WHERE b."Id" = c0."BlogPostId"
       ))::double precision, 0.0) AS "AverageComments"
FROM "Categories" AS c
LEFT JOIN "BlogPosts" AS b ON c."Id" = b."CategoryId"
GROUP BY c."Id", c."Name"

Αναζήτηση πλήρους κειμένου PostgreSQL

Για μια βαθύτερη κατάδυση σε αναζήτηση πλήρους κειμένου, δείτε το άρθρο μου σχετικά με εφαρμογή αναζήτησης πλήρους κειμένου με πυρήνα EF.

Κωδικός C#:

var searchResults = await _context.BlogPosts
    .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .OrderByDescending(p => p.SearchVector.Rank(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .Take(20)
    .ToListAsync();

Δημιουργία SQL:

SELECT b."Id", b."Title", b."Content", b."SearchVector"
FROM "BlogPosts" AS b
WHERE b."SearchVector" @@ to_tsquery('english', @__searchTerm_0)
ORDER BY ts_rank(b."SearchVector", to_tsquery('english', @__searchTerm_0)) DESC
LIMIT 20

Critical EF Core Warnings and Pitfalls

1. Αλλαγή εντοπισμού διαρροών μνήμης

Το πρόβλημα:

// ❌ DANGER: This can cause memory leaks!
public class PostCache
{
    private readonly BlogDbContext _context;
    private List<BlogPost> _cachedPosts;

    public PostCache(BlogDbContext context)
    {
        _context = context;
    }

    public async Task LoadCacheAsync()
    {
        // These entities are now tracked by the context
        _cachedPosts = await _context.BlogPosts.ToListAsync();

        // The DbContext holds references to these entities FOREVER
        // They can never be garbage collected while the context lives!
    }
}

Γιατί είναι πρόβλημα:

  • Εντοπισμένες οντότητες παραμένουν στη μνήμη για τη διάρκεια ζωής του DbContext
  • Ο ανιχνευτής αλλαγών διατηρεί αναφορές, εμποδίζοντας τη συλλογή σκουπιδιών
  • Μακροζωία περιβάλλοντα (π.χ. μονότονα) = διαρροή μνήμης
  • Στο ASP.NET Core, το πλαίσιο είναι προεπιλεγμένο (καλό!)
  • Αλλά αν κρυπτογραφήσεις οντότητες, έχεις πρόβλημα.

Η Λύση:

public async Task LoadCacheAsync()
{
    // ✅ Use AsNoTracking() for read-only queries
    _cachedPosts = await _context.BlogPosts
        .AsNoTracking()
        .ToListAsync();

    // Or detach entities after loading
    var posts = await _context.BlogPosts.ToListAsync();
    foreach (var post in posts)
    {
        _context.Entry(post).State = EntityState.Detached;
    }
    _cachedPosts = posts;
}

2. Proxy Γενιά και τεμπέλης Φόρτωση Κίνδυνοι

ΚΡΙΤΙΚΗ ΠΡΟΕΙ∆ΟΠΟΙΗΣΗ: ΜΗ ΧΡΗΣΙΜΟΠΟΙΕΙΤΕ EF CORE PROXIES if you cache agricultures

Τεμπέλης φόρτωσης proxies + caching = ΕΓΓΥΗΣΗ ΜΝΗΜΙΚΟΥ ΛΙΘΟΥ

Εάν cache DbContext περιπτώσεις ή cache οντότητες φορτωμένες με proxies ενεργοποιημένη, σας ΓΟΥΙΛ Ο μηχανισμός μεσολάβησης διατηρεί αναφορές στο DbContext, εμποδίζοντας τη συλλογή σκουπιδιών. Αυτό είναι ένα από τα πιο κοινά και επικίνδυνα λάθη στις εφαρμογές EF Core.

Κανόνας του αντίχειρα: Πάντα να περιλαμβάνει συλλογές ρητά με .Include()Χρησιμοποιήστε πληρεξούσια μόνο αν κατανοήσετε πλήρως τις συναλλαγές και ποτέ, ποτέ, ποτέ πληρεξουσίους φορείς.

Πρόβλημα 1: Ο Εφιάλτης Ν+1

// ❌ Enable lazy loading
optionsBuilder
    .UseNpgsql(connectionString)
    .UseLazyLoadingProxies();  // Convenient but dangerous!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public virtual Category Category { get; set; }  // Virtual = proxy
    public virtual List<Comment> Comments { get; set; }
}

// Somewhere in your code
var posts = await _context.BlogPosts.ToListAsync();

foreach (var post in posts)
{
    Console.WriteLine(post.Category.Name);  // N+1 query here!
    Console.WriteLine(post.Comments.Count);  // Another N+1 query!
}

Τι συμβαίνει;

  1. Πρώτη ερώτηση φορτώνει όλες τις θέσεις
  2. Για κάθε θέση, πρόσβαση Category ενεργοποιεί ένα ερώτημα βάσης δεδομένων
  3. Για κάθε θέση, πρόσβαση Comments πυροδοτεί άλλο ένα ερώτημα
  4. Αν έχεις 100 θέσεις, απλά εκτελέστηκες. 201 ερωτήματα!

Πρόβλημα 2: Proxy + Caching = Διαρροή μνήμης

// ❌ CATASTROPHIC: Lazy loading proxies + caching
public class BlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly BlogDbContext _context;

    public BlogPostCache()
    {
        var optionsBuilder = new DbContextOptionsBuilder<BlogDbContext>();
        optionsBuilder
            .UseNpgsql(connectionString)
            .UseLazyLoadingProxies();  // ⚠️ DANGER!

        _context = new BlogDbContext(optionsBuilder.Options);
    }

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            // ❌ These proxy entities hold references to _context
            _cachedPosts = await _context.BlogPosts.ToListAsync();
        }
        return _cachedPosts;
    }
}

Γιατί αυτό είναι καταστροφικό:

  • Οι Proxy οντότητες διατηρούν μια αναφορά στις DbContext
  • Η DbContext διατηρεί αναφορά σε όλες τις ιχνηλατούμενες οντότητες
  • Η κρυψώνα σου τώρα εμποδίζει ολόκληρο το γράφημα του αντικειμένου να είναι σκουπίδι.
  • Κάθε φορά που έχετε πρόσβαση σε μια ιδιοκτησία πλοήγησης, μπορεί να προκαλέσει ερωτήματα χρησιμοποιώντας το Παλιά, εγκλωβισμένα συμφραζόμενα
  • Η μνήμη μεγαλώνει αδέσμευτη καθώς φορτώνεις περισσότερα δεδομένα
  • Τελικά θα ξεμείνεις από πισίνες μνήμης ή εξατμίσεων.

Η Λύση: Να Είστε Ακριβής

// ✅ NEVER use lazy loading proxies - always be explicit
optionsBuilder
    .UseNpgsql(connectionString);
    // NO .UseLazyLoadingProxies()!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public Category Category { get; set; }  // NOT virtual
    public List<Comment> Comments { get; set; }  // NOT virtual
}

// ✅ Explicit eager loading - you control what's loaded
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();

// ✅ Or use split queries for better performance
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()
    .ToListAsync();

// ✅ Or use projection to DTOs (best for caching)
var posts = await _context.BlogPosts
    .Select(p => new PostDto
    {
        Title = p.Title,
        CategoryName = p.Category.Name,
        CommentCount = p.Comments.Count
    })
    .ToListAsync();

// ✅ If you MUST cache, use AsNoTracking and no proxies
public class SafeBlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            using var context = await _contextFactory.CreateDbContextAsync();

            _cachedPosts = await context.BlogPosts
                .Include(p => p.Category)
                .Include(p => p.Comments)
                .AsNoTracking()  // Critical for caching!
                .ToListAsync();
        }
        return _cachedPosts;
    }
}

Όταν Proxies μπορεί να είναι αποδεκτή (κατανόηση των συναλλαγών):

Η τεμπέλα φόρτωσης μπορεί να είναι αποδεκτή ΜΟΝΟ όταν:

  1. Έχεις βραχείας διάρκειας Περιεχόμενα πλαίσια (π.χ. ανά αίτηση HTTP)
  2. Εσύ Ποτέ. φορείς κρυφής μνήμης
  3. Είσαι εντάξει με την απόδοση του Ν+1
  4. Είσαι πρωτοτυπικός και θα βελτιστοποιηθείς αργότερα.
  5. □ Η ομάδα σας κατανοεί πλήρως τις επιπτώσεις

Αλλά ακόμα και τότε, ρητή Include() είναι σχεδόν πάντα η καλύτερη επιλογή επειδή:

  • Φτιάχνει τη φόρτωση δεδομένων σαφής και προφανής
  • Είναι ευκολότερο να βελτιστοποιηθεί (μπορείς να δεις τι φορτώνεται)
  • Είναι αποτρέπει τυχαία ερωτήματα N+1
  • Λειτουργεί σωστά με caching και μακροχρόνια περιβάλλοντα
  • Είναι το συνιστώμενη προσέγγιση από την ομάδα EF Core

3. DbContext θέματα ζωής

Για περισσότερες λεπτομέρειες σχετικά με τη διαχείριση της διάρκειας ζωής DbContext στην παραγωγή, δείτε το άρθρο μου σχετικά με Μεταναστεύσεις EF Ο Σωστός Τρόπος.

Το πρόβλημα:

// ❌ NEVER do this - singleton DbContext
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<BlogDbContext>();  // WRONG!
}

// ❌ Also wrong - storing context in static field
public static class DataAccess
{
    private static BlogDbContext _context = new BlogDbContext();

    public static async Task<BlogPost> GetPostAsync(int id)
    {
        return await _context.BlogPosts.FindAsync(id);
    }
}

Γιατί είναι λάθος:

  • DbContext ί ας όχι από νήμα-ασφαλές
  • Ταυτόχρονα αιτήματα θα προκαλέσουν διαφθορά δεδομένων
  • Αλλαγή ιχνηλάτη αυξάνεται επ 'αόριστον
  • Εξάντληση της ομάδας σύνδεσης
  • Στοιχεία από την κρύπτη

Η Λύση:

// ✅ Use scoped lifetime (default in ASP.NET Core)
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

// ✅ Or use DbContext factory for background services
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

public class BlogBackgroundService
{
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task ProcessPostsAsync()
    {
        // Create a new context for this operation
        using var context = await _contextFactory.CreateDbContextAsync();

        var posts = await context.BlogPosts.ToListAsync();
        // Process posts...
    }
}

4. Απροσδόκητα Συμπεριλαμβάνει στις Ιδιότητες Πλοήγησης

Το πρόβλημα:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public List<Comment> Comments { get; set; }
}

// You query one post...
var post = await _context.BlogPosts.FirstAsync();

// Add a new comment
var newComment = new Comment { Content = "Great post!" };
post.Comments.Add(newComment);

await _context.SaveChangesAsync();

// ❌ EF Core saves the comment, BUT...
// If Comments wasn't loaded, you just lost all existing comments!
// The collection is empty, so EF thinks there are no other comments

Η Λύση:

// ✅ Always load navigation properties before modifying
var post = await _context.BlogPosts
    .Include(p => p.Comments)
    .FirstAsync(p => p.Id == postId);

post.Comments.Add(newComment);
await _context.SaveChangesAsync();

// Or add directly to the DbSet
_context.Comments.Add(new Comment
{
    BlogPostId = postId,
    Content = "Great post!"
});
await _context.SaveChangesAsync();

5. Async vs Sync Mixing

Το πρόβλημα:

// ❌ Mixing sync and async - deadlock risk!
public async Task<BlogPost> GetPostAsync(int id)
{
    var post = _context.BlogPosts
        .Where(p => p.Id == id)
        .FirstOrDefault();  // Sync method in async context!

    return post;
}

// ❌ Even worse - blocking async code
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id)
        .Result;  // DEADLOCK RISK!
}

Η Λύση:

// ✅ Use async all the way
public async Task<BlogPost> GetPostAsync(int id)
{
    return await _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id);
}

// ✅ Or use sync all the way (not recommended for ASP.NET Core)
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefault(p => p.Id == id);
}

EF πυρήνας 10: Τι είναι νέο και σπάσιμο αλλαγών

Με EF πυρήνας 10 NET 10, υπάρχουν πολλές σημαντικές αλλαγές που πρέπει να γνωρίζουμε κατά την αναβάθμιση. Για την πλήρη λίστα, δείτε Σπάζοντας τις αλλαγές στο πυρήνα EF 10.

Απαιτήσεις χρόνου λειτουργίας

Το EF Core 10 απαιτεί NET 10. NET 8, .NET 9, ή .NET πλαίσιο. Αυτή είναι η πιο σημαντική αλλαγή - εξασφαλίζουν τους στόχους του έργου σας net10.0 πριν από την αναβάθμιση.

Ερωτηματολόγια σχετικά με τις αλλαγές

1. Παράμετρος μετάφραση συλλογής (προκαθορισμένη αλλαγή)

Ο πυρήνας EF 10 αλλάζει τον τρόπο Contains() με συλλογές in-memory μεταφράζεται σε SQL. Προηγουμένως, EF Core χρησιμοποιείται OpenJson() (SQL Server) ή παρόμοια. Συστοιχίες παραμέτρων Τα οποία παρέχουν καλύτερο σχέδιο έρευνας.

Αντίκτυπος: Μπορείτε να δείτε διαφορετικά SQL που παράγονται για ερωτήματα όπως:

var ids = new List<int> { 1, 2, 3, 4, 5 };
var posts = await _context.BlogPosts
    .Where(p => ids.Contains(p.Id))
    .ToListAsync();

EF Core 8/9 (OpenJson):

SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" IN (SELECT value FROM OPENJSON(@__ids_0))

Πυρήνας EF 10 (Βελτιώσεις Παραμέτρου):

SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" = ANY(@__ids_0)  -- PostgreSQL
-- Or: WHERE b."Id" IN (@__ids_0_0, @__ids_0_1, @__ids_0_2, ...) -- SQL Server

Εάν αισθανθείτε οπισθοδρόμηση απόδοσης, να επανέλθει στην παλιά συμπεριφορά:

// SQL Server
optionsBuilder.UseSqlServer(connectionString,
    o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));

// PostgreSQL - generally parameter arrays work well, but you can opt out if needed

2. ΕκτελέστεUpdateAsync αλλαγή υπογραφής

Η ExecuteUpdateAsync υπογραφή έχει αλλάξει για να υποστηρίξει τη μη-έκφραση lambdas. Αυτό είναι πιο ευέλικτο, αλλά σπάει τον κώδικα που έχτισε δέντρα έκφρασης προγραμματικά:

Παλαιός τρόπος (EF Core 7-9):

// This still works
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));

Νέα σε πυρήνα EF 10 - Λάμδα μη-έκφρασης:

// Now you can include custom logic!
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters =>
    {
        setters.SetProperty(p => p.IsArchived, true);
        setters.SetProperty(p => p.UpdatedAt, DateTime.UtcNow);
        // Can now include conditional logic, loops, etc.
    });

3. Σύνθετη ονομασία στήλη τύπου

Ο πυρήνας EF 10 αλλάζει τον τρόπο με τον οποίο ονομάζονται οι στήλες του σύνθετου τύπου για την πρόληψη της διαφθοράς των δεδομένων:

EF Core 9:

NestedComplex_Property

EF πυρήνας 10:

OuterComplex_NestedComplex_Property

Επιπτώσεις στη μετανάστευση: Εάν έχετε υφιστάμενους πίνακες με πολύπλοκους τύπους, μπορεί να χρειαστεί να μετονομάσετε στήλες ή να ρυθμίσετε ρητά ονόματα στήλης:

modelBuilder.Entity<Order>()
    .ComplexProperty(o => o.ShippingAddress)
    .Property(a => a.Street)
    .HasColumnName("ShippingAddress_Street"); // Explicit name

SQL Server / Azure SQL συγκεκριμένες αλλαγές

Προκαθορισμένος τύπος δεδομένων JSON

Για Azure SQL βάση δεδομένων ή SQL Server 2025 (επίπεδο συμβατότητας 170+), προεπιλογές EF Core 10 για τη νέα μητρική JSON Τύπος δεδομένων αντί του NVARCHAR(MAX).

Για να αποσυνδεθείς. (αν χρειάζεστε πίσω συμβατότητα):

optionsBuilder.UseAzureSql(connectionString,
    o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior

Λίστα ελέγχου αναβάθμισης

Κατά την αναβάθμιση από τον πυρήνα EF 8/9 σε πυρήνα EF 10:

  1. □ Ενημέρωση του πλαισίου-στόχου στο net10.0
  2. Ενημέρωση όλων Microsoft.EntityFrameworkCore.* συσκευασίες έως 10.x
  3. Ενημέρωση@ info: whatsthis Npgsql.EntityFrameworkCore.PostgreSQL έως 10 x
  4. Ανασκόπηση ερωτημάτων με χρήση Contains() με συλλογές για αλλαγές απόδοσης
  5. Δοκιμάστε κάθε κώδικα που χτίζει προγραμματικά ExecuteUpdateAsync εκφράσεις
  6. □ Ελέγξτε τα σύνθετα ονόματα στήλης τύπου εάν χρησιμοποιείτε τους τύπους του σύνθετου συγκροτήματος
  7. Χρήση στήλης SQL Server JSON εάν χρησιμοποιείτε Azure SQL/SQL Server 2025

Τι είναι νέο (Highlights)

  • Βελτιωμένη μετάφραση LINQ: Καλύτερη γενιά SQL για περίπλοκα ερωτήματα
  • Μη-έκφραση lambdas σε ExecuteUpdateAsync: Περισσότερη ευελιξία στις μαζικές ενημερώσεις
  • Καλύτερος χειρισμός παραμέτρων: Βελτιωμένο σχέδιο έρευνας
  • Ενισχυμένη υποστήριξη JSON: Native JSON type on SQL Server 2025
  • Βελτιώσεις των επιδόσεων: Γρηγορότερη υλοποίηση και παρακολούθηση αλλαγών

Χαρακτηριστικά επιδόσεων

  • Επιδόσεις ερωτήσεων20-50% από πάνω σε σύγκριση με Dapper για απλά ερωτήματα
  • Χρήση μνήμης: Πιο ψηλά λόγω αλλαγής εντοπισμού και παραγωγής πληρεξουσίου
  • Πρώτη ερώτηση: Αργή (συλλεξούσια και caching)
  • Επακόλουθες ερωτήσεις: Γρηγορότερα λόγω της κατάρτισης cache ερώτημα
  • Εισαγωγή/Ενημέρωση: Αυτόματη παρακολούθηση αλλαγών προσθέτει πάνω από το κεφάλι
  • Δραστηριότητες μαζικής μεταφοράς: Κακή απόδοση με προεπιλεγμένες μεθόδους (σκεφτείτε EFCore.BulkExtensions ή ΕκτελέστεUpdate/ExecuteDelete in EF Core 7+)

Βέλτιστες πρακτικές για τον πυρήνα EF

Γενικές κατευθυντήριες γραμμές

  1. Χρήση AsNoTracking () για ερωτήματα μόνο ανάγνωσης για τη μείωση της μνήμης από πάνω
  2. Αποφύγετε τις ερωτήσεις N+1 - χρήση Include() ή ερωτήσεις διάσπασης κατάλληλα
  3. Χρήση συντεταγμένων ερωτημάτων για επαναλαμβανόμενα σχέδια ερωτήσεων
  4. Σκεφτείτε το AssplitQuery () για σύνθετα προϊόντα περιλαμβάνει για την αποφυγή προϊόντων καρτεσιανής
  5. Χρήση παρτίδας για πολλαπλά ένθετα/ενημερώσεις
  6. Έργο για τους DTO νωρίς για τη μείωση της μεταφοράς δεδομένων και της χρήσης μνήμης
  7. ExecuteUpdate/ExecuteDelete (EF Core 7+) για εργασίες χύμα
  8. Πάντα log and review created SQL στην ανάπτυξη

Όταν Εργάζεστε με το PostgreSQL

  1. Χρήση αναζήτησης πλήρους κειμένου χαρακτηριστικά (Ο οδηγός μου.) αντί της LIKE Ερωτήσεις
  2. Άρθρο 4 παράγραφος 1 στοιχείο α) σημείο ii) και άρθρο 4 παράγραφος 1 στοιχείο β) του ΚΚΑ για ευέλικτα δεδομένα χωρίς σχήμα
  3. Χρήση τύπων συλλεκτών για συλλογές εντός οντοτήτων
  4. Ρύθμιση συγκέντρωσης σύνδεσης κατάλληλα για τον φόρτο εργασίας σας
  5. Δείξε μου τι έχεις να πεις. tsvector στήλες με δείκτες GIN
  6. Χρήση τύπων εύρους για τα όρια ημερομηνίας/ώρας

Προχωρώντας στο Μέρος 2

Στο επόμενο άρθρο, θα εξερευνήσουμε:

  • ΝτάππερCity name (optional, probably does not need a translation): Το γλυκό σημείο micro-ORM
  • Ακατέργαστο ADO.NET/Npgsql: Μέγιστη απόδοση και έλεγχος
  • Αντικείμενο Βιβλιοθήκες Χαρτογράφησης: Mapster vs AutoMapper
  • Υβριδικές προσεγγίσεις: Συνδυάζοντας τον πυρήνα EF και τη σκούπα (CQRS μοτίβο)
  • Βαθμολογήσεις επιδόσεων: Συγκρίσεις πραγματικού κόσμου
  • Απόφαση Matrix: Επιλέγοντας το σωστό εργαλείο για το σενάριο σας

Αναφορές και Περαιτέρω Ανάγνωση

Σχετικά άρθρα σε αυτό το blog:


Στο Μέρος 2, θα βουτήξουμε στο Ντάπερ, ωμό Npgsql, και θα εξερευνήσουμε πώς να συνδυάσουμε πολλαπλές προσεγγίσεις για βέλτιστη απόδοση και συντηρησιμότητα.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.