Najbardziej popularnym ORMem w świecie .NETu jest bez wątpienia Entity Framework. Posiada on wiele wbudowanych mechanizmów ułatwiających pracę z bazą danych, ale odpokutowuje to pewnymi niedogodnościami. Nie należy on do najwydajniejszych ORMów, generuje czasami pokrętne zapytania, oraz łatwo można wpaść w tarapaty, np. przez problem N+1. Istnieją również alternatywy, jak np. nHibernate, czy Dapper!
Jak sami twórcy wskazują, a są to ludzie od StackOverflow, Dapper jest królem wśród micro ORMów pod względem prostoty oraz wydajności działania. Jego zadanie polega tylko na przekształcaniu danych z jednego modelu w drugi, czyli na samej istocie działania mechanizmu ORM: mapowaniu. Nie uświadczycie tutaj wielu fajerwerków, zapytania pisze się z palca, wiec czasami trzeba się pogimnastykować z SQLem. Jest to darmowe rozwiązanie, którego kod źródłowy znajdziecie tutaj.
Dapper może działać z wieloma różnymi bazami danych. W rzeczywistości rozszerza on interfejs IDbConnection o kilka metod. Są to:
- Execute
- Query
- QueryFirst
- QueryFirstOrDefault
- QuerySingle
- QuerySingleOrDefault
- QueryMultiple
Ta pierwsza związana jest z modyfikowaniem danych. Za jej pomocą wykona się SQLowego: INSERTa, DELETEa, oraz UPDATEa, a w wyniku zwróci liczbę przetworzonych rekordów. Pozostałe to wariancje związane z pobieraniem danych.
Poniżej przedstawię jak użyć Dappera i wykonać na nim operacje CRUD.
Przygotowanie bazy danych
Zanim zaczniemy musimy mieć stworzoną bazę danych, do której się podepniemy. W przykładzie będzie zawierała ona jedną tabelę z trzema kolumnami.
Bazę danych utworzę ręcznie, za pomocą Visual Studio. Można to zrobić w SQL Server Object Expolorer, albo za pomocą SQLowego skryptu, albo za pomocą kreatora w którym definiuje się kolumny tabeli.
Skrypt:
CREATE TABLE [dbo].[User] ( [Id] INT NOT NULL PRIMARY KEY IDENTITY, [Name] NVARCHAR(100) NULL, [Age] INT NULL )
Designer:
Gdy już mamy stworzoną bazę warto przekopiować connectionStringa, który znajduje się w zakładce Properties. Będzie on potrzebny do połączenia się z bazą za pośrednictwem przykładowego programu.
W przeciwieństwie do Entity Framework, sam Dapper nie pozwala na stworzenie bazy danych. Nie ma tu czegoś takiego jak podejście Code First i generowanie bazy danych wraz z migracjami.
Instalacja Dappera
Do instalacji Dappera można posłużyć się nugetami. Albo zainstalować go z graficznego interfejsu:
albo za pomocą konsoli Package Manager Console:
Install-Package Dapper
CRUDujemy z Dapperem
Przyszła pora pobawić się Dapperem. Poniżej zobaczycie w jaki sposób można dokonać podstawowych operacji na bazie danych za pomocą tej biblioteki.
Dodawanie elementu (C)
string connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DapperCrudBlog;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; string insertSql = "INSERT INTO dbo.[User] (Name, Age) Values ('User1', 23);"; using (var connection= new SqlConnection(connectionString)) { var affectedRow= connection.Execute(insertSql); Console.WriteLine("Affected Row: " + affectedRow); }
W przykładzie łączymy się do naszej bazy danych przez SqlConnection, a następnie za pomocą Dappera przetwarzamy kod SQLowy. Wywołujemy go w metodzie Execute, co powoduje utworzenie nowego użytkownika. Dodatkowo w wyniku dostajemy liczbę utworzonych obiektów, w tym przypadku 1.
Rezultat widoczny w bazie danych:
W SQLu można zamieścić więcej zapytań, które Dapper przetworzy jedno po drugim.
string insertSql = @"INSERT INTO dbo.[User] (Name, Age) Values ('User2', 23); INSERT INTO dbo.[User] (Name, Age) Values ('User3', 23); INSERT INTO dbo.[User] (Name, Age) Values ('User4', 23);"; using (var connection= new SqlConnection(connectionString)) { var affectedRow= connection.Execute(insertSql); Console.WriteLine("Affected Row: " + affectedRow); }
Wynik w konsoli:
Odczyt elementów (R)
Do pobrania wszystkich elementów z bazy danych wykorzystamy metodę Query. W wyniku powinniśmy otrzymać 4 użytkowników, bo tyle rekordów znajduje się w bazie danych.
var querySql = "SELECT Id, Name, Age FROM dbo.[User]"; using (var connection= new SqlConnection(connectionString)) { var users = connection.Query(querySql); foreach (var user in users) { Console.WriteLine($"{user.Id} {user.Name} {user.Age}"); } }
Wynik w konsoli:
Dane pobrały się zgodnie z oczekiwaniami, ale nie zdefiniowaliśmy przecież żadnego typu. Dapper bardzo dobrze radzi sobie z typami anonimowymi.
Można jednak utworzyć typ danych odpowiadający tabeli User. Oto i on:
public class User { public int Id { get; set; } public string Login { get; set; } public int Age { get; set; } }
Dapper domyślnie przypisze odpowiednie pola o takiej samej nazwie. Dlatego też powyżej celowo zmieniłem nazwę property Name na Login, żeby pokazać jak wskazać kolumnę, której nazwa się nie zgadza.
var querySql = $@"SELECT Id, Name AS {nameof(User.Login)}, Age FROM dbo.[User]"; using (var connection= new SqlConnection(connectionString)) { var users = connection.Query<User>(querySql); foreach (var user in users) { Console.WriteLine($"{user.Id} {user.Login} {user.Age}"); } }
Efekt jest dokładni taki sam, jak w zapytaniu z typem anonimowym. W zapytaniu należy ręcznie wskazać, do której kolumny odwołujemy się, kiedy nazwa property nie zgadza się z nazwą kolumny. Można to zrobić za pomocą samego stringa (tutaj było by to “AS Login”), ale dużo wygodniej jest użyć nameof, co uwalnia od potrzeby edycji nazwy w czystych stringach, gdy zmianie ulegnie nazwa w modelu danych. W takim przypadku kompilator powiadomi nas zgłaszając błąd przy próbie kompilacji, a nie w trakcie działania programu.
Gdy pola nie będą sobie odpowiadały, Dapper nie przypisze do nich wartości, a zostaną nadane wartości domyślne. W typach referencyjnych jest to null.
Gdy wszystkie pola mają takie samie nazewnictwo i chcesz pobrać je wszystkie, SELECTa można skrócić do: SELECT * FROM tabela. Nie jest to jednak często stosowane.
Zapytania możemy oczywiście budować o wiele bardziej złożone niż jest to pokazane w przykładzie. Można tutaj użyć niemal wszystkiego co oferuje SQL: czy to WHERE, czy JOIN, itp.
Edycja elementów (U)
string querySql = $@"SELECT Id, Name AS {nameof(User.Login)}, Age FROM dbo.[User]"; string updateSql = "UPDATE dbo.[User] SET Name= 'Użytkownik1' WHERE Id = 1"; using (var connection= new SqlConnection(connectionString)) { var affectedRow= connection.Execute(updateSql); Console.WriteLine("Affected Row: " + affectedRow); var users = connection.Query<User>(querySql); foreach (var user in users) { Console.WriteLine($"{user.Id} {user.Login} {user.Age}"); } }
Działa dokładnie jak przy dodawaniu nowego rekordu. Należy zbudować zapytanie aktualizujące dane i wywołać je w metodzie Execute. Metoda zwróci liczbę przetworzonych elementów.
Usuwanie elementów (D)
var querySql = $@"SELECT Id, Name AS {nameof(User.Login)}, Age FROM dbo.[User]"; string deleteSql = "DELETE FROM dbo.[User]"; using (var connection= new SqlConnection(connectionString)) { var affectedRow= connection.Execute(deleteSql); Console.WriteLine("Affected Row: " + affectedRow); var users = connection.Query<User>(querySql); Console.WriteLine("Liczba uzytkowników: " + users.Count()); }
Tak samo działa i usuwanie. Odpowiednie zapytanie kończące żywot danych i w rezultacie metody Execute liczba usuniętych elementów. Efekt w konsoli:
Podsumowanie
Dapper to bardzo fajne narzędzie dające kontrole nad zapytaniami SQL, które wysyłasz do bazy danych. Szczyci się dużą wydajnością i jest wykorzystywane przede wszystkim do pobierania danych. Trzeba jednak te zapytania pisać własnoręcznie, co czasem zmusza nas do wypisywania wielu elementów, które chcemy pobrać. Gdy jest wiele joinów łatwo można się pogubić. Jest bardzo dobrą alternatywą do Entity Framework, jeżeli zależy Ci na tym, co ma do zaoferowania.
W przyszłości napisze jeszcze na temat Dappera, chociażby o parametrach, jakie można używać w zapytaniach.
A czy Wy używacie Dappera na co dzień w swoich projektach lub w pracy? Podzielcie się informacją w komentarzu.
PS. Dapper jest też elegancki, wiesz może dlaczego? 🙂