Persistir informações da aplicação é um requisito básico de quase todos os sistemas desenvolvidos atualmente. E o meio mais utilizado para isso são os bancos de dados relacionais.
Porém, o processo de persistência de dados em um banco de dados relacional não é simples, são necessárias uma gama de configurações, além de certas adaptações para que os objetos sejam persistidos corretamente no banco.
Para facilitar este processo, foram criados os frameworks ORM (Object-Relational Mapping – mapeamento objeto relacional). Eles se encarregam do trabalho pesado, como o processo de conversão dos objetos da aplicação em informações compreendidas pelo banco de dados (códigos SQL), e abstraem o máximo do acesso ao banco para facilitar a persistência de dados. Com isso, o desenvolvedor pode focar no que é mais importante para a aplicação.
Só que, com a facilidade, esses frameworks ORM acabaram comprometendo um pouco da performance das aplicações. Para resolver este problema, criou-se os chamados micro frameworks ORM. Este tipo abstrai apenas o necessário e otimiza ao máximo. Um dos frameworks que se destacam nesta categoria é o Dapper.NET.
Criado pela equipe por atrás do Stack Overflow, este micro framework ORM foi criado visando rapidez, fácil integração e liberdade de configuração. Com poucas linhas de código é possível adicioná-lo em uma aplicação e já fazer uso de todo o seu poder.
Para se ter ideia da sua rapidez e das características que o caracterizam, podemos ver a comparação que a sua equipe realiza com outros frameworks ORM disponíveis para .NET:
Curso C# (C Sharp) Avançado
Conhecer o cursoPerformance de uma pesquisa com 500 iterações - POCO serialization
Method | Duration | Remarks |
---|---|---|
Hand coded (using a SqlDataReader ) |
47ms | |
Dapper ExecuteMapperQuery |
49ms | |
ServiceStack.OrmLite (QueryById) | 50ms | |
PetaPoco | 52ms | Can be faster |
BLToolkit | 80ms | |
SubSonic CodingHorror | 107ms | |
NHibernate SQL | 104ms | |
Linq 2 SQL ExecuteQuery |
181ms | |
Entity framework ExecuteStoreQuery |
631ms |
Performance de uma pesquisa com 500 iterações - dynamic serialization
Method | Duration | Remarks |
---|---|---|
Dapper ExecuteMapperQuery (dynamic) |
48ms | |
Massive | 52ms | |
Simple.Data | 95ms |
Performance de uma pesquisa com 500 iterações - typical usage
Method | Duration | Remarks |
---|---|---|
Linq 2 SQL CompiledQuery | 81ms | Not super typical involves complex code |
NHibernate HQL | 118ms | |
Linq 2 SQL | 559ms | |
Entity framework | 859ms | |
SubSonic ActiveRecord.SingleOrDefault | 3619ms |
Para exemplificar o seu uso, vamos ver um exemplo dele em uma aplicação ASP.NET Core.
Criando a aplicação
No terminal digite o código abaixo para criar uma aplicação chamada AspNetCoreDapper:
dotnet new mvc -n AspNetCoreDapper
Agora, adicione o pacote do Dappper
:
dotnet add package Dapper
Não se esqueça de aplicar o restore
no projeto:
dotnet restore
Com isso já podemos começar a nossa configuração Dapper.
Criando o model / entidade
Para este exemplo será utilizada a entidade abaixo:
using System;
using System.ComponentModel.DataAnnotations;
namespace AspNetCoreDapper.Models
{
public class Product
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public int Quantity { get; set; }
[Required]
public double Price { get; set; }
}
}
Configurando o acesso ao banco com Dapper
Para este exemplo vou utilizar como banco de dados o SQLite, assim, antes de configurar o Dapper é necessário adicionar o pacote deste banco de dados na aplicação:
dotnet add package Microsoft.Data.Sqlite
Aplique o restore
para iniciarmos o processo de configuração do banco.
Diferente de outros frameworks ORM, o Dapper não define uma classe de configuração. Assim, configuraremos o acesso dele na classe base dos repositórios:
namespace AspNetCoreDapper.Repositories
{
public abstract class AbstractRepository<T>
{
private string _connectionString;
protected string ConnectionString => _connectionString;
public AbstractRepository(IConfiguration configuration){
_connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
}
public abstract void Add(T item);
public abstract void Remove(int id);
public abstract void Update(T item);
public abstract T FindByID(int id);
public abstract IEnumerable<T> FindAll();
}
}
O Dapper também não cria o banco de dados de acordo com as entidades. Nele, caso o banco ainda não esteja criado, ele deve ser criado “na mão”. Que é o que iremos fazer na classe abaixo:
namespace AspNetCoreDapper.Db
{
public class Seed
{
private static IDbConnection _dbConnection;
private static void CreateDb(IConfiguration configuration)
{
var connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
var dbFilePath = configuration.GetValue<string>("DBInfo:ConnectionString");
if (!File.Exists(dbFilePath))
{
_dbConnection = new SqliteConnection(connectionString);
_dbConnection.Open();
// Create a Product table
_dbConnection.Execute(@"
CREATE TABLE IF NOT EXISTS [Product] (
[Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
[Name] NVARCHAR(128) NOT NULL,
[Quantity] INTEGER NULL,
[Price] NUMERIC NOT NULL
)");
_dbConnection.Close();
}
}
}
}
E ele deve ser chamado na classe AbstractRepository
:
public AbstractRepository(IConfiguration configuration){
_connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
AspNetCoreDapper.Db.Seed.CreateDb(configuration);
}
Para finalizar a configuração de acesso Dapper, iremos definir o repositório abaixo:
namespace AspNetCoreDapper.Repositories
{
public class ProductRepository: AbstractRepository<Product>
{
public ProductRepository(IConfiguration configuration): base(configuration) { }
public override void Add(Product item)
{
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
string sQuery = "INSERT INTO Product (Name, Quantity, Price)"
+ " VALUES(@Name, @Quantity, @Price)";
dbConnection.Open();
dbConnection.Execute(sQuery, item);
}
}
public override void Remove(int id)
{
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
string sQuery = "DELETE FROM Product"
+ " WHERE Id = @Id";
dbConnection.Open();
dbConnection.Execute(sQuery, new { Id = id });
}
}
public override void Update(Product item)
{
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
string sQuery = "UPDATE Product SET Name = @Name,"
+ " Quantity = @Quantity, Price= @Price"
+ " WHERE Id = @Id";
dbConnection.Open();
dbConnection.Query(sQuery, item);
}
}
public override Product FindByID(int id)
{
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
string sQuery = "SELECT * FROM Product"
+ " WHERE Id = @Id";
dbConnection.Open();
return dbConnection.Query<Product>(sQuery, new { Id = id }).FirstOrDefault();
}
}
public override IEnumerable<Product> FindAll()
{
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
dbConnection.Open();
return dbConnection.Query<Product>("SELECT * FROM Product");
}
}
}
}
Note que o Dapper realiza o processo de impedância, a transformação dos dados do banco para entidades da aplicação de forma quase transparente:
dbConnection.Query<Product>("SELECT * FROM Product");
Na busca basta informar a classe que mapeia os dados do banco que o framework se encarregará de transformar os resultados obtidos em objetos desta classe. Aqui é importante que os nomes das colunas da tabela sejam iguais aos nomes das propriedades da classe, pois o Dapper irá realizar a transformação por Reflection.
Este mesmo processo é aplicado quando um dado é enviado para o banco:
using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
string sQuery = "INSERT INTO Product (Name, Quantity, Price)"
+ " VALUES(@Name, @Quantity, @Price)";
dbConnection.Open();
dbConnection.Execute(sQuery, item);
}
Também repare que é necessário informar o código SQL, pois este é um recurso dos frameworks ORM tradicionais que o Dapper não implementa. Assim, é possível definir um código mais eficiente, obtendo ainda mais performance.
Testando o Dapper
Para testar o Dapper, crie um controller:
namespace AspNetCoreDapper.Repositories
{
public class ProductController : Controller
{
private readonly ProductRepository productRepository;
public ProductController(IConfiguration configuration){
productRepository = new ProductRepository(configuration);
}
// GET: Products
public ActionResult Index()
{
return View(productRepository.FindAll().ToList());
}
// GET: Products/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return StatusCode(StatusCodes.Status404NotFound);
}
Product product = productRepository.FindByID(id.Value);
if (product == null)
{
return StatusCode(StatusCodes.Status404NotFound);
}
return View(product);
}
// GET: Products/Create
public ActionResult Create()
{
return View();
}
// POST: Products/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("Id,Name,Quantity,Price")] Product product)
{
if (ModelState.IsValid)
{
productRepository.Add(product);
return RedirectToAction("Index");
}
return View(product);
}
// GET: Products/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return StatusCode(StatusCodes.Status400BadRequest);
}
Product product = productRepository.FindByID(id.Value);
if (product == null)
{
return StatusCode(StatusCodes.Status404NotFound);
}
return View(product);
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind("Id,Name,Quantity,Price")] Product product)
{
if (ModelState.IsValid)
{
productRepository.Update(product);
return RedirectToAction("Index");
}
return View(product);
}
// GET: Products/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return StatusCode(StatusCodes.Status400BadRequest);
}
Product product = productRepository.FindByID(id.Value);
if (product == null)
{
return StatusCode(StatusCodes.Status404NotFound);
}
return View(product);
}
// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
productRepository.Remove(id);
return RedirectToAction("Index");
}
}
}
Também crie as views as actions acima e então podemos ver o sistema funcionando:
Você pode fazer download dos sources dessa aplicação clicando aqui.
Até a próxima!