Dapper – podstawy

Proste i szybkie łączenie się z bazą danych

Piotr Wandycz
Narzędzia

W życiu każdego człowieka programisty i programistki przychodzi czas, że musi połączyć się z bazą danych. Są na to sposoby mniej i bardziej finezyjne. Dzisiaj pokażę Ci jeden z najprostszych, jakie znam, a zarazem najbardziej wydajnych. Z powodzeniem stosuję go w codziennej pracy – do odczytu danych, lub do pełnego CRUDa w prostej aplikacji.

Dapper to nakładka na tradycyjny SqlConnection. Dorzuca ona zbiór metod rozszerzających (extension method), ułatwiających odpytywanie bazy, oraz wykonywanie zmian. Używamy tutaj czystego SQLa, więc możemy mieć zoptymalizowane odczyty. Niestety Entity Framework ciągnie więcej danych, niż potrzebujemy, dopóki nie zaczniemy używać AutoMappera i metody ProjectTo. Jako że nie lubię stosować skomplikowanych rozwiązań, w swoim podejściu do CQRS przepuszczam odczyty (szybkość) przez Dappera, a zapisy (bezpieczeństwo) odbywają się przy użyciu EF.

Cały setup sprowadza się do pobrania dwóch Nugetów: System.Data.SqlClient oraz Dapper. Et voilà! Jedyne co musimy wymyślić sami to, w jaki sposób dostarczyć tu connection stringa do bazy. Najlepiej by było skorzystać z IConfiguration, a przy dużej aplikacji może zrobić jakąś owijkę na to. Dla uproszczenia zrobiłem po prostu klasę ze stałymi, więc tak wygląda minimalny kod do pobrania kolekcji wpisów z tabelki:

public async Task<IEnumerable<RecipeDto>> GetRecipesAsync()
{
    // Constants.ConnectionString => "Server=.\\SQL17; Database=Cookbook; Trusted_Connection=True;";
    using (var connection = new SqlConnection(Constants.ConnectionString))
    {
        var sql = "SELECT [Id], [Name], [Description], [CreatedAt] FROM [Recipe]";
        var result = await connection.QueryAsync<RecipeDao>(sql);
        return result.Select(x => new RecipeDto
        {
            Id = x.Id,
            Name = x.Name,
            Description = x.Description,
            CreatedAt = x.CreatedAt
        });
    }
}

Jeśli dorzuciliśmy using Dapper w pliku, to zmienna connection dostanie możliwość wywołania kilku dodatkowych metod. Najważniejsze z nich to: Query do zwracania kolekcji oraz QueryFirst/Single do pojedynczego wpisu. QueryMultiple na start jest niepotrzebne, pozwala ono na wykonanie kilku zapytań w jednym “batchu”.

Dużą zaletą jest tu silne typowanie zwracanego obiektu. Jeśli nie podalibyśmy zwracanego typu w nawiasach <>, to wróciłby nam dynamic. Jest to też miejsce, gdzie może nam się coś rozjechać. Musimy upewnić się, że nazwy zmiennych w zwracanym typie pokrywają się z nazwami wyciąganych kolumn w zapytaniu SQL.

Dla własnego bezpieczeństwa korzystam z Dao (data access object) i upewniam się, że pozostaje on prywatny w warstwie dostępu do bazy. Na zewnątrz wydostaje się Dto (data transfer object) czy inny read model. Nawet jeśli niektórzy uważają to za zbędne, to ja po prostu sobie nie ufam…

class RecipeDao
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class RecipeDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public DateTime CreatedAt { get; set; }
}

Staram się trzymać jak najprostsze zapytania SQL, sprowadzające się zawsze do odpytania jednej tabelki lub widoku. Jeśli istnieje potrzeba połączenia dwóch tabel JOINem, to tworzę taki widok w bazie, zaczynając jego nazwę od vw_. Wtedy od razu widzę czy zapytanie strzela do tabelki, czy widoku.

W zależności od wielkości aplikacji i innych potrzeb Dapper może być też wykorzystany do pozostałych operacji, zmieniających stan obiektów. Jeśli pisałbym małe narzędzie, składające się z 3 tabel po 2 kolumny, to nie pakowałbym Entity Framework do projektu. Do kompletu informacji brakuje nam jedynie jak wstrzyknąć wartość zmiennej do zapytania:

public async Task UpdateAuthor(AuthorDto author)
{
    using (var connection = new SqlConnection(Constants.ConnectionString))
    {
        var sql = "UPDATE [Author] SET [FirstName] = @FirstName, [LastName] = @Last WHERE [Id] = @Id";
        await connection.ExecuteAsync(sql, new { author.Id, author.FirstName, Last = author.LastName });
    }
}

W taki sam sposób możemy wstrzykiwać parametry do odpytywania bazy (np. żeby wyszukać coś po Id). Najważniejsze, żeby @nazwa parametru z SQL w stringu pokrywała się z tą przekazaną do metody Execute (patrz: @Last). Jeśli mielibyśmy tutaj Insert i chcielibyśmy pobrać Id nowo utworzonego rekordu, moglibyśmy wykorzystać ExecuteScalar i poza Insertem dorzucić SELECT SCOPE_IDENTITY() do SQL. Tylko wtedy trzeba pamiętać, że nawet jeśli nasz Id jest typu int, to MS SQL w tej funkcji zwraca decimal. Kiedyś zeszło mi trochę czasu, żeby ogarnąć, dlaczego mój kod się wykrzacza 🙂

Jeśli chciałbyś/chciałabyś poczytać więcej o Dapperze, zapraszam na stronę https://dapper-tutorial.net/dapper.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *