Se você procura uma maneira simples e eficaz de incorporar um banco de dados leve e muito performático em suas aplicações, recomendo fortemente o LiteDB.
LiteDB é um embedded NoSQL Database multiplataforma para dotnet. Ele foi projetado para ser simples e plug-in-play, isto é, não demanda nenhuma configuração. Isso o torna uma excelente escolha para armazenamento de dados local em aplicativos desktop, móveis e até mesmo aplicações web.
Uma das maiores vantagens do LiteDB é que ele é um banco de dados incorporado, o que significa que não há necessidade de configurar ou gerenciar um servidor. Isso torna o LiteDB uma escolha ideal para aplicativos que precisam armazenar dados localmente. Basicamente nosso banco de dados é apenas um singelo arquivo, algo como o que acontece em aplicações que usam o SQLite, por exemplo.
É interessante e importante notar que existe uma certa "similaridade" entre a estrutura de dados do LiteDB e a estrutura de dados utilizada pelo MongoDB: Ambos são orientados a documentos, o que significa que armazenam dados em formato JSON (na verdade BSON - uma representação binária do JSON). Além disso, ambos são amplamente utilizados em cenários onde a flexibilidade no esquema de dados é essencial, permitindo que sejam adicionados campos e estruturas de dados sem a necessidade de alterações complexas no esquema.
Além disso, o LiteDB oferece suporte para transações ACID (Atomicidade, Consistência, Isolamento e Durabilidade), garantindo que suas operações sejam seguras e confiáveis, mesmo em cenários de alta concorrência.
Também temos as seguintes características:
- Thread-safe para Transações entre Coleções.
- Sem bloqueios para readers. Bloqueios de escrita por coleção.
- Suporte para pesquisa/carregamento parcial de documentos e consultas por índice.
- Suporte ao Linq ❤️
E sabe o que é o melhor de tudo? É um projeto 100% nacional, liderado pelo Maurício David, Cassiano Sombrio e Leandro Nascimento.
A minha ideia aqui nesse post, é passear por algumas das features do LiteDB e mostrar como é possível fazer um tão famigerado CRUD.
Outro ponto importante que quero mostrar é o quão eficiente é utilizar índices nas coleções para efetuar buscas mais performáticas. Aliás, no geral, usar índices em banco de dados é uma boa prática. Invista um tempo para entender como eles funcionam.
Mãos à obra...
Para começar, vamos criar um projeto console simples em dotnet e adicionar a biblioteca do LiteDB e o BenchmarkDotNet. Essa última biblioteca vai servir para que a gente faça algumas medições de performance nas consultas com e sem índice.
dotnet new console -o LiteDBSample
cd LiteDBSample
dotnet add package LiteDB
dotnet add package BenchmarkDotNet
Inserção em Lote
Vamos começar criando uma model chamada Pessoa
:
public class Pessoa
{
public int Id {get; set;}
public string Nome {get; set;} = string.Empty;
public string Email {get; set;} = string.Empty;
public int Idade {get; set;}
public string UF {get; set;} = string.Empty;
public override string ToString()
=> $"ID: {Id}, Nome: {Nome}, Email: {Email}, Idade: {Idade}, UF: {UF}";
}
Em seguida, no arquivo program.cs vamos inicializar nosso banco de dados. Caso o arquivo *.db não exista, ele será criado, se já existir, ele será carregado.
Depois de inicializado vamos criar nossa collection. Novamente aqui, o LiteDB é inteligente o suficiente para criar uma coleção
automagicamente caso ela não exista.
Uma coleção nada mais é do que o local onde serão armazenados os documentos. Nesse caso, indicamos que a coleção pessoas armazena dados do tipo Pessoa
. A partir do momento que temos a coleção criada podemos efetuar as operações de inserção, atualização, exclusão e consulta.
Vamos começar inserindo várias pessoas no nosso banco de dados. Você vai notar o quão fácil é fazer essa inserção em lote:
using LiteDB;
// Defina o nome do arquivo de banco de dados
const string databaseName = "LiteDBSample.db";
// Crie ou abra um banco de dados
using var db = new LiteDatabase(databaseName);
// Obtenha uma coleção chamada "pessoas". Caso não exista, na primeira inserção ela será criada
var pessoas = db.GetCollection<Pessoa>("pessoas");
// ChatGPT ajudou na criação dessa massa de dados ;)
var pessoa1 = new Pessoa { Id = 1, Nome = "José", Email = "jose@email.com", Idade = 30, UF = "SP" };
var pessoa2 = new Pessoa { Id = 2, Nome = "Maria", Email = "maria@email.com", Idade = 25, UF = "BH" };
var pessoa3 = new Pessoa { Id = 3, Nome = "Antônio", Email = "antonio@email.com", Idade = 28, UF = "SP" };
var pessoa4 = new Pessoa { Id = 4, Nome = "Sofia", Email = "sofia@email.com", Idade = 35, UF = "AC" };
var pessoa5 = new Pessoa { Id = 5, Nome = "Carlos", Email = "carlos@email.com", Idade = 22, UF = "CE" };
// Inserir as pessoas na coleção
// Aqui podemos notar que é possível efetuar um insert em lote \o/
pessoas.InsertBulk(new List<Pessoa> { pessoa1, pessoa2, pessoa3, pessoa4, pessoa5 });
// Ou, o caso mais comum, inserir uma pessoa por vez
var pessoa6 = new Pessoa { Id = 6, Nome = "Ana", Email = "ana@email.com", Idade = 27, UF = "RS" };
pessoas.Insert(pessoa6);
A inserção de itens dentro de uma collection no LiteDB é realmente muito simples. Basta utilizar o método InsertBulk e correr pro abraço! Caso queria inserir apenas uma pessoa, basta usar o método Insert. Simples e objetivo!
Agora, leia atentamente o código abaixo e responda se será possível inserir essa nova pessoa na coleção:
// Será que essa pessoa vai ser inserida na collection?
var pessoa7 = new Pessoa { Id = 1, Nome = "Mario", Email = "mario@email.com", Idade = 36, UF = "PE" };
pessoas.Insert(pessoa7);
A resposta é NÃO! Vamos receber a seguinte exception:
LiteDB.LiteException: Cannot insert duplicate key in unique index '_id'. The duplicate value is '1'.
Isso porque o LiteDB assume que a coluna Id
(ou ID
) é uma chave primária, e como já temos um registro com Id igual a 1, recebemos erro.
É possível utilizar qualquer nome - e tipo - de propriedade como chave primária, basta utilizar o atributo BsonId
:
public class Pessoa
{
[BsonId]
public int Codigo {get; set;}
/*...*/
}
Você pode optar por usar outros tipos de dados para o campo chave primária, como um Guid
por exemplo, porém o LiteDB disponibiliza um tipo específico para esses casos chamado ObjectId
:
public ObjectId Id { get; set; } = ObjectId.NewObjectId();
O ObjectId
é um tipo BSON de 12 bytes, contendo a seguinte estrutura:
- Timestamp: Valor que representa os segundos desde o início da era Unix (4 bytes)
- Machine: Identificador da máquina (3 bytes)
- Pid: Identificador do processo (2 bytes)
- Increment: Um contador, começando com um valor aleatório (3 bytes).
Abaixo segue um exemplo de uma representação JSON de um ObjectId
:
{ "$oid": "507f1f55bcf96cd799438110" }
Mais informações, acesse a documentação aqui.
Agora vamos seguir a diante, efetuando consultas, atualizações e exclusões.
Consulta simples
var maria = pessoas.FindOne(p => p.Email == "maria@email.com");
Console.WriteLine(maria);
Muito simples, não é? E temos LINQ?
var pessoasComMaisDe25Anos =
from pessoa in pessoas.Query() where pessoa.Idade > 25
select pessoa;
Console.WriteLine("\nPessoas com mais de 25 anos:");
foreach (var pessoa in pessoasComMaisDe25Anos.ToList())
{
Console.WriteLine(pessoa);
}
Sim temos :)
Dá pra agrupar?
var pessoasPorUF = pessoas
.FindAll()
.GroupBy(p => p.UF)
.Select(g => new
{
UF = g.Key,
Quantidade = g.Count()
});
Console.WriteLine("\nPessoas por UF:");
foreach (var pessoa in pessoasPorUF)
{
Console.WriteLine(pessoa);
}
Utilizando os métodos de extensão do LINQ conseguimos agrupar e saber quantas pessoas existem por UF. Moleza!
Existem outras maneiras de se efetuar consultas, agrupamentos, ordenações e etc. utilizando uma sintaxe muito parecida com a do SQL. Eu sinceramente nunca senti a necessidade de utilizar essa abordagem em código já que os métodos LINQ suprem praticamente todas as necessidades, porém caso queria saber mais sobre, acesse a documentação nesse link.
Eis um exemplo:
// note que pessoas é o nome da coleção configurado logo após criar o banco de dados...
using var reader = db.Execute("SELECT $ FROM pessoas WHERE Idade > 20");
while (reader.Read())
{
Console.WriteLine((object)reader. Current);
}
Aqui temos uma similaridade com o DataReader do nosso querido ADO.Net, e o mais interessante é que o retorno dessa execução é:
{
"_id":1,
"Nome":"José",
"Email":"jose@email.com",
"Idade":30,
"UF":"SP"
}
Caso queria obter o valor de cada coluna, basta utilizar:
var id = reader["_id"];
var nome = reader["Nome"].ToString();
/*...*/
Outra opção é usar uma consulta no método Where da coleção:
var pessoasComMaisDe20Anos = pessoas.Query().Where("Idade > 20").ToList();
foreach (var pessoa in pessoasComMaisDe20Anos)
{
Console.WriteLine(pessoa);
}
Se você curte essa abordagem, de fazer as consultas utilizando uma sintaxe parecida com SQL, recomendo muito utilizar o LiteDB Studio:
Essa ferramenta facilita muito o dia-a-dia, onde precisamos fazer consultas diversas em nosso banco de dados. Infelizmente não existe uma versão dela para macOS ou Linux, pelo menos eu não conheço :/
Atualizações
Se para inserir nós utilizamos o método Insert
da collection, para atualizar, nada mais justo do que ter um método chamado Update ;p
var antonio = pessoas.FindOne(x => x.Id == 3);
if (antonio != null)
{
antonio.Idade = 26;
pessoas.Update(antonio);
Console.WriteLine("\nAntônio após a atualização de idade:");
Console.WriteLine(Antonio);
}
Exclusão
Adivinha qual método usamos para excluir um item de uma coleção!
var jose = pessoas.FindOne(x => x.Id == 3);
if (jose != null)
{
pessoas.Delete(jose.Id);
Console.WriteLine("\nJosé foi excluído...");
}
Bingo!
Utilizando índices
Até agora vimos como utilizar os métodos para inserir, consultar, atualizar e excluir itens de uma coleção. Tudo isso de forma bem simples.
Porém, quando o assunto é consultar, o LiteDB nos dá a opção de criar índices em colunas o que torna nossa busca muito mais performática.
Como é descrito na documentação, cada índice armazena o valor de uma coluna específica de forma ordenada. Sem um índice, o LiteDB deve executar uma consulta usando uma verificação completa do documento. As varreduras completas de documentos são ineficientes porque o LiteDB precisa desserializar todos os documentos da coleção para aplicar o filtro de busca.
Para isso, podemos utilizar o método EnsureIndex
da coleção:
pessoas = db.GetCollection<Pessoa>("pessoas");
pessoas.EnsureIndex(x => x.Email);
Para saber mais, acesse a documentação.
Agora vamos medir a performance da consulta com e sem índice.
Para isso, criei um benchmark bem simples:
public class LiteDBBenchmark
{
private LiteDatabase _db;
private ILiteCollection<Pessoa> _pessoasComIndice;
private ILiteCollection<Pessoa> _pessoasSemIndice;
[GlobalSetup]
public void Setup()
{
var caminho = "BenchmarkIndice.db";
if (File.Exists(caminho))File.Delete(caminho);
_db = new LiteDatabase(caminho);
// Coleção com índice no campo de e-mail
_pessoasComIndice = _db.GetCollection<Pessoa>("pessoasComIndice");
_pessoasComIndice.EnsureIndex(x => x.Email);
// Coleção sem índice no campo de e-mail
_pessoasSemIndice = _db.GetCollection<Pessoa>("pessoasSemIndice");
for (var i = 0; i < 1_000; i++)
{
var pessoa = new Pessoa { Nome = $"Pessoa {i}", Email = $"email_{i}@email.com", Idade = i, UF = "SP" };
_pessoasComIndice.Insert(pessoa);
_pessoasSemIndice.Insert(pessoa);
}
}
[GlobalCleanup]
public void Cleanup()
{
_db.Dispose();
}
[Benchmark]
public void BuscarComIndice()
=> _pessoasComIndice.FindOne(p => p.Email == "email_1@exemplo.com");
[Benchmark]
public void BuscarSemIndice()
=> _pessoasSemIndice.FindOne(p => p.Email == "email_1@exemplo.com");
}
Basicamente, são criadas duas coleções, uma com um índice no campo e-mail e a outra sem o índice.
Em seguida são incluídos 1000 pessoas em cada coleção.
E na classe Program eu inicio os testes:
var summary = BenchmarkRunner.Run<LiteDBBenchmark>();
Para executar o benchmark é necessário rodar a aplicação no modo Release:
sudo dotnet run --configuration Release
Alguns segundos depois temos o resultado:
Method | Mean | Error | StdDev |
---|---|---|---|
BuscarComIndice | 9.191 us | 0.0289 us | 0.0270 us |
BuscarSemIndice | 701.412 us | 1.8909 us | 1.6762 us |
Impressionante a diferença, não é?
Pra fechar, reforço aqui minha ideia de que esse post serve apenas para demonstrar um apanhando de features que o LiteDB dispõe. Existem várias outras como armazenamento de arquivos por exemplo.
Recomendo muito que vocês acessem o github do projeto https://github.com/mbdavid/litedb e deixem uma estrelinha! Isso é muito importante para valorizar esse trabalho fantástico, 100% nacional liderado pelo Maurício David, Cassiano Sombrio e Leandro Nascimento!
Testem aí o LiteDB e deixem seus comentários!
Código Fonte: https://github.com/angelobelchior/LiteDBSample
Até a próxima!
Top comments (0)