Back to "Datan käyttö .NETissä: ORM- ja kartoitusstrategioiden vertailu (osa 2 - Dopper, Raw SQL ja Hybrid Approachs)"

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

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

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

.NET Dapper Database Performance PostgreSQL

Datan käyttö .NETissä: ORM- ja kartoitusstrategioiden vertailu (osa 2 - Dopper, Raw SQL ja Hybrid Approachs)

Wednesday, 03 December 2025

Tervetuloa .NETin kattavan oppaan 2. osaan! In Osa 1Tutkimme perusteellisesti Entity Framework Corea, mukaan lukien SQL-sukupolvea, yleisiä salahautoja ja kriittistä varoitusta valtakirjoista ja välienselvittelystä.

Tässä artikkelissa tutustutaan kevyempiin vaihtoehtoihin ja siihen, miten voidaan yhdistää useita lähestymistapoja optimaaliseen suorituskykyyn:

  • Dopper: Mikro-ORM makea paikka
  • Raw ADO.NET/Npgsql: Maksimaalinen suorituskyky ja ohjaus
  • Kohteen kartoitus kirjastoissa: Kartta 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

Sisällys

Dopper: Micro-ORM

Dopper Se on Stack Overflow'n luoma kevyt, suorituskykyinen mikro-ORM. Se tarjoaa ADO.NETin päälle ohuen kerroksen, joka käsittelee kyselytulosten kartoitustyön objekteille ja antaa samalla täyden SQL-ohjauksen.

Miksi Dapper on olemassa

Dapper syntyi Stack Overflow'n tarpeesta saada korkeatasoista dataa. Tiimi havaitsi, että täydet ORM:t, kuten Entity Framework (pre-Core), lisäsivät liikaa ylimenoja suurliikenteen skenaarioihin. Dapper antaa 95 prosenttia mukavuudesta, kun vain 5-15 prosenttia on raakaa ADO.NETiä.

Tärkeimmät ominaisuudet

  • Raaka SQL: Kirjoitat kaikki SQL itse - täydellinen hallinta
  • Korkea suorituskyky: ADO.NET-verkon (5–15 prosenttia) minimiylityspaikka
  • Moninkertainen kartoitus: Karttakompleksiset kyselyt useille toisiinsa liittyville tyypeille
  • Parametrin käsittely: Automaattinen parametrisointi estää SQL-injektion
  • Transaktiotuki: Transaktioiden täysi hallinta
  • Yksinkertaisuus: Ei kokoonpanoa, ei muutosta, ei taikaa
  • Asenc/Await: Täydellinen tuki modernille . NET-verkolle

Dapperin perusesimerkit

using Npgsql;
using Dapper;

public class DapperBlogRepository
{
    private readonly string _connectionString;

    public DapperBlogRepository(string connectionString)
    {
        _connectionString = connectionString;

        // Configure Dapper to work with PostgreSQL naming conventions
        DefaultTypeMap.MatchNamesWithUnderscores = true;
    }

    // Simple query
    public async Task<IEnumerable<BlogPost>> GetRecentPostsAsync(int count)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT id, title, content, tags, published_date, category_id
            FROM blog_posts
            ORDER BY published_date DESC
            LIMIT @Count";

        return await connection.QueryAsync<BlogPost>(sql, new { Count = count });
    }

    // Query with WHERE clause
    public async Task<BlogPost> GetPostByIdAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT id, title, content, published_date
            FROM blog_posts
            WHERE id = @Id";

        return await connection.QueryFirstOrDefaultAsync<BlogPost>(sql, new { Id = id });
    }

    // Insert with returning ID
    public async Task<int> CreatePostAsync(BlogPost post)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            INSERT INTO blog_posts (title, content, published_date, category_id)
            VALUES (@Title, @Content, @PublishedDate, @CategoryId)
            RETURNING id";

        return await connection.ExecuteScalarAsync<int>(sql, post);
    }

    // Update
    public async Task UpdatePostAsync(BlogPost post)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            UPDATE blog_posts
            SET title = @Title,
                content = @Content,
                published_date = @PublishedDate
            WHERE id = @Id";

        await connection.ExecuteAsync(sql, post);
    }

    // Delete
    public async Task DeletePostAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = "DELETE FROM blog_posts WHERE id = @Id";

        await connection.ExecuteAsync(sql, new { Id = id });
    }
}

Moninpeli: Käsittely liittyy

Yksi Dapperin tehokkaimmista ominaisuuksista on monikartoitus, jossa käsitellään tehokkaasti ja kartoitetaan useita toisiinsa liittyviä esineitä:

public async Task<IEnumerable<BlogPost>> GetPostsWithCategoryAsync()
{
    using var connection = new NpgsqlConnection(_connectionString);

    const string sql = @"
        SELECT
            p.id, p.title, p.content, p.published_date,
            c.id, c.name, c.description
        FROM blog_posts p
        INNER JOIN categories c ON p.category_id = c.id
        ORDER BY p.published_date DESC";

    return await connection.QueryAsync<BlogPost, Category, BlogPost>(
        sql,
        (post, category) =>
        {
            post.Category = category;
            return post;
        },
        splitOn: "id"  // Split at the second "id" column
    );
}

// More complex: Posts with comments
public async Task<IEnumerable<BlogPost>> GetPostsWithCommentsAsync()
{
    using var connection = new NpgsqlConnection(_connectionString);

    const string sql = @"
        SELECT
            p.id, p.title, p.content,
            c.id, c.author, c.content, c.created_at
        FROM blog_posts p
        LEFT JOIN comments c ON p.id = c.blog_post_id
        ORDER BY p.published_date DESC, c.created_at";

    var postDict = new Dictionary<int, BlogPost>();

    await connection.QueryAsync<BlogPost, Comment, BlogPost>(
        sql,
        (post, comment) =>
        {
            if (!postDict.TryGetValue(post.Id, out var existingPost))
            {
                existingPost = post;
                existingPost.Comments = new List<Comment>();
                postDict.Add(post.Id, existingPost);
            }

            if (comment != null)
            {
                existingPost.Comments.Add(comment);
            }

            return existingPost;
        },
        splitOn: "id"
    );

    return postDict.Values;
}

Advanced Dapper -tekniikat

Dynaamiset muuttujat monimutkaisille kyselyille:

public async Task<IEnumerable<BlogPost>> SearchWithDynamicFiltersAsync(SearchCriteria criteria)
{
    using var connection = new NpgsqlConnection(_connectionString);

    var parameters = new DynamicParameters();
    var conditions = new List<string>();

    var sql = new StringBuilder("SELECT * FROM blog_posts");

    if (!string.IsNullOrEmpty(criteria.SearchTerm))
    {
        conditions.Add("search_vector @@ to_tsquery('english', @SearchTerm)");
        parameters.Add("SearchTerm", criteria.SearchTerm);
    }

    if (criteria.CategoryIds?.Any() == true)
    {
        conditions.Add("category_id = ANY(@CategoryIds)");
        parameters.Add("CategoryIds", criteria.CategoryIds);
    }

    if (criteria.FromDate.HasValue)
    {
        conditions.Add("published_date >= @FromDate");
        parameters.Add("FromDate", criteria.FromDate.Value);
    }

    if (criteria.Tags?.Any() == true)
    {
        conditions.Add("tags && @Tags");  // PostgreSQL array overlap
        parameters.Add("Tags", criteria.Tags);
    }

    if (conditions.Any())
    {
        sql.Append(" WHERE ");
        sql.Append(string.Join(" AND ", conditions));
    }

    sql.Append(" ORDER BY published_date DESC LIMIT @Limit");
    parameters.Add("Limit", criteria.Limit);

    return await connection.QueryAsync<BlogPost>(sql.ToString(), parameters);
}

PostgreSQL-tyypeille mukautetut tyyppikäsittelimet:

// Handle PostgreSQL arrays
public class PostgresArrayTypeHandler : SqlMapper.TypeHandler<string[]>
{
    public override void SetValue(IDbDataParameter parameter, string[] value)
    {
        parameter.Value = value;
        ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Array | NpgsqlDbType.Text;
    }

    public override string[] Parse(object value)
    {
        return (string[])value;
    }
}

// Handle PostgreSQL JSONB
public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T>
{
    public override void SetValue(IDbDataParameter parameter, T value)
    {
        parameter.Value = JsonSerializer.Serialize(value);
        ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Jsonb;
    }

    public override T Parse(object value)
    {
        return JsonSerializer.Deserialize<T>(value.ToString());
    }
}

// Register handlers (in startup)
SqlMapper.AddTypeHandler(new PostgresArrayTypeHandler());
SqlMapper.AddTypeHandler(new JsonTypeHandler<Dictionary<string, object>>());

Bulk Operations with PostgreSQL COPY:

public async Task BulkInsertPostsAsync(IEnumerable<BlogPost> posts)
{
    using var connection = new NpgsqlConnection(_connectionString);
    await connection.OpenAsync();

    using var writer = await connection.BeginBinaryImportAsync(
        "COPY blog_posts (title, content, tags, published_date) FROM STDIN (FORMAT BINARY)"
    );

    foreach (var post in posts)
    {
        await writer.StartRowAsync();
        await writer.WriteAsync(post.Title);
        await writer.WriteAsync(post.Content);
        await writer.WriteAsync(post.Tags, NpgsqlDbType.Array | NpgsqlDbType.Text);
        await writer.WriteAsync(post.PublishedDate);
    }

    await writer.CompleteAsync();
}

Transaktiotuki:

public async Task TransferPostToCategoryAsync(int postId, int newCategoryId)
{
    using var connection = new NpgsqlConnection(_connectionString);
    await connection.OpenAsync();

    using var transaction = await connection.BeginTransactionAsync();

    try
    {
        // Update the post
        await connection.ExecuteAsync(
            "UPDATE blog_posts SET category_id = @CategoryId WHERE id = @PostId",
            new { CategoryId = newCategoryId, PostId = postId },
            transaction
        );

        // Log the change
        await connection.ExecuteAsync(
            @"INSERT INTO category_history (post_id, category_id, changed_at)
              VALUES (@PostId, @CategoryId, @ChangedAt)",
            new { PostId = postId, CategoryId = newCategoryId, ChangedAt = DateTime.UtcNow },
            transaction
        );

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Milloin Dapperia käytetään

Käytä Dapperia, kun:

  • Suorituskyky on kriittinen, mutta et tarvitse ehdotonta maksiminopeutta
  • Sinun on mukava kirjoittaa SQL:ää.
  • Sinulla on monimutkaisia kyselyitä, jotka eivät riitä linqiin
  • Tarvitset hienon hallinnan SQL-sukupolveen
  • Työskentelet olemassa olevan tietokannan skeeman kanssa
  • Haluat minimaalisia kuluja ja riippuvuuksia
  • Muistinkäyttö on ongelma
  • Sinun täytyy hyödyntää tietokantakohtaisia ominaisuuksia laajasti

Vältä Dopperia, kun:

  • Joukkueesi ei pidä SQL:stä
  • Tarvitset automaattisen vaihtoseurannan
  • Haluat koodiksi skema-siirtymät
  • Sinulla on nopeasti kehittyvä skeema
  • Tarvitset tietokannan siirrettävyyden
  • LINQ on parempi kuin SQL-syntaksi

Suorituskykyä koskevat ominaisuudet

  • Kyselytulos: 5-15 % ylimenoa raaka-aineen ADO.NET-verkosta
  • Muistinkäyttö: Minimaalinen - ei muutosta seuranta tai valtakirjat
  • Bulk-operaatiot: Erinomainen PostgreSQL COPY:n kanssa
  • Ensimmäinen kyselyNopea - ei koostevaihetta
  • Kehittäjän tuottavuus: Vaatii SQL-osaamista, mutta hyvin ennustettavissa

Raw ADO.NET ja Npgsql

Absoluuttiseen maksimaaliseen suorituskykyyn ja ohjaukseen voit käyttää Npgsql suoraan ilman ORM-kerrosta.

Kun Raw ADO.NET tekee järkeä

Raw ADO.NET on tarkoituksenmukaista, kun

  • Tarvitset joka millisekunnin suorituskykyä
  • Rakennat huipputason dataprosessoria
  • Työskentelet PostgreSQL-ominaisuuksien kanssa, joita ORM ei tue
  • Tarvitset tarkkaa kontrollia muistin varauksiin
  • Suoritat irtotoimintoja tai streamaat suuria tietoaineistoja

Esimerkki: Puhdas Npgsql

public class NpgsqlBlogRepository
{
    private readonly string _connectionString;

    public async Task<List<BlogPost>> GetRecentPostsAsync(int count)
    {
        var posts = new List<BlogPost>();

        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content, tags, published_date FROM blog_posts ORDER BY published_date DESC LIMIT @count",
            connection
        );

        command.Parameters.AddWithValue("count", count);

        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            posts.Add(new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2),
                Tags = reader.GetFieldValue<string[]>(3),
                PublishedDate = reader.GetDateTime(4)
            });
        }

        return posts;
    }

    // Using prepared statements for repeated queries
    public async Task<BlogPost> GetPostByIdAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content FROM blog_posts WHERE id = $1",
            connection
        );

        command.Parameters.AddWithValue(id);
        await command.PrepareAsync(); // Prepared statement for performance

        using var reader = await command.ExecuteReaderAsync();

        if (await reader.ReadAsync())
        {
            return new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2)
            };
        }

        return null;
    }

    // Working with PostgreSQL JSONB
    public async Task<Dictionary<string, object>> GetPostMetadataAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT metadata FROM blog_posts WHERE id = $1",
            connection
        );

        command.Parameters.AddWithValue(id);

        var json = await command.ExecuteScalarAsync() as string;
        return JsonSerializer.Deserialize<Dictionary<string, object>>(json);
    }

    // Streaming large result sets
    public async IAsyncEnumerable<BlogPost> StreamAllPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content FROM blog_posts ORDER BY id",
            connection
        );

        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            yield return new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2)
            };
        }
    }
}

Suorituskykyä koskevat ominaisuudet

  • Kyselytulos: Lähtötilanne - nopein mahdollinen
  • Muistinkäyttö: Alin - täydellinen kontrolli määrärahoista
  • Bulk-operaatiot: Erinomainen COPY-protokollan kanssa
  • Kehittäjän tuottavuus: Vaatii eniten manuaalista työtä

Kohteen kartoitus kirjastoissa

Kun työskentelet Dapperin tai raa'an ADO.NETin kanssa, sinun täytyy usein kartoittaa eri objektiesitelmien (DTO:t, kokonaisuudet, katso mallit) välillä. Useat kirjastot voivat automatisoida tämän.

Kartanantaja: Korkean suorituskyvyn kartoitus

Kartta on nopea, konventointiin perustuva esinekartturi, joka käyttää lähdesukupolvea optimaaliseen suorituskykyyn.

// Install: Mapster and Mapster.Tool
using Mapster;

public class BlogPostDto
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Summary { get; set; }
    public List<string> CategoryNames { get; set; }
}

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public List<Category> Categories { get; set; }
}

// Configuration
public class MappingConfig : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<BlogPost, BlogPostDto>()
            .Map(dest => dest.Summary, src => src.Content.Substring(0, Math.Min(200, src.Content.Length)))
            .Map(dest => dest.CategoryNames, src => src.Categories.Select(c => c.Name).ToList());

        // Reverse map with ignore
        config.NewConfig<BlogPostDto, BlogPost>()
            .Ignore(dest => dest.Content);
    }
}

// Registration in Program.cs
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());

// Usage with Dapper
public class BlogService
{
    private readonly string _connectionString;

    public async Task<List<BlogPostDto>> GetPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        var posts = await connection.QueryAsync<BlogPost>(@"
            SELECT p.id, p.title, p.content
            FROM blog_posts p
        ");

        // Map to DTOs - very fast with Mapster
        return posts.Adapt<List<BlogPostDto>>();
    }

    // Projection mapping (compile-time)
    public async Task<List<BlogPostDto>> GetPostsDtosDirectlyAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        // Query directly to DTO shape
        return (await connection.QueryAsync<BlogPostDto>(@"
            SELECT
                id,
                title,
                SUBSTRING(content, 1, 200) as summary
            FROM blog_posts
        ")).ToList();
    }
}

AutoMapper: Kypsä konventtipohjainen kartasto

AutoMapper on suosituin karttakirjasto, tosin hitaampi kuin Mapster.

// Install: AutoMapper and AutoMapper.Extensions.Microsoft.DependencyInjection
using AutoMapper;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<BlogPost, BlogPostDto>()
            .ForMember(d => d.Summary, opt => opt.MapFrom(s =>
                s.Content.Length > 200 ? s.Content.Substring(0, 200) : s.Content))
            .ForMember(d => d.CategoryNames, opt => opt.MapFrom(s =>
                s.Categories.Select(c => c.Name)));

        // Reverse map
        CreateMap<BlogPostDto, BlogPost>()
            .ForMember(d => d.Content, opt => opt.Ignore());
    }
}

// Registration in Program.cs
services.AddAutoMapper(typeof(MappingProfile));

// Usage
public class BlogService
{
    private readonly IMapper _mapper;
    private readonly string _connectionString;

    public BlogService(IMapper mapper, IConfiguration configuration)
    {
        _mapper = mapper;
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }

    public async Task<List<BlogPostDto>> GetPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        var posts = await connection.QueryAsync<BlogPost>(@"
            SELECT id, title, content FROM blog_posts
        ");

        return _mapper.Map<List<BlogPostDto>>(posts.ToList());
    }
}

Manuaalinen kartoitus: täysi hallinta

Joskus paras lähestymistapa on selkeä manuaalinen kartoitus:

public static class BlogPostMapper
{
    public static BlogPostDto ToDto(this BlogPost post)
    {
        return new BlogPostDto
        {
            Id = post.Id,
            Title = post.Title,
            Summary = post.Content.Length > 200
                ? post.Content.Substring(0, 200) + "..."
                : post.Content,
            CategoryNames = post.Categories?.Select(c => c.Name).ToList() ?? new List<string>()
        };
    }

    public static List<BlogPostDto> ToDtoList(this IEnumerable<BlogPost> posts)
    {
        return posts.Select(p => p.ToDto()).ToList();
    }

    // Inline mapping for simple cases
    public static BlogPostDto MapToDto(BlogPost post) => new()
    {
        Id = post.Id,
        Title = post.Title,
        Summary = post.Content[..Math.Min(200, post.Content.Length)]
    };
}

// Usage
var posts = await _repository.GetAllPostsAsync();
var dtos = posts.ToDtoList();

Mapping Performance Verrattuna

BenchmarkDotNet Results (mapping 1000 objects):

Method              | Mean      | Allocated
--------------------|-----------|----------
Manual Mapping      | 45.2 μs   | 78 KB
Mapster             | 52.1 μs   | 79 KB
AutoMapper          | 184.3 μs  | 156 KB

Tärkeimmät takeawayt:

  • Manuaalinen kartoitus on nopeinta, mutta vaatii lisää koodia
  • Mapster on lähes yhtä nopea vähemmällä koodilla
  • AutoMapper on kätevä, mutta sen yleiskustannukset ovat korkeammat

Hybridilähestymistavat: molempien maailmojen parhaat

Todellisissa sovelluksissa haluat usein käyttää erilaisia lähestymistapoja eri skenaarioihin saman sovelluksen sisällä. suositeltu lähestymistapa suurimmalle osalle tuotantojärjestelmistä.

CQRS-kuvio: Erottele lukemista ja kirjoittamista

CQRS-malli (Command Query Responsibility Segregation) on luonteva tapa käyttää hybrididataa. MartenKatso artikkelini aiheesta Nykyaikainen CQRS ja tapahtumakilpailu.

Miten Marten viittaa tähän keskusteluun:

Marten on PostgreSQL:lle rakennettu dokumenttitietokanta ja tapahtumamyymälä, joka vie hybrididatan toiselle tasolle. Siinä yhdistyvät:

  • Tapahtumahankinta kirjoituksista (muuttumattomat tapahtumavirrat)
  • Ennusteet Luetaan (kyselyihin optimoidut materiaaliset näkymät)
  • PostgreSQL:n JSONB asiakirjasäilytykseen
  • Sisäänrakennetut CQRS-mallit

Vaikka tässä artikkelissa keskitytään perinteiseen suhdetietoon (EF Core, Dapper), Marten näyttää, miten PostgreSQL:n kehittyneitä ominaisuuksia (JSONB, tapahtumavirrat) voidaan hyödyntää hienostuneen arkkitehtuurin toteuttamiseen. Periaatteet ovat samat:

  • Erilliset kirjoitusmallit (optimoitu liiketoimia ja johdonmukaisuutta varten)
  • Erilliset lukumallit (optimoitu kyselyihin ja suorituksiin)
  • Käytä oikeaa työkalua jokaiseen tehtävään
graph TB
    Client[Client Application]

    subgraph "Write Side - Commands"
        WriteAPI[Write API / Commands]
        EFCore[EF Core Context]
        WriteDB[(PostgreSQL<br/>Write Operations)]
    end

    subgraph "Read Side - Queries"
        ReadAPI[Read API / Queries]
        Dapper[Dapper Repository]
        ReadDB[(PostgreSQL<br/>Read Operations)]
    end

    Client -->|Create/Update/Delete| WriteAPI
    WriteAPI --> EFCore
    EFCore -->|Change Tracking<br/>Validation<br/>Business Logic| WriteDB

    Client -->|Query/Search| ReadAPI
    ReadAPI --> Dapper
    Dapper -->|Optimized SQL<br/>DTOs<br/>No Tracking| ReadDB

    WriteDB -.->|Same Database| ReadDB

    style Client stroke:#6366f1,stroke-width:2px
    style WriteAPI stroke:#2563eb,stroke-width:2px
    style EFCore stroke:#2563eb,stroke-width:2px
    style WriteDB stroke:#2563eb,stroke-width:2px
    style ReadAPI stroke:#059669,stroke-width:2px
    style Dapper stroke:#059669,stroke-width:2px
    style ReadDB stroke:#059669,stroke-width:2px

Tällä mallilla on vipuvaikutusta:

  • EF-ydin kirjoittaa: Muutosten seuranta, validointi, liiketoiminnan säännöt
  • Dopper seuraavasti: Maximum query performance and flexibility
  • Sama tietokanta, eri käyttötavat optimoitu jokaiselle käyttökerralle

Toteutus: CQRS ja EF Core ja Dapper

// Commands: Use EF Core for change tracking and validation
public class BlogCommandService
{
    private readonly BlogDbContext _context;
    private readonly ILogger<BlogCommandService> _logger;

    public BlogCommandService(BlogDbContext context, ILogger<BlogCommandService> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<int> CreatePostAsync(CreatePostCommand command)
    {
        // Business logic and validation
        var post = new BlogPost
        {
            Title = command.Title,
            Content = command.Content,
            CategoryId = command.CategoryId,
            PublishedDate = DateTime.UtcNow
        };

        _context.BlogPosts.Add(post);
        await _context.SaveChangesAsync();

        _logger.LogInformation("Created blog post {PostId}", post.Id);

        return post.Id;
    }

    public async Task UpdatePostAsync(UpdatePostCommand command)
    {
        var post = await _context.BlogPosts.FindAsync(command.Id);

        if (post == null)
            throw new InvalidOperationException($"Post {command.Id} not found");

        post.Title = command.Title;
        post.Content = command.Content;
        post.UpdatedAt = DateTime.UtcNow;

        await _context.SaveChangesAsync();

        _logger.LogInformation("Updated blog post {PostId}", post.Id);
    }

    public async Task DeletePostAsync(int id)
    {
        var post = await _context.BlogPosts.FindAsync(id);

        if (post != null)
        {
            _context.BlogPosts.Remove(post);
            await _context.SaveChangesAsync();

            _logger.LogInformation("Deleted blog post {PostId}", id);
        }
    }
}

// Queries: Use Dapper for read performance
public class BlogQueryService
{
    private readonly string _connectionString;
    private readonly ILogger<BlogQueryService> _logger;

    public BlogQueryService(IConfiguration configuration, ILogger<BlogQueryService> logger)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
        _logger = logger;
    }

    public async Task<BlogPostDto> GetPostBySlugAsync(string slug)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                p.content,
                p.published_date,
                c.id as category_id,
                c.name as category_name,
                (SELECT COUNT(*) FROM comments WHERE blog_post_id = p.id) as comment_count
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.slug = @Slug";

        var post = await connection.QueryFirstOrDefaultAsync<BlogPostDto>(sql, new { Slug = slug });

        if (post != null)
        {
            _logger.LogInformation("Retrieved blog post by slug {Slug}", slug);
        }

        return post;
    }

    public async Task<PagedResult<BlogPostSummaryDto>> GetRecentPostsAsync(int page, int pageSize)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                LEFT(p.content, 200) as summary,
                p.published_date,
                c.name as category_name
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id
            ORDER BY p.published_date DESC
            LIMIT @PageSize OFFSET @Offset";

        const string countSql = "SELECT COUNT(*) FROM blog_posts";

        var posts = await connection.QueryAsync<BlogPostSummaryDto>(
            sql,
            new { PageSize = pageSize, Offset = (page - 1) * pageSize }
        );

        var totalCount = await connection.ExecuteScalarAsync<int>(countSql);

        return new PagedResult<BlogPostSummaryDto>
        {
            Items = posts.ToList(),
            TotalCount = totalCount,
            Page = page,
            PageSize = pageSize
        };
    }

    public async Task<List<BlogPostDto>> SearchPostsAsync(string searchTerm)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                p.content,
                p.published_date,
                c.name as category_name,
                ts_rank(p.search_vector, query) as relevance_score
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id,
                 to_tsquery('english', @SearchTerm) query
            WHERE p.search_vector @@ query
            ORDER BY relevance_score DESC
            LIMIT 50";

        var posts = await connection.QueryAsync<BlogPostDto>(sql, new { SearchTerm = searchTerm });

        _logger.LogInformation(
            "Searched posts with term {SearchTerm}, found {Count} results",
            searchTerm,
            posts.Count()
        );

        return posts.ToList();
    }
}

// Service layer orchestrating commands and queries
public class BlogService
{
    private readonly BlogCommandService _commands;
    private readonly BlogQueryService _queries;

    public BlogService(BlogCommandService commands, BlogQueryService queries)
    {
        _commands = commands;
        _queries = queries;
    }

    // Write operations delegate to command service
    public Task<int> CreatePostAsync(CreatePostCommand command) => _commands.CreatePostAsync(command);
    public Task UpdatePostAsync(UpdatePostCommand command) => _commands.UpdatePostAsync(command);
    public Task DeletePostAsync(int id) => _commands.DeletePostAsync(id);

    // Read operations delegate to query service
    public Task<BlogPostDto> GetPostBySlugAsync(string slug) => _queries.GetPostBySlugAsync(slug);
    public Task<PagedResult<BlogPostSummaryDto>> GetRecentPostsAsync(int page, int pageSize)
        => _queries.GetRecentPostsAsync(page, pageSize);
    public Task<List<BlogPostDto>> SearchPostsAsync(string searchTerm)
        => _queries.SearchPostsAsync(searchTerm);
}

EF Core ja satunnainen Raw SQL

Sovellukset, jotka ovat pääasiassa EF Corea mutta jotka tarvitsevat satunnaista suorituskyvyn optimointia:

public class BlogService
{
    private readonly BlogDbContext _context;

    // 95% of queries: Use EF Core LINQ
    public async Task<List<BlogPost>> GetPostsByCategoryAsync(int categoryId)
    {
        return await _context.BlogPosts
            .Where(p => p.CategoryId == categoryId)
            .Include(p => p.Comments)
            .ToListAsync();
    }

    // 5% of queries: Use raw SQL for complex analytics
    public async Task<List<PostAnalytics>> GetPostAnalyticsAsync()
    {
        using var connection = _context.Database.GetDbConnection();
        await _context.Database.OpenConnectionAsync();

        using var command = connection.CreateCommand();
        command.CommandText = @"
            WITH post_metrics AS (
                SELECT
                    p.id,
                    p.title,
                    COUNT(DISTINCT c.id) as comment_count,
                    COUNT(DISTINCT v.id) as view_count,
                    AVG(c.sentiment_score) as avg_sentiment
                FROM blog_posts p
                LEFT JOIN comments c ON p.id = c.post_id
                LEFT JOIN post_views v ON p.id = v.post_id
                WHERE p.published_date >= NOW() - INTERVAL '30 days'
                GROUP BY p.id, p.title
            )
            SELECT * FROM post_metrics
            ORDER BY view_count DESC";

        var analytics = new List<PostAnalytics>();
        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            analytics.Add(new PostAnalytics
            {
                PostId = reader.GetInt32(0),
                Title = reader.GetString(1),
                CommentCount = reader.GetInt64(2),
                ViewCount = reader.GetInt64(3),
                AverageSentiment = reader.IsDBNull(4) ? 0 : reader.GetDouble(4)
            });
        }

        return analytics;
    }
}

Tulosvertailu

Tarkastellaanpa tosimaailman suoritusten vertailukohtia Postin ja SQL:n yhteisille toiminnoille:

Vertailukohta: Reading 1000 Records

BenchmarkDotNet Results (Lower is Better):

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (No Tracking)    | 12.34 ms   | 2.4 MB
EF Core (With Tracking)  | 15.67 ms   | 4.8 MB
Dapper                   | 8.21 ms    | 1.8 MB
Raw Npgsql               | 7.45 ms    | 1.2 MB

Vertailukohta: 1000 levyn lisääminen

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (SaveChanges)    | 245.3 ms   | 15.2 MB
EF Core (BulkInsert)     | 42.1 ms    | 8.4 MB
Dapper (Loop)            | 189.7 ms   | 2.1 MB
Npgsql COPY              | 18.3 ms    | 0.8 MB

Vertailukohta: Monimutkainen liittymä kyselyyn

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (Include)        | 28.5 ms    | 5.2 MB
EF Core (Split Query)    | 24.1 ms    | 4.8 MB
Dapper (Multi-Map)       | 16.8 ms    | 3.1 MB
Raw Npgsql               | 15.2 ms    | 2.4 MB

Tärkeimmät takeawayt

  1. Raw Npgsql on nopein mutta vaatii eniten koodia
  2. Dapper tarjoaa erinomaisen suorituksen minimaalinen abstraktio maksaa (60–70 prosenttia nopeammin kuin EF Core)
  3. EF Core -kyselyjä, joita ei seurata ovat järkeviä useimmille skenaarioille
  4. Bulk-toiminnot näytä suurimmat suorituserot (10-13x ero!)
  5. Muistinjako Noudata samanlaista kaavaa kuin teloitusaika

Päätös Matrix: Mikä tapa käyttää

Käytä EF-ydintä, kun:

  • Uuden sovelluksen rakentaminen muuttuvin vaatimuksin
  • â € € # Domain-vetoinen muotoilu rikkaiden kokonaisuuksien malleilla
  • Tarvitsit muuttoliikkeitä ja skeemanhallintaa
  • Joukkue viihtyy paremmin C#:n kuin SQL:n kanssa
  • Luku- ja kirjoitussuhde on tasapainossa
  • â € € € Query suorituskyky alle 20-50 % optimaalista on hyväksyttävää

Katso Osa 1 kattava EF Core -ohjeistus.

Käytä Dapperia, kun:

  • Suorituskyky on tärkeää, mutta ei kriittistä
  • Sinulla on monimutkaisia kyselyitä, jotka eivät riitä linqiin
  • SQL:n kirjoittaminen on mukavaa
  • Tarvitset hienon hallinnan SQL-sukupolveen
  • â € â € € â Read-raskas työtaakka yksinkertaisilla kirjoituksilla
  • Haluatko minimaalisen abstraktion?

Käytä raakaa Npgsqliä, kun:

  • â € € € Maksimaalinen suorituskyky on kriittinen
  • Työskentely laaja-alaisesti PostgreSQL-ominaisuuksien kanssa
  • Joka millisekunnilla ja megatavulla on merkitystä
  • Tarvitset ehdotonta kontrollia

Hybridilähestymistapa, kun:

  • CQRS-kuvio (EF Core kirjoittaa, Dapper lukee)
  • Useimmissa kyselyissä käytetään EF Corea, mutta muutama tarvitsee raakaa SQL:tä
  • "Haluat asteittaisen siirtymän lähestymistapojen välillä"
  • â € # Suuret, monimutkaiset sovellukset, joilla on erilaisia vaatimuksia

Parhaiden käytäntöjen yhteenveto

Yleiset suuntaviivat

  1. Aloita EF-ytimellä uusiin hankkeisiin, ellei sinulla ole erityisiä suoritusvaatimuksia
  2. Profiili ennen optimointia - Älä oleta, että tarvitset SQL:ää.
  3. Käytä hybridimalleja - yhdistä eri työkalujen vahvuudet
  4. Pidä datan käyttölogiikka erillään - Arkistomalli auttaa vaihtamaan toteutuksia
  5. Käytä yhteyden yhdistämistä - määritä työmääräsi oikein
  6. Leverage PostgreSQL -ominaisuudet - älä abstraktoi pois tehokkaita tietokantoja

Dopper Specific

  1. Käytä parametreja SQL-injektion estäminen
  2. Harkitse kyselyn tulosten välilyöntejä kalliille, toistuville kyselyille
  3. Käytä monikartoitusta liittymille useamman kierroksen sijaan
  4. Uudelleenkäyttöyhteydet yhteysaltaasta
  5. Mietihän, Dapper.Contrib yksinkertaiset CRUD-operaatiot

Suoritusvinkkejä

  1. Hakeudu kyselyihisi - analysoi hitaita kyselyitä ANALYZEn avulla
  2. Käytä valmisteltuja lausuntoja toistuviin kyselyihin
  3. Erätapahtumat mahdollisuuksien mukaan
  4. Käytä COPYa irtotavarana PostgreSQL:ssä
  5. Seuraa yhteyspoolia - Määritä min/max allaskoko
  6. Harkitse lukujäljennöksiä Reagoivalle työtaakalle

Päätelmät

.NET-sovelluksen oikean tietoyhteyden valinnassa PostgreSQL:n kanssa ei ole kyse "paras" työkalun löytämisestä, vaan oikean työkalun sovittamisesta omiin erityistarpeisiin:

  • EF-ydin (ks. Osa 1) Erinomaisesti nopea kehitys, domain-mallinnus ja sovellukset, joissa kehittäjän tuottavuus voittaa raa'an suorituskyvyn
  • Dopper tarjoaa erinomaisen keskikentän lähes optimaalisella suorituskyvyllä ja kohtuullisella abstraktiolla
  • Raw Npgsql tarjoaa maksimaalisen suorituskyvyn ja valvonnan dataintensiivisille toiminnoille
  • Kartoitetaan kirjastoja kuten Mapster ja AutoMapper vähentävät kattilalevyä työskennellessään alemman tason datan kanssa

Käytännössä menestyneimmissä sovelluksissa käytetään usein hybridilähestymistapaKunkin työkalun vahvuuksien hyödyntäminen tarvittaessa:

  • Käyttö EF-ydin Oman verkkotunnuksen logiikalle ja kirjoittaa
  • Käyttö Dopper Monimutkaisiin lukukyselyihin ja raportointiin
  • Käyttö raaka Npgsql irtolastitoimintaan ja analytiikkaan

Avain on:

  1. Ymmärrä vaatimuksesi - suorituskyky, kehitysnopeus, tiimitaidot
  2. Profiloitu sovellus - tunnistaa todelliset pullonkaulat, ei oletetut
  3. Valitse pragmaattisesti - käytä yksinkertaisinta työkalua, joka vastaa tarpeitasi
  4. Pysy joustavana - saman sovelluksen sisällä voi sekoittaa lähestymistapoja

Muista, että ennenaikainen optimointi on kaiken pahan alkulähde, mutta niin on myös sellaisen järjestelmän rakentaminen, joka ei voi tarvittaessa skaalata. Aloita yksinkertainen, mittaa suorituskykyä ja optimoi siellä, missä sillä on merkitystä.

Referenssit ja jatkolukeminen

Tämän sarjan 1 osa:

Virallinen asiakirja:

Aiheeseen liittyvät artikkelit tästä blogista:


Tähän päättyy kaksiosainen tietoverkkosarjamme .NETissä! Olemme käsitelleet kaikkea EF Coren tehokkaista abstraktioista raakaan SQL:n maksimaaliseen suorituskykyyn ja käytännön ohjeistusta lähestymistapojen yhdistämisestä optimaalisiin tuloksiin.

logo

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