LiteDB: Um banco de dados NoSQL simples e poderoso para dotnet!
Tags: dotnet, database, csharp, nosql
Published: 2023-10-04 22:30:00

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**](https://www.litedb.org/).

[**LiteDB**](https://www.litedb.org/) é 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"](https://www.sympla.com.br/evento-online/apis-robustas-com-asp-net-core-8-entity-framework-docker-e-redis/2315443?referrer=www.vemcodar.com.br) 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/](https://vemcodar.com.br/)**

---

Uma das maiores vantagens do [**LiteDB**](https://www.litedb.org/) é 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**](https://www.litedb.org/) 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**](https://www.sqlite.org/index.html), por exemplo. 

É interessante e importante notar que existe uma certa "**similaridade**" entre a estrutura de dados do [**LiteDB**](https://www.litedb.org/) e a estrutura de dados utilizada pelo [**MongoDB**](https://www.mongodb.com/pt-br): Ambos são orientados a documentos, o que significa que armazenam dados em formato [**JSON**](https://www.json.org/json-en.html) (na verdade [**BSON**](https://www.mongodb.com/json-and-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**](https://www.litedb.org/) oferece suporte para [**transações ACID (Atomicidade, Consistência, Isolamento e Durabilidade)**](https://pt.wikipedia.org/wiki/ACID), 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**](https://github.com/mbdavid), [**Cassiano Sombrio**](https://github.com/kcsombrio) e [**Leandro Nascimento**](https://github.com/lbnascimento).

A minha ideia aqui nesse post, é passear por algumas das _features_ do [**LiteDB**](https://www.litedb.org/) e mostrar como é possível fazer um tão famigerado [**CRUD**](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete).

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**](https://en.wikipedia.org/wiki/Database_index). 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**](https://www.litedb.org/) e o [**BenchmarkDotNet**](https://github.com/dotnet/BenchmarkDotNet). Essa última biblioteca vai servir para que a gente faça algumas medições de performance nas consultas com e sem índice.

```bash
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`:

```csharp
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**](https://www.litedb.org/) é 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:

```csharp
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**](https://www.litedb.org/) é 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:

```csharp
// 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_:

```csharp
LiteDB.LiteException: Cannot insert duplicate key in unique index '_id'. The duplicate value is '1'.
```

Isso porque o [**LiteDB**](https://www.litedb.org/) assume que a coluna `Id` (ou `ID`) é uma [**chave primária**](https://pt.wikipedia.org/wiki/Chave_prim%C3%A1ria), 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`:

```csharp
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**](https://www.litedb.org/) disponibiliza um tipo específico para esses casos chamado `ObjectId`: 

```csharp
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`:

```json
{ "$oid": "507f1f55bcf96cd799438110" }
```

Mais informações, acesse a documentação [aqui](https://www.litedb.org/docs/data-structure/).

Agora vamos seguir a diante, efetuando consultas, atualizações e exclusões.

### Consulta simples

```csharp
var maria = pessoas.FindOne(p => p.Email == "maria@email.com");
Console.WriteLine(maria);
```

Muito simples, não é? E temos LINQ?

```csharp
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?

```csharp
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](https://pt.wikipedia.org/wiki/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**](https://www.litedb.org/api/query/).

Eis um exemplo:

```csharp
// 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_](https://learn.microsoft.com/pt-br/dotnet/framework/data/adonet/retrieving-data-using-a-datareader) do nosso querido [_ADO.Net_](https://learn.microsoft.com/pt-br/dotnet/framework/data/adonet/), e o mais interessante é que o retorno dessa execução é:

```json
{
   "_id":1,
   "Nome":"José",
   "Email":"jose@email.com",
   "Idade":30,
   "UF":"SP"
}
```

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

```csharp
var id = reader["_id"];
var nome = reader["Nome"].ToString();
/*...*/
```

Outra opção é usar uma consulta no método Where da coleção:

```csharp
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**](https://github.com/mbdavid/LiteDB.Studio):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8w5ol3h5ble5f1gg8mw7.gif)

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

```csharp
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!

```csharp
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**](https://www.litedb.org/) 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**](https://www.litedb.org/) deve executar uma consulta usando uma verificação completa do documento. As varreduras completas de documentos são ineficientes porque o [**LiteDB**](https://www.litedb.org/) 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:

```csharp
pessoas = db.GetCollection<Pessoa>("pessoas");
pessoas.EnsureIndex(x => x.Email);
```

Para saber mais, acesse a [**documentação**](https://www.litedb.org/docs/indexes/).

Agora vamos medir a performance da consulta com e sem índice.

Para isso, criei um benchmark bem simples:

```csharp
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:

```csharp
var summary = BenchmarkRunner.Run<LiteDBBenchmark>();
```

Para executar o _benchmark_ é necessário rodar a aplicação no modo _Release_:

```bash
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**](https://www.litedb.org/) 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**](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**](https://github.com/mbdavid), [**Cassiano Sombrio**](https://github.com/kcsombrio) e [**Leandro Nascimento**](https://github.com/lbnascimento)!

Testem aí o [**LiteDB**](https://www.litedb.org/) e deixem seus comentários!

**Código Fonte**: [https://github.com/angelobelchior/LiteDBSample](https://github.com/angelobelchior/LiteDBSample)

Até a próxima!