DEV Community

Cover image for LiteDB: Um banco de dados NoSQL simples e poderoso para dotnet!
Angelo Belchior
Angelo Belchior

Posted on • Edited on

LiteDB: Um banco de dados NoSQL simples e poderoso para dotnet!

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.


Antes de continuar, #VemCodar com a gente!!

Tá afim de criar APIs robustas com .NET?

Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.

Não fique de fora! Dê um Up na sua carreira!

O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs

Acesse: https://vemcodar.com.br/


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
Enter fullscreen mode Exit fullscreen mode

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}";
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

A resposta é NÃO! Vamos receber a seguinte exception:

LiteDB.LiteException: Cannot insert duplicate key in unique index '_id'. The duplicate value is '1'.
Enter fullscreen mode Exit fullscreen mode

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;}
    /*...*/
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Caso queria obter o valor de cada coluna, basta utilizar:

var id = reader["_id"];
var nome = reader["Nome"].ToString();
/*...*/
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Se você curte essa abordagem, de fazer as consultas utilizando uma sintaxe parecida com SQL, recomendo muito utilizar o LiteDB Studio:

Image description

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);
}
Enter fullscreen mode Exit fullscreen mode

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...");
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

Para executar o benchmark é necessário rodar a aplicação no modo Release:

sudo dotnet run --configuration Release
Enter fullscreen mode Exit fullscreen mode

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)