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

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

Wednesday, 03 December 2025

//

21 minute read

Під час створення програм .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, подивіться мої інші статті:

Зміст

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ядро блокування сутності is Microsoft' s flagshship ORM, надаючи повну абстракцію над вашою базою даних. Вона підтримує PostgreSQL за допомогою Npgsql.EntityFrameworkCore.PostgreSQL провайдер.

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

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

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

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

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

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

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

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

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

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

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

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

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

ECF Core з Raw SQL

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

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

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

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

    await _context.Database.OpenConnectionAsync();

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

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

    return results;
}

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

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

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

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

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

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

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

Перегляд створеного SQL

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

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

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

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

        return await query.ToListAsync();
    }
}

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

C# LINQ:

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

Створений SQL (EF Core 8+):

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

Зауважте, як EF Core 8+ створює чисту, ефективну SQL з належним параметром. EF Core 10 продовжує цю тенденцію ще більшим поліпшенням.

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

C# код:

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

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

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# Код з розділеним запитом:

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

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

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

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

C# код:

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

Створений SQL:

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

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

C# код:

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

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

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

Створений SQL:

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

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

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

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

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

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

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

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

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

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

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

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

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

Старий шлях:

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

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

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

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

Створений SQL:

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

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

C# код:

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

Створений SQL (EF Core 8+10):

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

Повнотекстовий пошук PostgreSQL

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

C# код:

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

Створений SQL:

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

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

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

Проблема:

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

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

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

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

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

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

Вирішення:

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

// ❌ 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 = Прозора пам' ять

// ❌ 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 підтримує посилання на всі об' єкти, за якими ведеться стеження
  • Ваш кеш дозволяє зібрати весь об' єктний граф сміття
  • Кожного разу, коли ви отримуєте доступ до властивості навігації, вона може викликати запити за допомогою старий, кешований контекст
  • Під час завантаження додаткових даних пам' ять стає недоступною
  • У вас, врешті-решт, закінчиться пам'ять або вихлопні басейни.

Вирішення.

// ✅ 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 у виробництві, дивіться мою статтю про ЕФФ - мігрує на правильний шлях.

Проблема:

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

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

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

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

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

Вирішення:

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

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

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

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

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

4. Незмінене включення у властивості навігації

Проблема:

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

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

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

await _context.SaveChangesAsync();

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

Вирішення:

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

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

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

5. Синхронізація/ Змішування синхронізації

Проблема:

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

    return post;
}

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

Вирішення:

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

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

Корінь: Що нового і розриву?

З EF Core 10 10 - й випуск журналу " Пробудись! ," виданий поруч з сайтом NET, містить декілька важливих змін, про які слід пам'ятати під час оновлення. Breaking changes in EF Core 10.

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

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

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

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

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

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

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

EF Core 8/ 9 (OpenJson):

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

EF Core 10 (Зонаметри):

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

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

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

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

2. Виконати команду UpdateAsync Зміна підпису

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

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

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

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

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

3. Складений тип набору стовпчиків

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

EF Core 9:

NestedComplex_Property

EF Core 10:

OuterComplex_NestedComplex_Property

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

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).

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

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 або Виконати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. Використовувати повнотекстовий пошук Можливості (мій провідник) замість 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)
  • Швидкодія- Бенхмарки: Порівняння у реальному світі
  • Матриця визначення: Виберіть правильний інструмент для вашого сценарію

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

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


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

Finding related posts...
logo

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