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.

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)