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

<!-- category -- .NET, EF Core, PostgreSQL, Performance, Database -->
<datetime class="hidden">2025-12-03T14:00</datetime>

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:

- [Bloggaavien viestien (osa 1) lisätietokehys](/blog/addingentityframeworkforblogpostspt1) - Perustetaan EF Core tyhjästä
- [EF:n muuttoliikkeet oikealla tavalla](/blog/efmigrationstherightway) - Miten käsitellä muuttoliikkeitä kunnolla tuotannossa
- [Tekstin haku kokonaisuudessaan (Pt 1)](/blog/textsearchingpt1) - PostgreSQL-kokotekstihaku EF Corella
- [Nykyaikainen CQRS ja tapahtumakilpailu](/blog/moderncqrsandeventsourcing) - Kehittyneet arkkitehtoniset kuviot EF Corella

## 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ä:

```mermaid
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

```mermaid
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"](https://learn.microsoft.com/en-us/ef/core/) On Microsoftin lippulaiva ORM, joka tarjoaa täydellisen abstraktion tietokantaan. Se tukee PostgreSQL kautta [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.npgsql.org/efcore/) palveluntarjoaja.

Käytännöllisiä ohjeita EF Coren perustamiseen hankkeessasi, katso artikkelini aiheesta [Entiteettikehyksen lisääminen blogikirjoituksiin](/blog/addingentityframeworkforblogpostspt1).

### 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](/blog/efmigrationstherightway))
- **LINQ-palveluntarjoaja**: Tyyppiturvalliset kyselyt käyttäen C#-kielirakennetta
- **Lazy/Eager -lataus**: Joustavat lastausstrategiat lähiyhteisöille
- **Advanced PostgreSQL Features**: Kokotekstihaku ([katso artikkelini](/blog/textsearchingpt1)), JSON-pylväitä, sarjoja, kantamatyyppejä
- **Estäjät ja tapahtumat**: Laaja-alaisia kysymyksiä

### Esimerkki: Perus CRUD EF Corella

```csharp
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:

```csharp
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](/blog/efmigrationstherightway))
- 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

```csharp
// 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:**

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

**Luotu SQL ([EF Core 8+](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)):**

```sql
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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) Tätä suuntausta jatketaan vielä paremmalla parannuksella.

### Esimerkki: Liity mukaan (ennen EF Core 5)

**C#-koodi:**

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

**Vanha SQL (EF Core 3.1 – Cartesian Explosion):**

```sql
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ä:**

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

**Luotu SQL (Multiple Questions):**

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

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

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

**C#-koodi:**

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

**Luotu SQL:**

```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:**

```csharp
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:**

```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):**

```csharp
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+):**

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

**Luotu SQL (Single Query!):**

```sql
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:**

```csharp
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:**

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

**Luotu SQL:**

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

### Esimerkki: Kompleksinen aggregaatio

**C#-koodi:**

```csharp
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):**

```sql
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](/blog/textsearchingpt1).

**C#-koodi:**

```csharp
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:**

```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:**

```csharp
// ❌ 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:**

```csharp
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**

```csharp
// ❌ 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**

```csharp
// ❌ 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**

```csharp
// ✅ 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](/blog/efmigrationstherightway).

**Ongelma:**

```csharp
// ❌ 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:**

```csharp
// ✅ 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:**

```csharp
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:**

```csharp
// ✅ 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:**

```csharp
// ❌ 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:**

```csharp
// ✅ 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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) .NET 10:n ohessa julkaistaan useita tärkeitä muutoksia, joista kannattaa olla tietoinen päivitettäessä. [EF-ytimen muutosten rikkominen 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes).

### 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:

```csharp
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):**

```sql
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):**

```sql
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:

```csharp
// 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):**

```csharp
// 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:**

```csharp
// 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:

```csharp
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):

```csharp
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ä
7. 

### 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 kysely**Hidas (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](https://github.com/borisdj/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](/blog/textsearchingpt1)) `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

- ["Yhteenliittymän ydindokumentaatio"](https://learn.microsoft.com/en-us/ef/core/)
- [Npgsql Yksikön viitekehys ydinpalvelujen tarjoaja](https://www.npgsql.org/efcore/)
- [EF Core Performance](https://learn.microsoft.com/en-us/ef/core/performance/)
- [Mitä uutta EF-ytimessä 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Muutokset EF-ytimessä 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes)
- [Mitä uutta EF-ytimessä 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)
- [PostgreSQL-dokumentaatio](https://www.postgresql.org/docs/)

**Aiheeseen liittyvät artikkelit tästä blogista:**

- [Entiteettikehyksen lisääminen blogikirjoituksiin](/blog/addingentityframeworkforblogpostspt1)
- [EF:n muuttoliikkeet oikealla tavalla](/blog/efmigrationstherightway)
- [Täydellinen tekstihaku EF-ytimellä](/blog/textsearchingpt1)
- [Nykyaikainen CQRS ja tapahtumakilpailu](/blog/moderncqrsandeventsourcing)

---


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