# Доступ до даних у. NET: порівняння стратегій ORM і картування (Part 1 - Corement Framework Frame)

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

Під час створення програм .NET, одним з найважливіших архітектурних рішень, які ви зробите, є спосіб обробки доступу до даних і відображення об' єктів. Екосистема .NET пропонує багате розмаїття підходів, від повнофункціональних ORM до виконання Fole- Methal SQL. Кожен підхід має свій власний торговий підхід з точки зору продуктивності, продуктивності розробки, введіть безпеку і надійність.

У цьому універсальному посібнику з двох частин ми дослідимо найпопулярніші шаблони доступу до даних у. NET. У нашому прикладі ми використовуємо PostgreSQL з Npgsql (оскільки саме це і дає змогу користуватися цим блогом), концепції, шаблони і компроміси, які застосовуються до сервера SQL, MySQL, SQL та інших реляційних баз даних. Ці принципи залишаються однаковими - лише діалект SQL і деякі специфічні можливості відрізняються.

**Частина 1 (ця стаття)** Зосереджується на створенні Centity Framework Core, створення SQL і типових пастках.
**Частина 2** буде висвітлено Dapper, raw ADO.NET, об'єкти для відображення об'єктів і гібридні підходи.

## Супутні статті

Якщо ви цікавитеся практичною реалізацією EC Core, подивіться мої інші статті:

- [Додавання блоку сутності для дописів блогу (частина 1)](/blog/addingentityframeworkforblogpostspt1) - Налаштовування EF- ядра з нуля
- [ЕФФ - мігрує на правильний шлях](/blog/efmigrationstherightway) - Як правильно давати собі раду з міграцією у виробництві.
- [Повний пошук тексту (Pt 1)](/blog/textsearchingpt1) - Впровадження повнотекстового пошуку PostgreSQL за допомогою ядра EF
- [Сучасний CQRS і розвиток подій](/blog/moderncqrsandeventsourcing) - З додатковими архітектурними візерунками з ядром ЕФ

## Зміст

## Спектр доступу до даних

Ланцюжок. NET для доступу до даних можна візуалізувати як спектр:

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

Рухаючись зліва направо, ви здобуваєте швидкодію та контроль, але втрачаєте зручні та автоматичні можливості.

### Порівняння потоку даних

Ось візуальне порівняння того, як кожен підхід відповідає типовому запиту:

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

### Швидкодія/розробка продуктивної торгівлі розробником

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

## Ядро блокування сутності: Повнофункціональна ОРС

[Ядро блокування сутності](https://learn.microsoft.com/en-us/ef/core/) is Microsoft' s flagshship ORM, надаючи повну абстракцію над вашою базою даних. Вона підтримує PostgreSQL за допомогою [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.npgsql.org/efcore/) провайдер.

За практичними порадами щодо встановлення EC Core у вашому проекті, дивіться мою статтю про [Додавання блоку сутності для дописів блогу](/blog/addingentityframeworkforblogpostspt1).

### Можливості ключів

- **Змінити стеження**: Автоматично стежити за змінами сутності і створювати відповідні SQL
- **Міграції**: Керування схемами коду і керування версіями (див. [ЕФФ - мігрує на правильний шлях](/blog/efmigrationstherightway))
- **Постачальник LINQ**: Запити щодо безпеки типів за допомогою конструкцій C#
- **Завантаження Lazy/Eager**: Гнучка стратегія завантаження пов' язаних об' єктів
- **Додаткові можливості PostgreSQL**: Повнотекстовий пошук ([перегляд моєї статті](/blog/textsearchingpt1)), колонки JSON, масиви, типи діапазонів
- **Інтерцептори та події**: Видимі точки для перехресних турбот

### Приклад: основний CRUD з ядром EF

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

### ECF Core з Raw SQL

Крім того, EF Core підтримує сирі запити SQL, якщо вам потрібно додаткове керування:

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

### Коли використовувати ядро EF

**⇩ Використовувати EF core, якщо:**

- Побудова нової програми з вимогами схеми розвитку
- Вам потрібна перевірка часу вводу і збирання запитів
- Важливими є міграції і зміни схеми (див. [Мій провідник міграції](/blog/efmigrationstherightway))
- Ваша команда надає перевагу роботі з об' єктами над SQL
- Ви використовуєте складні домени-моделі з стосунками.
- Швидкість розробки критичніша, ніж сира швидкодія
- Вам слід заблокувати портування між базами даних (хоча і специфічні для PostgreSQL можливості)

**⇩ Вимкнено EF core, коли:**

- Максимальна швидкодія є критичною (додаткові інтерфейси, пакетна обробка)
- У вас є складні, налаштовані вручну запити SQL
- Ваші запити несумісні з об' єктними графами
- Вам потрібен контроль над кожним висновком SQL
- Використання пам' яті є критичним обмеженням (змінення стеження над головою)
- Ви працюєте з застарілими схемами, які не відносяться до конвенцій.

## Створення EF Core SQL: розуміння того, що виконується

Одним з найважливіших аспектів ефективного використання ECF Core є розуміння того, що генерує SQL. ECF Core значно покращило створення SQL протягом багатьох років, але важливо перевіряти запити, надіслані до PostgreSQL.

### Перегляд створеного 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();
    }
}
```

### Приклад: простий запит

**C# LINQ:**

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

**Створений 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
```

Зауважте, як EF Core 8+ створює чисту, ефективну SQL з належним параметром. [EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) продовжує цю тенденцію ще більшим поліпшенням.

### Приклад: Приєднатися до включення (до ядра EF 5)

**C# код:**

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

**Старий SQL (EF Core 3. 1 - Декартовий вибух):**

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

Це створює a **Декартовий продукт** - якщо допис складається з 10 коментарів, то рядок повторюється 10 разів!

### Приклад: розділювачі (EF Core 5+)

**C# Код з розділеним запитом:**

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

**Створений 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"
```

Це усуває декартовий продукт, і часто його використовують. **набагато швидше** на колекції!

### Приклад: фільтроване включення (EF Core 5+)

**C# код:**

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

**Створений 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"
```

### Приклад: JSON Column Questions (EF Core 7+)

**C# код:**

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

**Створений SQL:**

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

ECF Core 7+ може перекладати доступ до власності JSON операторам JSON!

### Приклад: масштабне оновлення (EF Core 7+Supupdate)

**Старий шлях (неефективний):**

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

**Новий шлях (EF Core 7+):**

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

**Створений SQL (Single request!):**

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

Це **масивний** покращення - одне твердження SQL замість N!

### Приклад: ulk Delete (EF Core 7+)

**Старий шлях:**

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

**Новий спосіб:**

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

**Створений SQL:**

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

### Приклад: складене складання

**C# код:**

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

**Створений 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

Щоб глибше зануритись у повнотекстовий пошук, прочитайте мою статтю [Впровадження повнотекстового пошуку за допомогою ядра EF](/blog/textsearchingpt1).

**C# код:**

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

**Створений 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
```

## біса ЕФове попередження і пастки

### 1. Змінити поділки пам' яті

**Проблема:**

```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!
    }
}
```

**Чому це проблема:**

- Речі, за якими стежать, залишаються у пам'яті протягом життя `DbContext`
- Стеження за зміною підтримує посилання, запобігаючи збірці відходів
- Контексти довгожителів (наприклад, однотони) = витік пам' яті
- У Core ASP.NET контекст типово вимірюється (добре!)
- Но если вы скрываете объекты отслеживания, у вас будут проблемы.

**Вирішення:**

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

### Покоління проксі - сервера і завантаження простори небезпеки

> **теперь ЦЕРИТИЧНА ПЕРЕСТОРОГА: НЕ ВЖИВАЙТЕ ПРАВЕДЛИВОСТІ, ЯКЩО ВИ ЗАЦІКАВЛЯЄТЕ ВПЛИВ**
> 
> Широке завантаження проксій + кеш = **ГАРАНТОВАНЕ МЕМОРІЯ ЛЕЕК**
> 
> Якщо ви кешуєте екземпляри DbContext або елементи кешу, завантажені проксіями, ви можете **WOW** пам' ять витоків. Механізм проксі- сервера підтримує посилання на DbContext, що запобігає збірці відходів. Це одна з найпоширеніших і небезпечних помилок у програмах EF Core.
> 
> **Правило великого пальця**: Завжди включати колекції явно з `.Include()`... використовувати проксі- сервери, лише якщо ви повністю розумієте компроміси і ніколи, ніколи не кешувати проксі- сервери.

**Проблема 1. Норма запиту N+1**

```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!
}
```

**Що відбувається:**

1. Перший запит завантажує всі дописи
2. for **кожен допис**, доступ `Category` запускає запит на базу даних
3. for **кожен допис**, доступ `Comments` запускає інший запит
4. Якщо у вас 100 постів, ви щойно стратили **201 запити**!

**Проблема 2: Проксі + Caching = Прозора пам' ять**

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

**Чому це катастрофічно:**

- Елементи проксі підтримують посилання на них `DbContext`
- The `DbContext` підтримує посилання на всі об' єкти, за якими ведеться стеження
- Ваш кеш дозволяє зібрати весь об' єктний граф сміття
- Кожного разу, коли ви отримуєте доступ до властивості навігації, вона може викликати запити за допомогою **старий, кешований контекст**
- Під час завантаження додаткових даних пам' ять стає недоступною
- У вас, врешті-решт, закінчиться пам'ять або вихлопні басейни.

**Вирішення.**

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

**Коли можуть бути прийнятні прокази (Зрозуміти, що відбувається з торговцями):**

Проксі завантаження можуть бути прийнятними протягом ONLY, якщо:

1. ♫ У вас є **коротке життя** Контексти обчислювальної області (напр., на запит HTTP)
2. ♫ Ви **ніколи** об' єкти кешу
3. ♫ Ви в порядку з швидкодією запиту N+1
4. ♪ You're prototyping and will оптимізовано пізніше
5. ▸ Ваша команда повністю розуміє суть справи.

Але навіть тоді, явно `Include()` майже завжди кращий вибір, тому що:

- Він завантажує дані **явні і очевидні**
- Це **спрощує оптимізацію** (ви бачите, що завантажується)
- Це **запобігти випадковим запитам N+1**
- Це працює належним чином з кешуванням і довгостроковими контекстами
- Це **рекомендований підхід** від команди EF core

### 3. Спіральні питання, пов'язані з часом життя DBContext

За детальнішою інформацією про керування життям DBContext у виробництві, дивіться мою статтю про [ЕФФ - мігрує на правильний шлях](/blog/efmigrationstherightway).

**Проблема:**

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

**Чому це неправильно:**

- `DbContext` є **не захищено від гілки**
- Регулярні запити призведуть до пошкодження даних
- Зміна стеження зростає нескінченно
- Виснаження басейну з' єднання
- Стале- дані з кешу

**Вирішення:**

```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. Незмінене включення у властивості навігації

**Проблема:**

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

**Вирішення:**

```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. Синхронізація/ Змішування синхронізації

**Проблема:**

```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!
}
```

**Вирішення:**

```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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) 10 - й випуск журналу " Пробудись! ," виданий поруч з сайтом NET, містить декілька важливих змін, про які слід пам'ятати під час оновлення. [Breaking changes in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes).

### Вимоги до виконання

**EF Core 10 потребує NET 10**. Його не запущено на . NET 8,. NET 9 або. NET Framework. Це найважливіша зміна - переконайтеся, що ціллю вашого проекту є `net10.0` до поновления.

### Зміни, пов' язані з розривом запиту

#### 1. Параметрізований переклад збірки (типово змінено)

ECF Core 10 змінює спосіб `Contains()` зі збірками у пам' яті буде перекладено на SQL. Раніше було використано EF- ядро `OpenJson()` (сервер SQL) або подібний. Тепер типовим значенням є **масиви параметрів** що забезпечує краще планування запиту.

**Зчеплення**: Ви можете бачити різні файли SQL, створені для запитів на зразок:

```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 (Зонаметри):**

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

**Якщо ви відчуваєте регресію швидкодії**, повернутися до старої поведінки:

```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. Виконати команду UpdateAsync Зміна підпису

The `ExecuteUpdateAsync` Підпис було змінено на підтримку лямбдів без виразу. Це гнучкіше, але **розбиває код, що побудований виразом дерева програмно**:

**Старий шлях (EF Core 7- 9):**

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

**Створити у EC Core 10 - не- виразних лямбдах:**

```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. Складений тип набору стовпчиків

ECF Core 10 змінює спосіб вкладання стовпчиків складеного типу, за допомогою яких можна запобігти пошкодженню даних:

**EF Core 9:**

```
NestedComplex_Property
```

**EF Core 10:**

```
OuterComplex_NestedComplex_Property
```

**Ефект міграції**: Якщо у вас є таблиці зі складними типами, ймовірно, вам слід буде перейменувати стовпчики або налаштувати явні назви стовпчиків:

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

### Сервер SQL / Azure SQL Особливі зміни

#### Типовий тип даних JSON

Для бази даних SQL Azure або SQL Server 2025 (рівень несумісності 170+), EF Core 10 типово для нової рідної `JSON` Тип даних замість `NVARCHAR(MAX)`.

**Щоб вибрати** (якщо вам потрібна зворотна сумісність):

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

### Оновлення списку перевірок

Під час оновлення з EF Core 8/9 до EF Core 10:

1. біса Перезапустити оболонок цілі до `net10.0`
2. ⇩ Передати всі `Microsoft.EntityFrameworkCore.*` пакунки до 10. x
3. ▸ Час `Npgsql.EntityFrameworkCore.PostgreSQL` до 10.x
4. ⇩ Повторення з використанням `Contains()` зі збірками для змін швидкодії
5. ⇩Випробовувати будь-який код, що створює программатично `ExecuteUpdateAsync` вирази
6. ⇩ Позначити складні назви колонок, якщо у них вкладені складні типи
7. Використання стовпчика SQL Server JSON, якщо використовується сервер SQL/ SQL 2025

### What's New (Highlights)

- **Покращений переклад LINQ**: Краще створення SQL для складних запитів
- **Не- виразні лямбда у DollupdateAsync**: Більш гнучкість у оновленні громіздких даних
- **Покращена обробка параметрів**: Покращений план кешування запитів
- **Покращена підтримка JSON**: Рідний тип JSON на сервері SQL 2025
- **Покращення швидкодії**: Швидка матеріалізація та зміна стеження

## Символи швидкодії

- **Швидкодія запиту**: 20- 50% над головою порівняно з Dapper для простих запитів
- **Використання пам' яті**: Вищий через зміну відслідковування і створення проксі- сервера
- **Перший запит**: Повільне (збірки вікон і кешування)
- **Подальші запити**: Швидкий через скомпільований кеш запитів
- **Inserts/Updates**: Автоматичне стеження додає надпис
- **Місткенькі дії**: Погана продуктивність з типовими методами (вважати [EFCore.BulkExtensions](https://github.com/borisdj/EFCore.BulkExtensions) або ВиконатиUpdate/ ExecuteDelete in EF Core 7+)

## Найкращі методи для EF- ядра

### Загальні напрямні

1. **Використовувати " NoTracking ")** запити лише для читання для зменшення пам' яті над головою
2. **Уникайте запитів N+1** - use `Include()` або достатньо розділити запити
3. **Використовувати скомпільовані запити** для повторюваних шаблонів запиту
4. **Розгляньмо приклад Асплієвої Книжки)** комплексний, у тому числі для уникнення декартових продуктів
5. **Використовувати пакетизацію** для декількох вставок/ подань
6. **Проект до DTO** ранній для зменшення використання передавання даних та пам'яті
7. **Leterage Exupdate/ExecuteDelete** (EF Core 7+) для операцій з масою
8. **Завжди записувати журнал і рецензування створено SQL** у розробці

### Під час роботи з PostgreSQL

1. **Використовувати повнотекстовий пошук** Можливості ([мій провідник](/blog/textsearchingpt1)) замість `LIKE` Запити
2. **Стовпчики Leberage JSONB** для гнучких даних за схемою
3. **Використовувати типи масивів** збірки у одиницях
4. **Налаштування набору з' єднаньName** належним чином для вашого завантаження робіт
5. **Індексувати ваш `tsvector` стовпчиків** з індексами GIN
6. **Використовувати типи діапазонів** для діапазонів дат/ часу

## Наближається частина 2

У наступній статті ми розглянемо:

- **Dapper**: солодке місце для мікро- ORM
- **Raw ADO. NET/ Npgsql**: Максимальна швидкодія і контроль
- **Бібліотеки, що розташовують об' єкти**: Mapster vs AutoMapper
- **Підходи гібриду**: Об' єднання EF Core і Dapper (взір CQRS)
- **Швидкодія- Бенхмарки**: Порівняння у реальному світі
- **Матриця визначення**: Виберіть правильний інструмент для вашого сценарію

## Посилання і подальше читання

- [Основна документація з блоку сутності](https://learn.microsoft.com/en-us/ef/core/)
- [Провайдер фреймів Npgsql для роботи з сутністю Npgsql](https://www.npgsql.org/efcore/)
- [Швидкодія ядра EF](https://learn.microsoft.com/en-us/ef/core/performance/)
- [What's New in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Breaking changes in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes)
- [What's New in EF Core 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)
- [Документація з PostgreSQL](https://www.postgresql.org/docs/)

**Супутні статті про цей блог:**

- [Додавання блоку сутності для дописів блогу](/blog/addingentityframeworkforblogpostspt1)
- [ЕФФ - мігрує на правильний шлях](/blog/efmigrationstherightway)
- [Повнотекстовий пошук за допомогою ядра EF](/blog/textsearchingpt1)
- [Сучасний CQRS і розвиток подій](/blog/moderncqrsandeventsourcing)

---


*У другій частині ми зануримося в Dapper, сирий Npgsql, і досліджуємо, як комбінувати кілька підходів для оптимальної продуктивності та стійкості.*