Dapper – Micro ORM Framework
Dapper je malý a jednoduchý ORM (objektově relační mapper) pro .NET, který má na svědomí tým vývojářů ze StackOwerflow. Na StackOwerflow je nasazen v produkčním prostředí, takže se vůbec nemusíte bát ho použít na Vašem projektu. Na rozdíl od jiných ORM farameworků se vyznačuje jednoduchostí a především rychlostí.
Výkon
Klíčovou vlastností Dapperu je rchlost. To je dáno tím, že pracuje nad čistým ADO.NET data readerem jako rozšíření nad rozhraním IDbConnetion. Porovnání rychlostí ukazuje tabulka dole, kde se testovalo 500 selectů a mapování vráceného výsledku na POCO objekty.
Method | Duration |
---|---|
Hand coded (using a SqlDataReader) | 47ms |
Dapper ExecuteMapperQuery | 49ms |
ServiceStack.OrmLite (QueryById) | 50ms |
PetaPoco | 52ms |
BLToolkit | 80ms |
SubSonic CodingHorror | 107ms |
NHibernate SQL | 104ms |
Linq 2 SQL ExecuteQuery | 181ms |
Entity framework ExecuteStoreQuery | 631ms |
Dapper kromě mapování na POCO objekty podporuje i mapování na dynamické objekty, ale výsledky jsou prakticky stejné. Další tabulky a srovnání najdete v oficiální dokumetnaci.
Instalace
Dapper je vyvíjen pod Open Source licencí Appache 2.0 a MIT Licence takže je volně k použití. Instalace Dapperu je velmi jednoduchá prostě si do projektu přidáte NuGet balíček https://www.nuget.org/packages/Dapper a máte hotovo. Případně přes konzolu:
1 | PM> Install-Package Dapper |
Příklad použití
Další z řady výhod Dapperu je, že není implementován nad nějakou konkrétní databází. Jak už jsem psal Dapper rozšiřuje interface IDbConnection a může tak fungovat s kterýmkoliv ADO. NET providerem. Velmi jednoduše můžete vytvořit datovou vrstvu například s databází Oracle, MySQL, PostgreSQL, MS SQL nebo SQLite.
K dispozici máte metody Execute, Query, QueryFirst, QueryFirstOrDefault, QuerySingle, QuerySingleOrDefault, QueryMultiple plus jejich asynchronních verze. Abych mohl ukázat, jak se s Dapperem pracuje, potřebuji nějakou databázi a pár entit. Mějme tedy následující tabulky Invoice a InvoiceItem ve vazbě 1:N a k nim příslušné entity v C#.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CREATE TABLE [dbo].[Invoice] ( [IdInvoice] INT NOT NULL PRIMARY KEY IDENTITY, [InvoiceNumber] NVARCHAR(50) NOT NULL, [Total] DECIMAL NOT NULL, [Date] DATETIME NOT NULL ) CREATE TABLE [dbo].[InvoiceItem] ( [IdInvoiceItem] INT NOT NULL PRIMARY KEY IDENTITY, [Description] NVARCHAR(250) NOT NULL, [Qty] INT NOT NULL, [UnitPrice] DECIMAL NOT NULL, [IdInvoice] INT NOT NULL ) ALTER TABLE [dbo].[InvoiceItem] ADD CONSTRAINT fk_InvoiceItem_Invoice FOREIGN KEY ([IdInvoice]) REFERENCES [dbo].[Invoice] ([IdInvoice]) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Invoice { public int IdInvoice { get; set; } public string InvoiceNumber { get; set; } public decimal Total { get; set; } public DateTime Date { get; set; } public List<InvoiceItem> Items { get; set; } } public class InvoiceItem { public int IdInvoiceItem { get; set; } public string Description { get; set; } public int Qty { get; set; } public decimal UnitPrice { get; set; } public int IdInvoice { get; set; } public Invoice Invoice { get; set; } } |
Všechny příklady používají instanci SqlConnection (conn), kterou vrací metoda GetConnection(), ke spouštění commandů nad databází. Connection je potřeba před provedením příkazu otevřít a na konci taky zavřít. Na konci článku je testovací projekt, který si můžete stáhnout a příklady vyzkoušet.
1 2 3 4 5 6 | IDbConnection conn = GetConnection(); conn.Open(); /* Example */ conn.Close(); |
Execute
Execute je základní metoda určená ke spouštění příkazů Insert, Update, Delte a Stored Procedury. Provede příkaz a vrátí počet ovlivněných řádků.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /** Insert **/ string sql1 = "insert into dbo.Invoice(InvoiceNumber, Total, [Date]) " + "values (@InvoiceNumber, @Total, @Date)"; //1 conn.Execute(sql1, new {InvoiceNumber = "20180123001", Total = 55.5m, Date = DateTime.Now}); //2 conn.Execute(sql1, new[] { new { InvoiceNumber = "20180123002", Total = 100m, Date = DateTime.Now }, new { InvoiceNumber = "20180123003", Total = 101m, Date = DateTime.Now } }); //3 DynamicParameters prm1 = new DynamicParameters(); prm1.Add("InvoiceNumber", "20180123004"); prm1.Add("Total", 585); prm1.Add("Date", DateTime.Now); DynamicParameters prm2 = new DynamicParameters(); prm2.Add("InvoiceNumber", "20180123005"); prm2.Add("Total", 858); prm2.Add("Date", DateTime.Now); conn.Execute(sql1, new[] { prm1, prm2 }); //4 DynamicParameters prm3 = new DynamicParameters(); prm3.AddDynamicParams(new {InvoiceNumber = "20180123006", Total = 1000m, Date = DateTime.Now}); prm3.AddDynamicParams(new {InvoiceNumber = "20180123007", Total = 1001m, Date = DateTime.Now}); conn.Execute(sql1, prm3); |
Na příkladu jsou vidět čtyři použití metody Execute pro příkaz insert. Příklady se liší akorát ve způsobu předání parametrů, kdy v prvních dvou případech je použit anonymní typ a pole anonymních typů a ve třetím a čtvrtém případě je použit pro předání parametrů objekt DynamicParameters. Stejným způsobem se pracuje i s příkazy typu update, delete.
Na metodě Execute je dobré, že dokáže přijmout pole parametrů a podle počtu záznamu v poli automaticky spustí příslušný počet příkazů. Takto nějak vypadá dotaz, který je spuštěn na serveru. Týká se to případu 2. Je vidět, že se opravdu spustí 2 příkazy sp_executesql.
1 2 3 | exec sp_executesql N'insert into dbo.Invoice(InvoiceNumber, Total, [Date]) values (@InvoiceNumber, @Total, @Date)',N'@InvoiceNumber nvarchar(4000),@Total decimal(3,0),@Date datetime',@InvoiceNumber=N'20180123002',@Total=100,@Date='2018-01-24 20:42:16.663' exec sp_executesql N'insert into dbo.Invoice(InvoiceNumber, Total, [Date]) values (@InvoiceNumber, @Total, @Date)',N'@InvoiceNumber nvarchar(4000),@Total decimal(3,0),@Date datetime',@InvoiceNumber=N'20180123003',@Total=101,@Date='2018-01-24 20:42:16.673' |
Query
Metoda Query slouží pro získání záznamů z databáze. Má několik variant First, FirstOrDefault, Single a SingleOrDefault, které fungují obdobně jako podobně pojmenované metody v Linqu. Způsob vrácení výsledku ukazuje tabulka níže.
Result | No Item | One Item | Many Items |
---|---|---|---|
First | Exception | Item | First Item |
Single | Exception | Item | Exception |
FirstOrDefault | Default | Item | First Item |
SingleOrDefault | Default | Item | Exception |
V příkladu 1 je ukázka použití standardní metody Query pro vytažení všech záznamů z tabulky Invoice. První řádek vrací seznam dynamických objektů. Na druhém řádku je použita generická Query metoda, která umožní přetypovat výsledek dotazu přímo na seznam příslušných Invoice objektů. Příklad dva potom ukazuje složitější select s podmínkou a předáním parametrů. Pro parametry platí stejná pravidla jako u metody Execute.
1 2 3 4 5 6 7 8 9 10 11 12 | // 1 var sql = "SELECT * FROM dbo.Invoice i WITH (NOLOCK)"; IEnumerable<dynamic> result = connection.Query(sql); IEnumerable<Invoice> result = connection.Query<Invoice>(sql); // 2 var sql = @"SELECT * FROM dbo.Invoice i WITH(NOLOCK) LEFT JOIN dbo.InvoiceItem ii WITH(NOLOCK) ON ii.IdInvoice = i.IdInvoice WHERE i.IdInvoice IN @IdInvoice"; IEnumerable<Invoice> result = connection.Query<Invoice>(sql, new { IdInvoice = new[] {1, 2} }); |
Procedura
Pokud chcete spustit stored proceduru, opět k tomu můžete využít metodu Execute jen navíc musíte zadat typ příkazu commandType: CommandType.StoredProcedure. Z procedury lze samozřejmě data i vrátit použít ale musíte metodu Query.
1 2 3 4 | conn.Execute("[dbo].[p_InsertInvoice]", new { InvoiceNumber = "20180123002",Total = 100m,Date = DateTime.Now }, commandType: CommandType.StoredProcedure); |
QueryMultiple
QueryMultiple je obdoba metody Query s tím rozdílem, že umožňuje jedním vrzem spustit i více různých dotazů. Metoda pak vrací objekt GridReader ze kterého voláním metody Read() můžete přečíst výsledky jednotlivých dotazů. V některých případech si takto můžete připravit jedním tahem data a ušetřit několik výletů na DB server.
1 2 3 4 5 6 7 8 9 | // Posle se jako 1 dotaz var sql = "SELECT * FROM dbo.Invoice i WITH (NOLOCK) WHERE i.IdInvoice IN @IdInvoice;" + "SELECT * FROM dbo.InvoiceItem ii WITH (NOLOCK) WHERE ii.IdInvoice in @IdInvoice"; using (var multiple = connection.QueryMultiple(sql, new { IdInvoice = new[] { 1, 2,3, 4 } })) { IEnumerable<Invoice> invoices = multiple.Read<Invoice>(); IEnumerable<InvoiceItem> invoiceItem = multiple.Read<InvoiceItem>(); } |
MultiMapping
Při používání Dapperu můžete využít i tzv. MultiMapping. To znamená, že pokud váš objekt obsahuje vnořený objekt lze ho jedním selectem naplnit. Např. Když náš Invoice má několik InvoiceItem položek a chcete vytáhnout ty InvoiceItemy i s Invoicem můžu to provést následovně.
1 2 3 4 5 6 7 8 9 | var sql = @"SELECT * FROM Invoice i WITH (NOLOCK) JOIN InvoiceItem ii WITH(NOLOCK) ON i.IdInvoice = ii.IdInvoice"; var multiple = connection.Query<Invoice, InvoiceItem, InvoiceItem>(sql, (invoice, invoiceItem) => { invoiceItem.Invoice = invoice; return invoiceItem; }, splitOn: "IdInvoiceItem").Distinct().ToList(); |
Generická metoda Query pak přijímá tři generické typy, kde první dva jsou dva vstupní typy a poslední třetí typ je ten co chcete z metody vrátit. V anonymní funkci (která je volána pro každý řádek výsledků) je pak zařízeno doplnění objektu Invoice do InvoiceItem. Parametrem metody spliOn : „IdInvoiceItem“ pak Dapperu řeknete, jak má výsledek dotazu rozsekat a kde začíná další objekt.
Je zde potřeba dát pozor na pořadí generických typů, argumentů anonymní metody a pořadí atributů podle kterého chcete objekt dělit. Metoda je na tom silně závislá a jakýkoliv překlep způsobuje v lepším případě nenamapování některého z objektů.Nemyslím si ale, že je Dapper primárně k něčemu takovému určený. V případě složitějších objektů a hlouběji zanořených stromů objektů dá dost práce ty funkce napsat a je to navíc dost náchylné na chybu. Tady je potřeba zvážit, jestli nepoužít nějaký jiný framework, který toto zvládne dělat jednodušeji.
Transakce
Samozřejmostí je i možnost použití transakcí. Máte zde na výběr dvě možnosti, buď použijte objekt IDbTransaction, který musíte do metod předávat, nebo využijete TransactionScope.
1 2 3 4 5 6 7 8 9 10 | using (IDbTransaction transaction = connection.BeginTransaction()) { string sql1 = "insert into dbo.Invoice(InvoiceNumber, Total, [Date]) " + "values (@InvoiceNumber, @Total, @Date)"; connection.Execute(sql1, new {InvoiceNumber = "20180123001", Total = 55.5m, Date = DateTime.Now}, transaction); transaction.Commit(); } |
Nebo transaction scope
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using (var ts = new TransactionScope()) { using (IDbConnection connection = new DatabaseHelper().GetConnection()) { connection.Open(); string sql1 = "insert into dbo.Invoice(InvoiceNumber, Total, [Date]) " + "values (@InvoiceNumber, @Total, @Date)"; connection.Execute(sql1, new {InvoiceNumber = "20180123001", Total = 55.5m, Date = DateTime.Now}); } ts.Complete(); } |
Závěr
Na konec bych řek, že Dapper je slušný pomocník, pomocí kterého rychle vyrobíte databázovou vrstvu prakticky na jakémkoliv typu projektu. Jeho použití je opravdu jednoduché a naučit se to dá za pár hodin. Dapper je tak po právu nazýván králem mezi Micro ORM frameworky pro .NET. Já osobně navíc nejsem velký fanda různých připojených řešení a tenhle přístup se mi zamlouvá více než např. EntityFramework. Hodně sice záleží na typu aplikace, ale osobně mám raději, když mám dotazy pod kontrolu. Více o Dapperu se můžete dočíst na blogu Sama Saffrona a v oficiální dokumentaci na GitHubu.