Modelagem de Dados NoSQL e Persistência de Objetos no Redis com C#

Bancos de dados NoSQL orientados a chave-valor, como o Redis, operam fundamentalmente de maneira diferente dos sistemas de gerenciamento de banco de dados relacionais (RDBMS). A ausência de operações nativas como JOIN ou UNION exige uma abordagem de modelagem baseada em desnormalização e agregação de dados. Em vez de espalhar entidades em tabelas altamente normalizadas, os dados são frequentemente estruturados como objetos complexos aninhados ou indexados através de coleções específicas do Redis, como Lists, Sets e Sorted Sets.

Para ilustrar essa arquitetura na plataforma .NET, considere um sistema de gerenciamento de conteúdo onde autores (Author) gerenciam publicações (Publication), que por sua vez contêm artigos (Article) com categorias, tópicos e feedbacks dos leitores (Feedback).

Definição do Domínio

No ambiente NoSQL, as relações são frequentemente resolvidas embutindo objetos diretamente ou armazenando listas de identificadores de referência. Utilizando um cliente Redis, os objetos C# podem ser serializados e persistidos diretamente na memória.

public class Author
{
    public Author() => PublicationIds = new List<long>();
    
    public long Id { get; set; }
    public string FullName { get; set; }
    public List<long> PublicationIds { get; set; }
}

public class Publication
{
    public Publication()
    {
        Topics = new List<string>();
        ArticleIds = new List<long>();
    }

    public long Id { get; set; }
    public long AuthorId { get; set; }
    public string AuthorName { get; set; }
    public List<string> Topics { get; set; }
    public List<long> ArticleIds { get; set; }
}

public class Article
{
    public Article()
    {
        Categories = new List<string>();
        Topics = new List<string>();
        Feedbacks = new List<Feedback>();
    }

    public long Id { get; set; }
    public long PublicationId { get; set; }
    public string Headline { get; set; }
    public string Body { get; set; }
    public List<string> Categories { get; set; }
    public List<string> Topics { get; set; }
    public List<Feedback> Feedbacks { get; set; }
}

public class Feedback
{
    public string Message { get; set; }
    public DateTime Timestamp { get; set; }
}

Configuração e Inicialização de Dados

Utilizando a biblioteca ServiceStack.Redis, é possível interagir com essas entidades através de clientes fortemente tipados. O cenário abaixo configura um ambiente de teste de integração para popular o banco de dados em memória com dados simulados.

[TestFixture]
public class ContentManagementTests
{
    private readonly IRedisClient _redis = new RedisClient("127.0.0.1");

    [SetUp]
    public void Initialize()
    {
        _redis.FlushAll();
        SeedData();
    }

    private void SeedData()
    {
        var authorRepo = _redis.As<Author>();
        var pubRepo = _redis.As<Publication>();
        var articleRepo = _redis.As<Article>();

        var alice = new Author { Id = authorRepo.GetNextSequence(), FullName = "Alice Silva" };
        var bob = new Author { Id = authorRepo.GetNextSequence(), FullName = "Bob Costa" };

        var techPub = new Publication
        {
            Id = pubRepo.GetNextSequence(),
            AuthorId = alice.Id,
            AuthorName = alice.FullName,
            Topics = new List<string> { "Software", "Cloud", "DevOps" }
        };

        var articles = new List<Article>
        {
            new Article
            {
                Id = articleRepo.GetNextSequence(),
                PublicationId = techPub.Id,
                Headline = "Microservices Architecture",
                Categories = new List<string> { "Backend", "Design" },
                Topics = new List<string> { "Docker", "Kubernetes" },
                Feedbacks = new List<Feedback>
                {
                    new Feedback { Message = "Excelente artigo!", Timestamp = DateTime.UtcNow }
                }
            },
            new Article
            {
                Id = articleRepo.GetNextSequence(),
                PublicationId = techPub.Id,
                Headline = "NoSQL Patterns",
                Categories = new List<string> { "Database", "Backend" },
                Topics = new List<string> { "Redis", "MongoDB" },
                Feedbacks = new List<Feedback>
                {
                    new Feedback { Message = "Muito útil.", Timestamp = DateTime.UtcNow },
                    new Feedback { Message = "Gostaria de ver mais exemplos.", Timestamp = DateTime.UtcNow }
                }
            }
        };

        // Estabelecendo as relações via identificadores
        alice.PublicationIds.Add(techPub.Id);
        techPub.ArticleIds.AddRange(articles.Select(a => a.Id));

        // Persistindo no Redis
        authorRepo.Store(alice);
        authorRepo.Store(bob);
        pubRepo.Store(techPub);
        articleRepo.StoreAll(articles);
    }
}

O método GetNextSequence() é utilizado para gerar identificadores autoincrementais nativos no Redis, simulando o comportamenot de chaves primárias de bancos relacionais.

Operações de Leitura e Estruturas de Dados

Recuperação Simples

Para obter todas as instâncias de uma entidade armazenada, o cliente tipado fornece métodos de recuperação em lote.

[Test]
public void RetrieveAllPublications()
{
    var pubRepo = _redis.As<Publication>();
    var allPubs = pubRepo.GetAll();
    
    // allPubs contém todas as publicações desserializadas da memória
}

Feed de Atividade Recente (Listas)

As estruturas de Lista do Redis são ideais para implementar feeds de atividades ou históricos com limite de tamanho (Rolling Lists).

[Test]
public void BuildRecentActivityFeed()
{
    var articleRepo = _redis.As<Article>();
    var feedbackRepo = _redis.As<Feedback>();

    var latestArticles = articleRepo.Lists["urn:feed:latest_articles"];
    var latestFeedbacks = feedbackRepo.Lists["urn:feed:latest_feedbacks"];

    foreach (var article in articleRepo.GetAll())
    {
        // Insere no início da lista
        latestArticles.Prepend(article);
        article.Feedbacks.ForEach(f => latestFeedbacks.Prepend(f));
    }

    // Mantém apenas os 5 itens mais recentes em cada lista
    latestArticles.Trim(0, 4);
    latestFeedbacks.Trim(0, 4);
}

Nuvem de Tópicos (Sorted Sets)

Para criar uma nuvem de tópicos onde a frequência de cada termo é contabilizada, o Sorted Set é a estrutura mais adequada, permitindo incrementos atômicos e consultas ordenadas por pontuação.

[Test]
public void GenerateTopicCloud()
{
    var articleRepo = _redis.As<Article>();
    
    foreach (var article in articleRepo.GetAll())
    {
        article.Topics.ForEach(topic => 
            _redis.IncrementItemInSortedSet("urn:analytics:topic_cloud", topic, 1));
    }

    // Recupera os 5 tópicos mais populares com suas respectivas contagens
    var topTopics = _redis.GetRangeWithScoresFromSortedSetDesc("urn:analytics:topic_cloud", 0, 4);
}

Catálogo Exclusivo (Sets)

O Set do Redis garante a unicidade dos elementos, sendo perfeito para armazenar categorias ou tags globais sem duplicatas.

[Test]
public void ExtractUniqueCategories()
{
    var articleRepo = _redis.As<Article>();
    
    foreach (var article in articleRepo.GetAll())
    {
        article.Categories.ForEach(cat => 
            _redis.AddItemToSet("urn:catalog:categories", cat));
    }

    var distinctCategories = _redis.GetAllItemsFromSet("urn:catalog:categories");
}

Atualização de Documentos Aninhados

Como os comentários (Feedbacks) são objetos de valor aninhados dentro do documento do artigo, adicionar um novo feedback requer a recuperação do objeto pai, a modificação da coleção e a persistência subsequente.

[Test]
public void AppendFeedbackToArticle()
{
    var articleRepo = _redis.As<Article>();
    var targetId = "1";
    
    var article = articleRepo.GetById(targetId);
    article.Feedbacks.Add(new Feedback 
    { 
        Message = "Nova dúvida sobre o tema.", 
        Timestamp = DateTime.UtcNow 
    });
    
    // Sobrescreve o documento inteiro no Redis
    articleRepo.Store(article);
}

Índices Secundários

Para consultar artigos por categoria de forma eficiente sem realizar varreduras completas (Full Table Scans), é necessário construir índices secundários manuais utilizando Sets como tabelas de mapeamento.

[Test]
public void IndexArticlesByCategory()
{
    var articleRepo = _redis.As<Article>();
    
    // Construção do índice invertido
    foreach (var article in articleRepo.GetAll())
    {
        article.Categories.ForEach(cat => 
            _redis.AddItemToSet($"urn:index:category:{cat}", article.Id.ToString()));
    }

    // Consulta utilizando o índice
    var backendArticleIds = _redis.GetAllItemsFromSet("urn:index:category:Backend");
    
    // Busca em lote pelos identificadores extraídos do índice
    var backendArticles = articleRepo.GetByIds(backendArticleIds);
}

Tags: C# Redis nosql ServiceStack.Redis key-value-store

Publicado em 6-29 00:10