Back to "Tietojen käyttö .NETissä: ORM- ja kartoitusstrategioiden vertailu (osa 1 - Yksikön toimintakehyksen ydin)"

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

Tietojen käyttö .NETissä: ORM- ja kartoitusstrategioiden vertailu (osa 1 - Yksikön toimintakehyksen ydin)

Wednesday, 03 December 2025

Kun rakennat .NET-sovelluksia, yksi tärkeimmistä arkkitehtonisista päätöksistäsi on se, miten käsittelet datan saantia ja esinekartoitusta. .NET-ekosysteemi tarjoaa monipuolisen valikoiman lähestymistapoja täyslaidallisista ORM-laitteista paljain jaloin metallisiin SQL-suorituksiin. Jokaisessa lähestymistavassa on omat kompromissinsa suorituskyvyn, kehittäjän tuottavuuden, tyyppiturvallisuuden ja säilyvyyden suhteen.

Tässä kattavassa kaksiosaisessa oppaassa tutustumme .NETin suosituimpiin datankäyttökuvioihin. Samalla kun käytämme esimerkeissämme PostgreSQL:ää Npgsqlin kanssa (koska se on tämän blogin valtti), käsitteet, kuviot ja sovitukset koskevat yhtä lailla SQL-palvelinta, MySQL:tä, SQLite:tä ja muita suhteellisia tietokantoja. Periaatteet pysyvät samoina - vain SQL:n murre ja tietyt erityispiirteet eroavat toisistaan.

Osa 1 (tämä artikla) keskittyy Entity Framework Coreen, SQL-sukupolveen ja yhteisiin sudenkuoppiin. 2 osa Se kattaa Dapperin, raa'an ADO.NETin, esinekartoituskirjastot ja hybridilähestymistavat.

Aiheeseen liittyvät artikkelit

Jos olet kiinnostunut käytännön EF Core -toteutuksista, tutustu muihin artikkeleihini:

Sisällys

Datayhteyksien taajuus

.NET-datakenttä voidaan visualisoida taajuudeksi:

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

Kun siirryt vasemmalta oikealle, saat suorituskykyä ja hallintaa, mutta menetät mukavuutta ja automaattisia ominaisuuksia. Tutkitaan jokainen lähestymistapa yksityiskohtaisesti.

Datan saantivirtojen vertailu

Tässä visuaalinen vertailu siitä, miten kukin lähestymistapa käsittelee tyypillistä kyselyä:

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

Performance vs. kehittäjä Tuottavuuskauppa

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

"Yhteenliittymän ydin: täysipiirteinen ORM"

"Yhteenliittymän ydin" On Microsoftin lippulaiva ORM, joka tarjoaa täydellisen abstraktion tietokantaan. Se tukee PostgreSQL kautta Npgsql.EntityFrameworkCore.PostgreSQL palveluntarjoaja.

Käytännöllisiä ohjeita EF Coren perustamiseen hankkeessasi, katso artikkelini aiheesta Entiteettikehyksen lisääminen blogikirjoituksiin.

Tärkeimmät ominaisuudet

  • Vaihda jäljitystä: Seuraa automaattisesti kokonaisuutta ja luo sopivan SQL:n
  • Muuttoliikkeet: Ensimmäinen koodiluettelon hallinta ja versioiden valvonta (ks. EF:n muuttoliikkeet oikealla tavalla)
  • LINQ-palveluntarjoaja: Tyyppiturvalliset kyselyt käyttäen C#-kielirakennetta
  • Lazy/Eager -lataus: Joustavat lastausstrategiat lähiyhteisöille
  • Advanced PostgreSQL Features: Kokotekstihaku (katso artikkelini), JSON-pylväitä, sarjoja, kantamatyyppejä
  • Estäjät ja tapahtumat: Laaja-alaisia kysymyksiä

Esimerkki: Perus CRUD EF Corella

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 Core ja raaka SQL

EF Core tukee myös raakaa SQL-kyselyä, kun tarvitset lisää kontrollia:

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;
}

Milloin käytetään EF-ydintä

Käytä EF-ydintä, kun:

  • Uuden sovelluksen rakentaminen kehittyvillä skeemavaatimuksilla
  • Tarvitset vahvan kirjoittamisen ja kokoajan kyselyn validoinnin
  • Muuttoliikkeet ja skeemaversiot ovat tärkeitä (ks. muutto-oppaani)
  • Tiimisi tekee mieluummin töitä esineiden kuin SQL:n kanssa
  • Käytät monimutkaisia domain-malleja, joissa on suhteita
  • Kehitysnopeus on kriittisempää kuin raaka suorituskyky
  • Tarvitset tietokannan siirrettävyyden (vaikkakin PostgreSQL-ominaisuus lukitsee sinut sisään)

Vältä EF-ydintä, kun:

  • Maksimisuorituskyky on kriittinen (korkeatehoiset API-rajapinnat, eräkäsittely)
  • Sinulla on monimutkaisia, käsin viritettyjä SQL-kyselyitä
  • Kyselysi eivät riitä graafien vastustamiseen
  • Jokaiseen SQL-lausuntoon tarvitaan hienopiirteistä valvontaa
  • Muistinkäyttö on kriittinen rajoitus (muutosten seuraaminen yläpuolella)
  • Työskentelet perintökuvien kanssa, jotka eivät kartoita konventteja

EF Core SQL Generation: ymmärrys siitä, mitä toteutetaan

Yksi tärkeimmistä osa-alueista EF Coren tehokkaassa käytössä on sen tuottaman SQL:n ymmärtäminen. EF Core on vuosien varrella parantanut merkittävästi SQL-sukupolvea, mutta on tärkeää varmistaa, että kyselyt lähetetään PostgreSQL:lle.

Näkeminen luotu 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();
    }
}

Esimerkki: yksinkertainen kysely

C# LINQ:

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

Luotu 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

Huomaa, miten EF Core 8+ tuottaa puhtaan ja tehokkaan SQL:n, jossa on asianmukainen parametrisointi. EF-ydin 10 Tätä suuntausta jatketaan vielä paremmalla parannuksella.

Esimerkki: Liity mukaan (ennen EF Core 5)

C#-koodi:

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

Vanha 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"

Tämä luo Kartesaanituote - jos viestissä on 10 kommenttia, tuo rivi toistetaan 10 kertaa!

Esimerkki: Split-kyselyt (EF Core 5+)

C#-koodi jaettuna kyselyllä:

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

Luotu SQL (Multiple Questions):

-- 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"

Tämä poistaa Cartesian tuote ja on usein paljon nopeammin kokoelmiin!

Esimerkki: Suodatettu sisällys (EF Core 5+)

C#-koodi:

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

Luotu 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"

Esimerkki: JSON-kolumnikyselyt (EF Core 7+)

C#-koodi:

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();

Luotu SQL:

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

EF Core 7+ voi kääntää JSON-kiinteistön pääsyn PostgreSQL JSON-operaattoreihin!

Esimerkki: Bulk Update (EF Core 7+ ExecutePäivitys)

Vanha tapa (tehoton):

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!

New Way (EF Core 7+):

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

Luotu SQL (Single Query!):

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

Tämä on massiivinen parannus - yksi SQL-lausunto N:n sijaan!

Esimerkki: Bulk Delete (EF Core 7+)

Vanha tapa:

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

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

Uusi tapa:

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

Luotu SQL:

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

Esimerkki: Kompleksinen aggregaatio

C#-koodi:

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();

Luotu 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 täystekstihaku

Syvempi sukellus kokotekstihakuun, katso artikkelini aiheesta täystekstihaun toteuttaminen EF Corella.

C#-koodi:

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();

Luotu 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

Kriittiset EF-ytimen varoitukset ja salahaudat

1. Muuta muistivuotoja

Ongelma:

// ❌ 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!
    }
}

Miksi se on ongelma?

  • Seuratut kokonaisuudet pysyvät muistissa koko DbContext
  • Muutosjäljitin ylläpitää viittauksia, estää jätteiden keräämisen
  • Pitkäikäiset kontekstit (esim. singletonit) = muistivuoto
  • ASP.NET Coressa konteksti on oletusarvoinen (hyvä!)
  • Mutta jos piilotat jäljitetyt olennot, olet pulassa

Ratkaisu:

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 Generation and Lazy Loading Dangers

KRIITTINEN VAROITUS: ÄLÄ KÄYTÄ EF CORE -PROJEKTIA, JOS KOSKET YHTEISÖITÄ

Laiskat kuormauspäätteet + välimuistit = TAKAttu MUISTO LEAK

Jos välimuistissa DbContext-tapaukset tai välimuistin kohteet ladattuna, TAPAHTUMA Vuotomuisti. Valtakirjamekanismissa säilytetään viittaukset DbContextiin, mikä estää jätteiden keräämisen. Tämä on yksi yleisimmistä ja vaarallisimmista virheistä EF Core -sovelluksissa.

Nyrkkisääntö: Sisällytä kokoelmat aina erikseen .Include(). Käytä valtakirjoja vain, jos ymmärrät täysin vaihtokaupat etkä koskaan, koskaan kätki välityspalvelimia.

Ongelma 1: N+1 Query Nightmare

// ❌ 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!
}

Mitä tapahtuu:

  1. Ensimmäinen kysely lataa kaikki viestit
  2. FINREP:n puolesta jokainen postaus, pääsy Category käynnistää tietokantakyselyn
  3. FINREP:n puolesta jokainen postaus, pääsy Comments laukaisee toisen kyselyn
  4. Jos sinulla on sata virkaa, teloitit juuri 201 kyselyä!

Ongelma 2: Proxy + Caching = muistivuoto

// ❌ 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;
    }
}

Tämä on katastrofaalista:

  • Proxy-yhteisöt pitävät yllä viittausta omiinsa DbContext
  • Erytropoietiini DbContext ylläpitää viittausta kaikkiin seurattaviin yhteisöihin
  • Kätkösi estää nyt koko esinekaavion keräämisen roskiin
  • Aina kun käytät navigointiominaisuutta, se voi käynnistää kyselyitä vanha, välimuistissa
  • Muisti kasvaa rajattomaksi, kun lataat lisää dataa
  • Loppujen lopuksi muistisi tai pakoputkien altaat loppuvat

Ratkaisu: Ole eksplisiittinen

// ✅ 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;
    }
}

Kun valtakirjat voivat olla hyväksyttäviä (selkeät kaupat):

Laiskat latauspäätteet saattavat olla hyväksyttäviä VAIN silloin, kun:

  1. â € â € € € lyhytaikainen laajennetut asiayhteystiedot (esim. HTTP-pyyntöä kohti)
  2. â € _ sinä ei koskaan Välimuistiyhteisöt
  3. N+1 query-esitys sopii sinulle
  4. Olet prototyypitys ja optimoit myöhemmin
  5. Tiimisi ymmärtää täysin seuraukset

Mutta vielä silloinkin, selväsanaisesti Include() on lähes aina parempi valinta, koska:

  • Se tekee datan lataamisesta yksiselitteinen ja ilmeinen
  • Se johtuu siitä, että helpompi optimoida Näkee, mitä lastataan.
  • Se estää satunnaiset N+1-kyselyt
  • Se toimii oikein välilyönneillä ja pitkäikäisillä asiayhteyksillä
  • Se johtuu siitä, että suositeltu lähestymistapa EF Core -tiimin toimesta

3. DbContext Lifetime issues

Lisätietoja DbContextin käyttöiän hallinnasta tuotannossa on artikkelissani aiheesta EF:n muuttoliikkeet oikealla tavalla.

Ongelma:

// ❌ 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);
    }
}

Miksi se on väärin?

  • DbContext is ei kierteiltään turvallinen
  • Rinnakkaiset pyynnöt aiheuttavat tietokorruptiota
  • Muutosjäljitin kasvaa loputtomiin
  • Liitäntäaltaan uupumus
  • Viivästyneet tiedot välimuistista

Ratkaisu:

// ✅ 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. Tahaton sisältää navigaation ominaisuuksia

Ongelma:

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

Ratkaisu:

// ✅ 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

Ongelma:

// ❌ 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!
}

Ratkaisu:

// ✅ 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 Core 10: Mikä on uutta ja murtavaa muutosta

yy) kanssa. EF-ydin 10 .NET 10:n ohessa julkaistaan useita tärkeitä muutoksia, joista kannattaa olla tietoinen päivitettäessä. EF-ytimen muutosten rikkominen 10.

Ajoaikaa koskevat vaatimukset

EF Core 10 vaatii .NET 10. Se ei toimi .NET 8, .NET 9, tai .NET Framework. Tämä on merkittävin muutos - varmista projektin tavoitteet net10.0 ennen parannusta.

Kyselyyn liittyvät murtavat muutokset

1. Paranneltu kokoelmakäännös (oletus muutettu)

EF Core 10 muuttaa tapaa, jolla Contains() muistinsisäiset kokoelmat käännetään SQL:lle. Aiemmin käytetty EF Core OpenJson() (SQL-palvelin) tai vastaava. Nyt se on oletuksena parametriryhmät joka tarjoaa paremman kyselysuunnitelman välimuistista.

Vaikutus: Voit nähdä erilaisia SQL luotu kyselyitä, kuten:

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 Core 10 (Parametrin säteet):

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

Jos koet suoritusregressioita, palaa vanhaan käytökseen:

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

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

2. Suorita UpdateAsync Signature Change

Erytropoietiini ExecuteUpdateAsync Allekirjoitus on vaihtunut tukemaan ilmaisuttomuutta. Tämä on joustavampaa, mutta murtaa koodia, joka rakensi ilmaisupuita ohjelmallisesti:

Vanha tapa (EF Core 7-9):

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

Uusi EF Core 10:ssä – Ei-ilmaistua lambdat:

// 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. Monimutkaisen tyyppisen sarakkeen nimeäminen

EF Core 10 muuttaa, kuinka pesityt monimutkaiset saraketyypit nimetään datan korruption estämiseksi:

EF-ydin 9:

NestedComplex_Property

EF-ydin 10:

OuterComplex_NestedComplex_Property

Maahanmuuttovaikutukset: Jos sinulla on olemassa taulukoita, joissa on monimutkaisia tyyppejä, saatat joutua nimeämään sarakkeet uudelleen tai konfiguroimaan sarakkeen nimenomaiset nimet:

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

SQL-palvelin / Azure SQL -erityismuutokset

JSON-tietojen tyyppi Oletus

Azure SQL Database tai SQL Server 2025 (yhteensopivuustaso 170+), EF Core 10 -oletukset uudelle syntyperäiselle JSON Tietotyyppi, ei NVARCHAR(MAX).

Jättäytyä pois (jos tarvitset takautuvaa yhteensopivuutta):

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

Päivitetään tarkistuslistaa

Parannettaessa EF-ytimestä 8/9 EF-ytimeen 10:

  1. â € € # Päivitä tavoitekehys net10.0
  2. Päivitä kaikki@ info: whatsthis Microsoft.EntityFrameworkCore.* paketit kohteeseen 10.x
  3. â € € + Päivitys Npgsql.EntityFrameworkCore.PostgreSQL to 10.x
  4. Contains() Kokoelmat suoritusmuutoksille
  5. Testaa kaikki koodit, jotka ohjelmallisesti rakennetaan ExecuteUpdateAsync ilmauksia
  6. Tarkista kompleksin tyypin sarakkeen nimet, jos käytät pesiviä kompleksityyppejä

Mitä uutta (kohovalot)

  • Parannettu LINQ-käännös: Parempaa SQL-sukupolvea monimutkaisiin kyselyihin
  • Teloituksessa ilmeettömät lambdatPäivitäAsyncissä: Joustavampia kokonaispäivityksiä
  • Parannettu parametrien käsittely: Parannettu kyselysuunnitelma välimuistiin
  • JSON-tuen lisääminen: Native JSON -tyyppi SQL-palvelimella 2025
  • Suorituskyvyn parantaminen: Nopeampi konkretisoituminen ja muutosseuranta

Suorituskykyä koskevat ominaisuudet

  • Kyselytulos: 20-50 % yleiskuluja verrattuna Dapperiin yksinkertaisissa kyselyissä
  • Muistinkäyttö: Suurempi johtuen muutosseurannasta ja välityspalvelimien tuottamisesta
  • Ensimmäinen kyselyHidas (keräys ja välimuisti)
  • Seuraavat kyselyt: Nopeampi kootun kyselyvälimuistin takia
  • Lisää/Päivitä: Automaattinen muutosseuranta lisää ylimenoja
  • Bulk-operaatiot: Huono suoritus oletusmenetelmillä (ks. EFCore.BulkExtensions tai Suorita päivitys/toteuta poisto EF Core 7+:ssa)

EF-ytimen parhaat käytännöt

Yleiset suuntaviivat

  1. Käytä AsNoTracking () Luetut kyselyt, joilla vähennetään muistin kulumista
  2. Vältä N+1-kyselyitä - käyttö Include() tai jakaa kyselyt oikein
  3. Käytä koottuja kyselyitä Toistuville kyselymalleille
  4. Harkitse AsplitQuerya () Monimutkaisille aineille kartesialaisten tuotteiden välttäminen
  5. Käytä erittelyjä useita inserttejä/päivityksiä varten
  6. Project to DTOs Tiedonsiirron ja muistin käytön varhainen vähentäminen
  7. Velkaerän suoritusPäivitä/Päivitä delete (EF Core 7+) irtotavarana
  8. Kirjaa ja arvioi aina tuotettu SQL kehitteillä

Kun työskentelet PostgreSQL:n kanssa

  1. Käytä kokotekstihakua Oheispiirteet (oppaani) LIKE kyselyt
  2. Velkakirjat JSONB-sarakkeessa Joustavalle skeemattomalle datalle
  3. Käytä sarjojen tyyppejä kokoelmien osalta yhteisöissä
  4. Määrittele yhdyskunnan yhdistäminen kunnolla työtaakallesi
  5. Hakeudu tsvector sarakkeita jossa on GIN-indeksit
  6. Käytä kantamatyyppejä päivä- ja aikaväleille

Tulossa 2. osassa

Seuraavassa artikkelissa tutkimme:

  • Dopper: Mikro-ORM makea paikka
  • Raaka ADO.NET/Npgsql: Maksimaalinen suorituskyky ja ohjaus
  • Kohteen kartoitus kirjastoissa: Mapster vs. AutoMapper
  • Hybridilähestymistavat: Yhdistetään EF Core ja Dapper (CQRS-kuvio)
  • Suorituskyvyn vertailuarvot: Reaalimaailman vertailut
  • Päätös Matrix: Oikean työkalun valitseminen skenaarioosi

Referenssit ja jatkolukeminen

Aiheeseen liittyvät artikkelit tästä blogista:


Osassa 2 sukeltamme Dapperiin, raakaan Npgsqliin, ja tutkimme, miten yhdistellään useita lähestymistapoja optimaaliseen suorituskykyyn ja säilyvyyteen.

logo

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