Repositórios Plug-and-Play

No artigo anterior falamos sobre o uso de JOINs para extrair as informações que armazenamos em uma base de dados relacional. Comentamos também das dificuldades que temos quando precisamos manter os dados originais do momento da execução daquela tarefa (exemplo: o nome do cliente no momento da emissão da nota fiscal). No fim deste mesmo artigo, falamos em segregação da base de dados, a fim de ter estruturas distintas e otimizadas para leitura e gravação.

Dando continuidade no artigo, vamos falar sobre a base de dados de escrita. Quando trabalhamos com um domínio rico, baseado em DDD, é comum nossas classes possuírem diversas propriedades, que representam suas características, bem como funcionalidades e comportamentos que refletem, em geral, as mesmas funções do mundo real. Uma vez que este domínio está desenvolvido, chega o momento de decidir como vamos persisti-lo fisicamente. E a resposta na maioria das vezes, a resposta sempre é o uso de uma base de dados relacional, tais como: SQL Server, Oracle, MySQL, etc.

O problema entre o nosso domínio e uma base de dados relacional é a impedância. Isso quer dizer, o atrito que temos ao mapear classes e propriedades para tabelas, colunas e linhas. Trata-se de um trabalho árduo, e muitas vezes recorremos à mapeadores (ORM) para auxiliar nesta atividade, e que apesar de fazerem um grande trabalho por convenção, há muito o que se customizar em um ambiente mais complexo. Isso sem falar sobre evolução do domínio, onde a criação de novos tipos, adição e remoção de novas propriedades que precisam ser armazenadas, refletirá no mapeamento e que deverá ser ajustado.

Por essa flexibilidade na evolução e diversos outros pontos positivos, é que bases NoSQL estão cada vez mais ganhando espaço. E aqui, vamos tentar fazer a migração de uma aplicação que faz uso de NHibernate para RavenDB. Como estamos trabalhando orientado ao domínio, nós já temos interfaces em nosso código que definem a estrutura dos repositórios que a mesma utiliza. Por padrão, temos apenas a implementação para o NHibernate:

public class RepositorioDeClientes : IRepositorioDeClientes
{
    private readonly ISession session;

    public RepositorioDeClientes(ISession session)
    {
        this.session = session;
    }

    public void Salvar(Cliente entidade)
    {
        session.Save(entidade);
    }

    public Cliente BuscarPor(string cnpj)
    {
        return
            (
                from c in session.Query<Cliente>()
                where c.Empresa.Cnpj == cnpj
                select c
            ).SingleOrDefault();
    }
}

Só que o repositório ainda depende do arquivo de mapeamento (HBM) para fazer o de/para das classes para as tabelas. Vou omitir o mapeamento aqui. Ele estará disponível no código fonte do projeto que estará relacionado à este artigo. Como a interface define a estrutura, criamos uma implementação do repositório para o RavenDB. É possível notar que a API do ORM é bem semelhante à API do RavenDB.

public class RepositorioDeClientes : IRepositorioDeClientes
{
    private readonly IDocumentSession session;

    public RepositorioDeClientes(IDocumentSession session)
    {
        this.session = session;
    }

    public void Salvar(Cliente entidade)
    {
        session.Store(entidade);
        session.SaveChanges();
    }

    public Cliente BuscarPor(string cnpj)
    {
        return
            (
                from c in session.Query<Cliente>()
                where c.Empresa.Cnpj == cnpj
                select c
            ).SingleOrDefault();
    }
}

Se notarmos os códigos que criam um novo cliente utilizando os repositórios criados acima, eles serão idênticos, exceto pela criação, que cada um deve recorrer ao seu próprio modelo de construção e conexão. Se você injeta o repositório em outras classes, elas continuarão funcionando de forma transparente, já que toda a “complexidade” está embutida no repositório, e sua interface continua expondo os mesmos métodos.

public static class Repositorios
{
    public static void Executar()
    {
        ViaNHibernate();
        ViaRavenDB();
    }

    private static void ViaNHibernate()
    {
        using (var session = NHibernateContext.Factory.OpenSession())
        {
            var repositorio = new R.NHibernate.RepositorioDeClientes(session);
            var cliente = new Cliente(new Empresa() { RazaoSocial = "Nome Ltda", Cnpj = "123" });

            repositorio.Salvar(cliente);

            Console.WriteLine(repositorio.BuscarPor(cliente.Empresa.Cnpj).Empresa.RazaoSocial);
        }
    }

    private static void ViaRavenDB()
    {
        using (var session = RavenDBContext.Store.OpenSession())
        {
            var repositorio = new R.RavenDB.RepositorioDeClientes(session);
            var cliente = new Cliente(new Empresa() { RazaoSocial = "Nome Ltda", Cnpj = "123" });

            repositorio.Salvar(cliente);

            Console.WriteLine(repositorio.BuscarPor(cliente.Empresa.Cnpj).Empresa.RazaoSocial);
        }
    }
}

E como uma das principais características de bases NoSQL é não ter um schema predefinido, não há nada que se precise fazer para estruturar a base antes de receber os dados. E com isso, todos os arquivos HBM de mapeamento do NHibernate (ou se usar o Fluent NHibernate), podem ser completamente descartados. O método BuscarPor está sendo chamado aqui apenas para gerar o round-trip de testes.

Relacionamentos

Quando trabalhamos com base de dados relacional, utilizamos os relacionamentos para referenciar em uma linha o conteúdo de outra, ou seja, para complementar a informação, e evitando com isso, a redundância de dados. Para dar um exemplo, quando um pedido é realizado, ao invés de armazenar nos itens do mesmo toda a informação acerca do produto comprado, simplesmente referenciamos o produto através de seus Id como uma chave estrangeira, e através de JOINs, conseguimos remontar o pedido e saber quais produtos foram comprados.

Como mencionei no artigo anterior, além do custo de executar um JOIN, muitas vezes a redundância é benéfica. E vale ressaltar novamente aqui: o custo de armazenamento, via de regra, é mais barato que o custo de processamento. Ao mudar para uma base de dados NoSQL, os relacionamentos são tratados de forma um pouco diferente aos quais conhecemos no mundo relacional. Abaixo vou mostrar alguns experimentos, e entender o comportamento de cada um deles para podemos escolher o qual melhor se encaixa com a nossa necessidade.

Para o exemplo, vamos considerar um exemplo bastante trivial: uma classe chamada Pedido, que possui os itens que foram comprados. Para representar cada item, teremos a classe Item, que, em princípio, referenciará o produto (classe Produto) que foi adquirido. Essa classe irá ser modificada para exemplificar cada um dos cenários. Por fim, a classe Pedido irá expor uma propriedade chamada Itens que retorna todos os itens associados à ele.

Flat

A primeira opção é conhecida como flat, ou seja, as referências serão armazenadas como parte do documento principal, embutindo todas as propriedades e seus respectivos valores. Para este primeiro cenário, a estrutura da classe Item do pedido será a seguinte:

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.Produto = produto;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public Produto Produto { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

Note que a instância da classe produto é passada como parâmetro no construtor da classe Item e armazenado em uma propriedade. O RavenDB irá embutir todas as propriedades do produto no interior do documento pedido. Como um paralelo, o NHibernate permite configurar a propriedade cascade para “save-update” e fazer com que a classe Produto seja salva em uma tabela exclusiva para o mesmo; mais tarde, se este produto for novamente comprado, então ela reutilizaria o mesmo registro. Abaixo o código que utilizamos para inserir o pedido e o JSON que foi armazenado na base:

var repositorioDePedidos = new RepositorioDePedidos(RavenDBContext.Store.OpenSession());

var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };
var pedido = new Pedido();

pedido.AdicionarItem(new Pedido.Item(produto, 2));

repositorioDePedidos.Salvar(pedido);
{
    "Data": "2017-01-17T16:59:58.2372992",
    "Total": 240,
    "Itens": [
        {
            "Produto": {
                "Descricao": "Mouse Microsoft",
                "Valor": 120,
                "Id": null
            },
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

O grande ponto negativo deste modelo, é que se precisarmos alterar qualquer informação no produto, teremos que varrer todos os pedidos existentes e fazer a tal alteração.

PorId

A opção por id faz com que a referência para o produto seja armazenada, ou seja, o item passa agora a referenciar o id do produto que foi comprado que, por sua vez, e acaba sendo armazenado em uma coleção a parte dos pedidos. Com isso, os dados do produto não serão duplicados, mas você terá que saltar para outro documento para ter acesso às informações sobre o produto que foi comprado. A classe Item sofre uma pequena alteração para armazenar apenas o Id do produto:

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.ProdutoId = produto.Id;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public string ProdutoId { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

Agora o produto deve ser armazenado separadamente em nossa base, afinal, é muito provável que ele seja um agregado na perspectiva do domínio, o que o confere direito de ter um repositório. Obviamente que antes de comprarmos algo, ele deve estar previamente cadastrado, e é esta instância deste produto já cadastrado que é passada para o item do pedido, que o reutilizará:

using (var session = RavenDBContext.Store.OpenSession())
{
    var repositorioDePedidos = new RepositorioDePedidos(session);
    var repositorioDeProdutos = new RepositorioDeProdutos(session);

    var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };

    repositorioDeProdutos.Salvar(produto);

    var pedido = new Pedido();

    pedido.AdicionarItem(new Pedido.Item(produto, 2));

    repositorioDePedidos.Salvar(pedido);
}
{
    "Data": "2017-01-17T17:11:49.7687856",
    "Total": 240,
    "Itens": [
        {
            "ProdutoId": "produtos/1",
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

Redundante

Por fim, esta opção nos permite criar uma classe e copiar somente os campos necessários do objeto de origem. A ideia é trazer para o item tudo o que é necessário para realizar a compra, e mesmo que no futuro as informações mudem, o pedido atual terá armazenado as informações originais do momento da compra. Quando, por exemplo, a descrição do produto for alterada, não mais refletirá nos pedidos já realizados.

Para isso, a classe Item passa a armazenar também a descrição do produto, além do valor que já vinha fazendo. Não há mais qualquer referência com a classe Produto.

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.Descricao = produto.Descricao;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public string Descricao { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

O código que armazena o pedido nada muda em relação ao que vimos no primeiro cenário, porém se inspecionarmos o JSON da base de dados, veremos que ele armazena apenas as propriedades da classe Item, sem qualquer menção ao Produto, muito menos ao seu Id.

using (var session = RavenDBContext.Store.OpenSession())
{
    var repositorioDePedidos = new RepositorioDePedidos(session);

    var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };
    var pedido = new Pedido();

    pedido.AdicionarItem(new Pedido.Item(produto, 2));

    repositorioDePedidos.Salvar(pedido);
}
{
    "Data": "2017-01-17T17:21:39.6605485",
    "Total": 240,
    "Itens": [
        {
            "Descricao": "Mouse Microsoft",
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

A utilização depende de cada situação que temos. Em muito casos quando trabalhamos com base de dados relacional, não nos atentamos para algumas situações, que podem gerar problemas mais sérios em termos de negócios, como por exemplo, não armazenar o nome ou o endereço do cliente no momento da realização da compra. No caso do RavenDB a redundância é natural, mas, como disse, nem sempre ela é ruim.

O código fonte dos exemplos utilizados no decorrer este artigo está disponível neste endereço.