# Πρόσβαση δεδομένων σε .NET: Συγκρίνοντας ORMs και Mapping Στρατηγικές (Μέρος 2 - Ντάπερ, Ακατέργαστο SQL, και Υβριδικές Προσεγγίσεις)

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

Καλώς ήρθατε στο Μέρος 2 του Περιεκτικού Οδηγού μας για την πρόσβαση των δεδομένων στο .NET! [Μέρος 1](/blog/orm-mapping-comparison-part1), εξερευνήσαμε τον πυρήνα πλαισίου οντοτήτων σε βάθος, συμπεριλαμβανομένης της γενιάς SQL, των κοινών παγίδων, και εκείνης της κρίσιμης προειδοποίησης για τους πληρεξίες και την αποθήκευση.

Σε αυτό το άρθρο, θα εξερευνήσουμε τις ελαφρύτερες-βάρους εναλλακτικές λύσεις και πώς να συνδυάσετε πολλαπλές προσεγγίσεις για τη βέλτιστη απόδοση:

- **[ΝτάππερCity name (optional, probably does not need a translation)](https://github.com/DapperLib/Dapper)**: Το γλυκό σημείο micro-ORM
- **Ακατέργαστο ADO.NET/[Npgsql](https://www.npgsql.org/)**: Μέγιστη απόδοση και έλεγχος
- **Αντικείμενο Βιβλιοθήκες Χαρτογράφησης**: [MapsterCity name (optional, probably does not need a translation)](https://github.com/MapsterMapper/Mapster) vs [AutoMapperName](https://automapper.org/)
- **Υβριδικές προσεγγίσεις**: Συνδυάζοντας τον πυρήνα EF και τη σκούπα (CQRS μοτίβο)
- **Βαθμολογήσεις επιδόσεων**: Συγκρίσεις πραγματικού κόσμου
- **Απόφαση Matrix**: Επιλέγοντας το σωστό εργαλείο για το σενάριο σας

## Πίνακας Περιεχομένων

## Dapper: Το Micro-ORM

[ΝτάππερCity name (optional, probably does not need a translation)](https://github.com/DapperLib/Dapper) είναι ένα ελαφρύ, υψηλής απόδοσης μικρο-ORM που δημιουργήθηκε από Stack Overflow. Παρέχει ένα λεπτό στρώμα πάνω από ADO.NET, το χειρισμό της κουραστικής εργασίας της χαρτογράφησης των αποτελεσμάτων ερώτημα σε αντικείμενα, ενώ σας δίνει πλήρη έλεγχο SQL.

### Γιατί Υπάρχει Ντάπερ

Το Dapper γεννήθηκε από την ανάγκη Stack Overflow για πρόσβαση δεδομένων υψηλής απόδοσης. Η ομάδα διαπίστωσε ότι το πλήρες ORMs όπως το πλαίσιο οντότητας (pre-Core) πρόσθεσε πάρα πολλά από πάνω για τα σενάρια υψηλής κυκλοφορίας τους. Το Dapper σας δίνει το 95% της ευκολίας με μόνο 5-15% πάνω από το ακατέργαστο ADO.NET.

### Βασικά χαρακτηριστικά

- **Ακατέργαστο SQL**: Μπορείτε να γράψετε όλα SQL τον εαυτό σας - πλήρη έλεγχο
- **Υψηλή απόδοση**: Minimal over ADO.NET (5-15%)
- **Πολλαπλή χαρτογράφηση@ action: inmenu**: Map complex questions to multiple related types
- **Χειρισμός παραμέτρου**: Αυτόματη παραμετροποίηση αποτρέπει την ένεση SQL
- **Υποστήριξη συναλλαγών**: Πλήρης έλεγχος των συναλλαγών
- **Ακρίβεια**: Καμία διαμόρφωση, καμία αλλαγή εντοπισμού, καμία μαγεία
- **Async/Await**: Πλήρης υποστήριξη async για το σύγχρονο .NET

### Βασικά παραδείγματα σκουπιδιών

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

### Multi-Mapping: Η διαχείριση ενώνει

Ένα από τα πιο ισχυρά χαρακτηριστικά του Dapper είναι η multi-mapping - αποτελεσματική διαχείριση ενώνει και χαρτογραφήσει σε πολλά συναφή αντικείμενα:

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

### Προηγμένες τεχνικές κουπαστής

**Δυναμικές παράμετροι για σύνθετες ερωτήσεις:**

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

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

**Μαζικές λειτουργίες με PostgreSQL COPY:**

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

**Υποστήριξη συναλλαγών:**

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

### Πότε να χρησιμοποιήσετε το Dapper

**Χρησιμοποίησε το Dapper όταν:**

- Η απόδοση είναι κρίσιμη, αλλά δεν χρειάζεστε απόλυτη μέγιστη ταχύτητα.
- Είσαι άνετα γράφοντας SQL
- Έχετε περίπλοκες ερωτήσεις που δεν χαρτογραφούν καλά σε LINQ
- Χρειάζεσαι καλό έλεγχο στη γενιά SQL
- Δουλεύεις με ένα υπάρχον σχέδιο βάσης δεδομένων.
- Θέλετε ελάχιστα από πάνω και εξαρτήσεις
- Η χρήση μνήμης είναι μια ανησυχία
- Πρέπει να αξιοποιήσετε εκτεταμένα τα ειδικά χαρακτηριστικά της βάσης δεδομένων

**Να αποφεύγετε το σκουπιδοφάγο όταν:**

- Η ομάδα σας δεν είναι άνετη με SQL
- Χρειάζεσαι αυτόματη παρακολούθηση αλλαγών.
- Θέλετε μετανάστες σχημάτων ως κωδικό
- Έχεις ένα ταχέως εξελισσόμενο σχήμα.
- Χρειάζεσαι τη φορητότητα της πολλαπλής βάσης δεδομένων.
- Προτιμάς LINQ αντί SQL σύνταξη

### Χαρακτηριστικά επιδόσεων

- **Επιδόσεις ερωτήσεων**: 5-15% πάνω από το ακατέργαστο ADO.NET
- **Χρήση μνήμης**: Minimal - no change tracking or proxies
- **Δραστηριότητες μαζικής μεταφοράς**: Εξαιρετικό με PostgreSQL COPY
- **Πρώτη ερώτηση**: Fast - no collection step
- **Παραγωγικότητα προγραμματιστή**: Απαιτεί γνώση SQL αλλά πολύ προβλέψιμη

## Ακατέργαστο ADO.NET με Npgsql

Για απόλυτη μέγιστη απόδοση και έλεγχο, μπορείτε να χρησιμοποιήσετε [Npgsql](https://www.npgsql.org/) απευθείας χωρίς στρώμα ORM.

### Όταν ωμό ADO.NET κάνει αίσθηση

Το ωμό ADO.NET είναι κατάλληλο όταν:

- Χρειάζεσαι κάθε χιλιοστό του δευτερολέπτου της παράστασης.
- Φτιάχνεις επεξεργαστές δεδομένων υψηλής απόδοσης
- Συνεργάζεστε με χαρακτηριστικά του PostgreSQL που δεν υποστηρίζονται από ORMs
- Χρειάζεσαι ακριβή έλεγχο των κατανομών μνήμης.
- Κάνεις μαζικές λειτουργίες ή ρέεις μεγάλα σύνολα δεδομένων

### Παράδειγμα: Καθαρό Npgsql

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

### Χαρακτηριστικά επιδόσεων

- **Επιδόσεις ερωτήσεων**: Αρχή - γρηγορότερο δυνατό
- **Χρήση μνήμης**: Χαμηλότερος - πλήρης έλεγχος των κονδυλίων
- **Δραστηριότητες μαζικής μεταφοράς**: Εξαιρετικό με πρωτόκολλο COPY
- **Παραγωγικότητα προγραμματιστή**: Απαιτεί τις περισσότερες χειροκίνητες εργασίες

## Αντικείμενο Βιβλιοθήκες Χαρτογράφησης

Όταν εργάζεστε με Dapper ή ωμό ADO.NET, συχνά πρέπει να χαρτογραφήσετε μεταξύ διαφορετικών αντικειμενικών αναπαραστάσεων (DTOs, οντότητες, μοντέλα προβολής).

### Mapster: Mapping υψηλής απόδοσης

[MapsterCity name (optional, probably does not need a translation)](https://github.com/MapsterMapper/Mapster) είναι ένας γρήγορος, βασισμένος στη σύμβαση mapper αντικείμενο που χρησιμοποιεί την παραγωγή πηγής για τη βέλτιστη απόδοση.

```csharp
// 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: Ώριμη σύμβαση-Βασισμένο Mapper

[AutoMapperName](https://automapper.org/) είναι η πιο δημοφιλής βιβλιοθήκη χαρτογράφησης, αν και πιο αργή από Mapster.

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

### Χειροκίνητη χαρτογράφηση: Πλήρης έλεγχος

Μερικές φορές η καλύτερη προσέγγιση είναι η ρητή χειροκίνητη χαρτογράφηση:

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

### Σύγκριση Απόδοσης Χαρτογράφησης

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

**Key Takeaways:**

- Η χειροκίνητη χαρτογράφηση είναι γρηγορότερη αλλά απαιτεί περισσότερο κώδικα
- Ο Mapster είναι σχεδόν τόσο γρήγορος με λιγότερο κωδικό
- AutoMapper είναι βολικό, αλλά έχει υψηλότερα γενικά

## Υβριδικές προσεγγίσεις: Καλύτερα και από τους δύο κόσμους

Σε πραγματικές εφαρμογές, συχνά θέλετε να χρησιμοποιήσετε διαφορετικές προσεγγίσεις για διαφορετικά σενάρια μέσα στην ίδια εφαρμογή. **η συνιστώμενη προσέγγιση** για τα περισσότερα συστήματα παραγωγής.

### Μοτίβο CQRS: Διαχωρίζοντας διαβάζει και γράφει

Το μοτίβο CQRS (Command Query Query Departmentation) είναι μια φυσική εφαρμογή για υβριδικές προσεγγίσεις πρόσβασης δεδομένων. [ΜάρτενCity name (optional, probably does not need a translation)](https://martendb.io/), δείτε το άρθρο μου για [Σύγχρονη CQRS και Περιήγηση Εκδηλώσεων](/blog/moderncqrsandeventsourcing).

**Πώς ο Μάρτεν σχετίζεται με αυτή τη συζήτηση:**

Η Marten είναι μια βάση δεδομένων εγγράφων και ένα κατάστημα εκδηλώσεων χτισμένο στο PostgreSQL που παίρνει υβριδική πρόσβαση δεδομένων σε άλλο επίπεδο. Συνδυάζει:

- **Πρόσοδος γεγονότων** για γραφές (αμετάβλητα ρεύματα γεγονότων)
- **Προβολές** για τις αναγνώσεις (υλοποιημένες απόψεις βελτιστοποιημένες για ερωτήματα)
- **PostgreSQL's JSONB** για την αποθήκευση εγγράφων
- Ενσωματωμένα πρότυπα CQRS

Ενώ αυτό το άρθρο επικεντρώνεται στην παραδοσιακή πρόσβαση των σχετικών δεδομένων (EF Core, Dapper), Marten δείχνει πώς μπορείτε να αξιοποιήσετε τα προηγμένα χαρακτηριστικά του PostgreSQL (JSONB, ρεύματα γεγονότων) για την εφαρμογή εξελιγμένων αρχιτεκτονικών. Οι αρχές είναι οι ίδιες:

- Ξεχωριστά μοντέλα γραφής (optimized για συναλλαγές και συνέπεια)
- Ξεχωριστά μοντέλα ανάγνωσης (optimized για ερωτήματα και επιδόσεις)
- Χρησιμοποιήστε το σωστό εργαλείο για κάθε εργασία

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

Αυτό το μοτίβο αξιοποιεί:

- **Πυρήνας EF** για γράφει: Αλλαγή εντοπισμού, επικύρωση, κανόνες επιχειρήσεων
- **ΝτάππερCity name (optional, probably does not need a translation)** για τις αναγνώσεις: Μέγιστη απόδοση ερωτημάτων και ευελιξία
- Ίδια βάση δεδομένων, διαφορετικά πρότυπα πρόσβασης βελτιστοποιηθεί για κάθε περίπτωση χρήσης

### Εφαρμογή: CQRS με πυρήνα EF και Dapper

```csharp
// 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 πυρήνας με περιστασιακά ωμό SQL

Για εφαρμογές που είναι κυρίως πυρήνας EF αλλά χρειάζονται περιστασιακή βελτιστοποίηση απόδοσης:

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

## Σύγκριση επιδόσεων

Ας δούμε τα πραγματικά σημεία αναφοράς επιδόσεων για κοινές επιχειρήσεις με PostgreSQL:

### Αξιολόγηση: 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
```

### Αξιολόγηση: Εισαγωγή 1000 αρχείων

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

### Αξιολόγηση: Complex Join Query

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

### Απομακρύνσεις κλειδιών@ title: window

1. **Το ωμό Npgsql είναι πιο γρήγορο** αλλά απαιτεί τον πιο κωδικό
2. **Το Dapper προσφέρει εξαιρετική απόδοση** με ελάχιστο κόστος αφαίρεσης (60-70% γρηγορότερα από τον πυρήνα EF)
3. **Ερωτήματα για μη παρακολούθηση πυρήνα EF** είναι λογικές για τα περισσότερα σενάρια
4. **Δραστηριότητες μαζικών εργασιών** δείχνουν τα μεγαλύτερα κενά απόδοσης (10-13x διαφορά!)
5. **Κατανομή μνήμης** ακολουθήστε ένα παρόμοιο μοτίβο με το χρόνο εκτέλεσης

## Μήτρα απόφασης: Ποια προσέγγιση για χρήση

### Χρήση πυρήνα EF Όταν:

- Η κατασκευή μιας νέας εφαρμογής με εξελισσόμενες απαιτήσεις
- Διαδικτυακός σχεδιασμός με μοντέλα πλούσιων οντοτήτων
- Χρειάζεσαι μετανάστευση και διαχείριση σχημάτων
- Η ομάδα είναι πιο άνετη με το C# από το SQL
- Ο λόγος ανάγνωσης/γραφής είναι ισορροπημένος
- Η απόδοση της ερώτησης εντός 20-50% της βέλτιστης είναι αποδεκτή
- □ Θέλετε την παρακολούθηση αλλαγών και τη μονάδα του μοτίβου εργασίας

Βλέπεις; [Μέρος 1](/blog/orm-mapping-comparison-part1) για ολοκληρωμένη καθοδήγηση πυρήνα EF.

### Χρήση σκουπιδιού όταν:

- Η απόδοση είναι σημαντική αλλά όχι κρίσιμη
- □ Έχετε περίπλοκες ερωτήσεις που δεν χαρτογραφούν καλά σε LINQ
- Είσαι άνετα γράφοντας SQL
- Χρειάζεσαι καλό έλεγχο στην γενιά SQL
- Δουλεύοντας με τα υπάρχοντα σχήματα βάσης δεδομένων
- Διαβάζοντας-βαρύ φόρτο εργασίας με απλές γραφές
- Θέλετε ελάχιστη αφαίρεση από πάνω

### Χρήση ωμού Npgsql Όταν:

- Η μέγιστη απόδοση είναι κρίσιμη
- □ Επεξεργαστές δεδομένων υψηλής απόδοσης κτιρίων
- Δουλεύοντας εκτενώς με ειδικά χαρακτηριστικά PostgreSQL
- □ Δραστηριότητες πολλών και μαζικές εισαγωγές
- Κάθε χιλιοστά του δευτερολέπτου και μεγάβυτε μετράει
- Χρειάζεσαι απόλυτο έλεγχο.

### Υβριδική προσέγγιση όταν:

- □ Διαφορετικά μέρη της εφαρμογής έχουν διαφορετικές ανάγκες
- μοτίβο CQRS (EF Core για γράφει, Dapper για ανάγνωση)
- Τα περισσότερα ερωτήματα χρησιμοποιούν EF Core, αλλά μερικά χρειάζονται ωμό SQL
- Θέλετε σταδιακή μετανάστευση μεταξύ προσεγγίσεων
- □ Μεγάλες, περίπλοκες εφαρμογές με ποικίλες απαιτήσεις

## Περίληψη βέλτιστων πρακτικών

### Γενικές κατευθυντήριες γραμμές

1. **Έναρξη με πυρήνα EF** για νέα έργα εκτός εάν έχετε ειδικές απαιτήσεις επιδόσεων
2. **Προφίλ πριν τη βελτιστοποίηση** - μην υποθέσετε ότι χρειάζεστε Dapper / raw SQL
3. **Χρήση υβριδικών προσεγγίσεων** - συνδυάζουν τις δυνάμεις των διαφορετικών εργαλείων
4. **Κρατήστε τη λογική πρόσβασης δεδομένων απομονωμένη** - μοτίβο αποθήκευσης βοηθά στην αλλαγή εφαρμογών
5. **Χρήση συγκέντρωσης σύνδεσης** - ρυθμίστε σωστά για το φόρτο εργασίας σας
6. **Χαρακτηριστικά Leverage PostgreSQL** - μην αφηρήσετε ισχυρές δυνατότητες βάσης δεδομένων

### Συγκεκριμένο σκουπιδοτενεκέ

1. **Χρήση παραμετροποιημένων ερωτημάτων** για την πρόληψη της ένεσης SQL
2. **Σκεφτείτε το αποτέλεσμα ερώτημα caching** για ακριβά, επαναλαμβανόμενα ερωτήματα
3. **Χρήση πολλαπλών χαρτογραφήσεων** για την ένταξη αντί για πολλαπλές στρογγυλές εκδρομές
4. **Επαναχρησιμοποίηση συνδέσεων** από τη λίμνη σύνδεσης
5. **Σκεφτείτε Dapper.Contrib** για απλές λειτουργίες CRUD

### Συμβουλές απόδοσης

1. **Δείξε τα ερωτήματά σου.** - αναλύετε αργά ερωτήματα με το EXPLAIN ΑΝΑΛΥΣΗ
2. **Χρήση προετοιμασμένων δηλώσεων** για επαναλαμβανόμενες ερωτήσεις
3. **Δραστηριότητες παρτίδας** όταν είναι δυνατόν
4. **Χρήση COPY** για ένθετα χύμα σε PostgreSQL
5. **Παρακολούθηση πισίνας σύνδεσης** - Ρύθμιση min/max μεγέθους πισίνας
6. **Εξετάστε τα αντίγραφα που διαβάσατε.** για αναγνωσμένους βαρείς φόρτοι εργασίας

## Συμπέρασμα

Η επιλογή της σωστής προσέγγισης πρόσβασης δεδομένων για την εφαρμογή σας.NET με PostgreSQL δεν έχει να κάνει με την εύρεση του "καλύτερου" εργαλείου - πρόκειται για την αντιστοίχιση του σωστού εργαλείου με τις συγκεκριμένες ανάγκες σας:

- **Πυρήνας EF** (βλ. [Μέρος 1](/blog/orm-mapping-comparison-part1)) υπερέχει σε ταχεία ανάπτυξη, μοντελοποίηση τομέα, και εφαρμογές όπου η παραγωγικότητα του προγραμματιστή υπερτερεί ωμή απόδοση
- **ΝτάππερCity name (optional, probably does not need a translation)** παρέχει ένα εξαιρετικό μεσαίο έδαφος με σχεδόν βέλτιστη απόδοση και λογική αφαίρεση
- **Ακατέργαστο Npgsql** παρέχει μέγιστες επιδόσεις και έλεγχο για εργασίες έντασης δεδομένων
- **Βιβλιοθήκες χαρτογράφησης** όπως Mapster και AutoMapper μείωση λεβήτων κατά την εργασία με χαμηλότερη πρόσβαση δεδομένων

Στην πράξη, οι πιο επιτυχημένες εφαρμογές συχνά χρησιμοποιούν **υβριδική προσέγγιση**, αξιοποιώντας τις δυνάμεις κάθε εργαλείου, κατά περίπτωση:

- Χρήση **Πυρήνας EF** για τη λογική τομέα σας και γράφει
- Χρήση **ΝτάππερCity name (optional, probably does not need a translation)** για πολύπλοκα ερωτήματα ανάγνωσης και υποβολή εκθέσεων
- Χρήση **ακατέργαστο Npgsql** για εργασίες χύμα και αναλυτική

Το κλειδί είναι:

1. **Κατανοήστε τις απαιτήσεις σας** - απόδοση, ταχύτητα ανάπτυξης, ομαδικές δεξιότητες
2. **Προφίλ της εφαρμογής σας** - εντοπισμός πραγματικών σημείων συμφόρησης, όχι υποθέσεις
3. **Επιλέξτε ρεαλιστικά** - χρησιμοποιήστε το απλούστερο εργαλείο που καλύπτει τις ανάγκες σας
4. **Παραμείνετε ευέλικτοι.** - μπορείτε να αναμίξετε προσεγγίσεις μέσα στην ίδια εφαρμογή

Να θυμάστε: η πρόωρη βελτιστοποίηση είναι η ρίζα όλων των κακών, αλλά έτσι είναι η οικοδόμηση ενός συστήματος που δεν μπορεί να κλιμακωθεί όταν χρειάζεται.

## Αναφορές και Περαιτέρω Ανάγνωση

**Μέρος 1 της παρούσας σειράς:**

- [Πρόσβαση δεδομένων στο NET Μέρος 1: Κεντρικό πλαίσιο οντοτήτων](/blog/orm-mapping-comparison-part1)

**Επίσημη τεκμηρίωση:**

- [Dapper GitHub](https://github.com/DapperLib/Dapper)
- [Μαθήματα Ντάπερ](https://dapper-tutorial.net/)
- [Τεκμηρίωση Npgsql](https://www.npgsql.org/doc/)
- [Npgsql ADO.NET Provider](https://www.npgsql.org/doc/basic-usage.html)
- [Mapster GitHub](https://github.com/MapsterMapper/Mapster)
- [Αυτόματη τεκμηρίωση Mapper](https://docs.automapper.org/)
- [PostgreSQL Τεκμηρίωση](https://www.postgresql.org/docs/)

**Σχετικά άρθρα σε αυτό το blog:**

- [Προσθήκη πλαισίου οντοτήτων για δημοσιεύσεις blog](/blog/addingentityframeworkforblogpostspt1)
- [Μεταναστεύσεις EF Ο Σωστός Τρόπος](/blog/efmigrationstherightway)
- [Πλήρης αναζήτηση κειμένου με πυρήνα EF](/blog/textsearchingpt1)
- [Σύγχρονη CQRS και Περιήγηση Εκδηλώσεων](/blog/moderncqrsandeventsourcing)

---


*Αυτό ολοκληρώνει τη σειρά δύο μερών για την πρόσβαση δεδομένων στο .NET! Καλύψαμε τα πάντα από τις ισχυρές αφαιρέσεις του EF Core μέχρι τις μέγιστες επιδόσεις του SQL, με πρακτική καθοδήγηση για το συνδυασμό προσεγγίσεων για βέλτιστα αποτελέσματα.*