Leo

Sobre tecnologia...

Laravel & MongoDB na Convenia

5 months ago · 7 MIN READ
#PHP  #Laravel  #MongoDB 

Olá pessoal nesse post vou falar um pouco de como utilizamos MongoDB com o Laravel na Convenia e quais vantagens que ele nos trouxe com documentos embedados.

Como desenvolvedores estamos muito familiarizados com o modelo Relacional, por isso é comum cometermos o erro de modelar qualquer tipo de banco da mesma forma que modelamos um relacional. Para enfatizar a diferença na modelagem vamos tentar imaginar uma modelagem relacional para a tela mostrada na imagem seguinte, em seguida vamos tentar reproduzir a mesma modelagem com o MongoDB:

employee-screen.png

Na imagem acima temos 2 seções, o card a esquerda contendo informações gerais do colaborador e o card na direita contendo "Endereço", de uma forma mais clássica modelariamos isso em 2 tabelas, sempre tendo em mente evitar qualquer tipo de duplicidade de informação:

mysql-modelagem.png

Do lado da aplicação precisaremos de 2 models para "dar vida" a essas 2 tabelas, a model do Laravel é uma implementação de active record, nesse padrão geralmente mapeamos uma tabela por model, essa model fará o papel da nossa entidade e também tem a responsabilidade de acessar o banco de dados.

//app/Models/Colaborador.php

class Colaborador extends Model
{
    use HasFactory;

    public function enderecos()
    {
        return $this->hasMany(
            Endereco::class,
            'id_colaborador',
            'id'
        );
    }
}
//app/Models/Endereco.php

class Endereco extends Model
{
    use HasFactory;
}

No nosso caso a Model principal é a model de Colaborador, ela mantém uma ligação com a model Endereco através do método enderecos que por si é um relacionamento HasMany (Colaborador possui vários Endereços), sempre que quisermos trazer o colaborador junto com o endereço temos 2 opções:

Podemos fazer um Lazy Loading:

Colaborador::find(1)
        ->enderecos
        ->first()
        ->cep;

O código acima resultará em 2 consultas: A primeira trazendo o colaborador e a segunda trazendo o endereço, a segunda por sua vez é executada no momento da chamada do relacionamento enderecos (representado pelo ->enderecos). A estrutura da consulta ficará da seguinte forma:

select
  *
from
  "colaboradors"
where
  "colaboradors"."id" = 1
limit
  1
select
  *
from
  "enderecos"
where
  "enderecos"."id_colaborador" = 1
  and "enderecos"."id_colaborador" is not null

Como você pode ver 2 consultas não é grande problema, mas se tivessemos uma coleçao de colaboradores teriamos o famoso problema de N+1 consultas, então com 10 colaboradores teriámos uma consulta para trazer os colaboradores e em seguida 10 consultas, para trazer os endereços de cada colaborador um a um, para resolver esse problema podemos fazer um Eager loading:

Colaborador::with('enderecos')->get();

Agora estamos trazendo todos os colaboradores com endereço, a chamada ao método with instrui o ORM a trazer os endereços de todos os colaboradores em uma única consulta:

select
  *
from
  "colaboradors"
select
  *
from
  "enderecos"
where
  "enderecos"."id_colaborador" in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

Perceba que o resultado da consulta de colaboradores é utilizado na consulta de endereços, e o próprio Eloquent sabe como combinar os dados de endereço com os dados de colaboradores por baixo dos panos.

Com a explicação acima podemos chegar a conclusão de que a consulta acima não é tão problemática mas vamos pensar que o colaborador na vida real tem muito mais relacionamentos, apenas como exercício tente estruturar os relacionamentos seguintes na sua cabeça: absences, contactInformation, dependents, documents, perceba que em um cenário real o número de queries separadas que precisamos fazer pode nos trazer um problema de performance, esse exemplo é um exemplo real, é um pedacinho da nossa entidade de colaborador, não seria exagero falar em dezenas de relacionamentos.

Agora que já vimos um exemplo de modelagem no MySQL (relacional), como ficaria esse mesmo exemplo no MongoDB e o quais seriam as vantagens?

O MongoDB é um banco de dados baseado em documentos, descrevemos o que é armazenado no formato JSON, e diferente do MySql ele tem um schema flexível, isso significa que você pode mudar a estrutura do documento de acordo com a necessidade, sem se preocupar em criar migrações.

O pacote mais difundido para se trabalhar com o mongo no Laravel é o Laravel MongoDB, ele "imita" o eloquent garantindo a mesma interface, e nos traz bastantes possibilidades, veja como ficaria a mesma modelagem em uma única coleção no mongo:

> db.colaboradors.find().pretty()
{
    "_id" : ObjectId("604c12d75c9f4250b731d662"),
    "name" : "Miss Amelia Schuppe",
    "cargo" : "in",
    "departamento" : "aut",
    "salario" : 1000,
    "data_admissao" : "2021-03-13",
    "endereco" : {
        "cep" : "15014140",
        "endereco" : "125 Hammes Extensions\nSouth Bernadinebury, OR 51480",
        "bairro" : "East Alf",
        "cidade" : "West Tremayne",
        "uf" : "Wisconsin",
        "numero" : 200
    },
    "updated_at" : ISODate("2021-03-13T01:18:15.747Z"),
    "created_at" : ISODate("2021-03-13T01:18:15.747Z")
}

O MongoDB nos apresenta um paradigma de modelagem diferente, em um banco de dados relacional nos baseamos nas entidades em si e na suas relações, buscamos uma maior normalização e evitamos duplicidade, ao modelar as coleções no MongoDB fazemos a modelagem baseado na consulta, podemos inclusive ter 2 coleções diferentes de colaboradores para consultas diferentes, claro que isso nos traz alguns desafios na escrita pois teremos que manter ambas as coleções atualizadas, mas ganhamos muita performance na leitura, não é errado inclusive pensarmos em manter o mesmo dado duplicado no mesmo documento porém em formatos diferentes, uma versão "crua" do dado e outra versão formatada pronta para exibição.

No documento modelado acima temos o endereço embedado na entidade colaborador, quando consultarmos o colaborador, com apenas uma consulta, recebemos o seu endereço "de graça", imagine que no exemplo real da Convenia onde temos uma série de relacionamentos do colaborador, apenas uma consulta basta para trazer o colaborador por completo, isso traz um ganho de performance bem interessante!

O pacote nos traz um relacionamento especial para tratar documentos embedados, o embeds many:

class Colaborador extends Model
{
    use HasFactory;

    protected $connection = 'mongodb';

    public function enderecos()
    {
        return $this->embedsMany(
            Endereco::class,
            'id',
            'id_colaborador'
        );
    }
}

Isso permite que você mantenha a mesma modelagem de Entidades no lado da aplicação, tendo uma model específica para o endereço, na hora de armazenar o ORM vai entender que ele deve embedar (fazer o upsert do registro) o endereço dentro do documento de colaborador.

Você pode estar se perguntando "mas e se eu quiser compartilhar o endereço com mais de um colaborador?", nesse caso tenha em mente que os tipos de relacionamentos convencionais (hasMany, hasOne, belongsTo), ainda estão disponíveis, nesse caso a modelagem fica muito parecida com o a modelagem do MySql, existem casos em que isso é bem vindo:

> db.colaboradors.find().pretty()
{
    "_id" : ObjectId("604c1572e8a76d528004f023"),
    "name" : "Jaycee Cole",
    "cargo" : "deleniti",
    "departamento" : "optio",
    "salario" : 1000,
    "data_admissao" : "2021-03-13",
    "endereco" : "604c1572e8a76d528004f022",
    "updated_at" : ISODate("2021-03-13T01:29:22.757Z"),
    "created_at" : ISODate("2021-03-13T01:29:22.757Z")
}
> db.enderecos.find().pretty()
{
    "_id" : ObjectId("604c1572e8a76d528004f022"),
    "cep" : "15014140",
    "endereco" : "36740 Jenkins Mountain Suite 890\nPort Rubie, UT 17199",
    "bairro" : "Lennashire",
    "cidade" : "Isaiahbury",
    "uf" : "California",
    "numero" : 200,
    "updated_at" : ISODate("2021-03-13T01:29:22.736Z"),
    "created_at" : ISODate("2021-03-13T01:29:22.736Z")
}

Como mostrado acima, peceba que é possível reproduzir no MongoDB qualquer tipo de estrutura que fariamos em um banco relacionar, porém precisamos ter em mente que o MongoDB não tem a operação de join, a operação similar no MongoDB seria o lookup mas utilizando o "Laravel MongoDB" presisamos fazer uma query raw, que fica bem desajeitada apesar de cumprir perfeitamente o seu papel.

Muito bem, mas agora como eu decido qual tipo de relacionamento devo utilizar?

O protagonismo da Entidade

Bem não existe uma receita perfeita para decidir o que devemos embedar ou não, mas temos sinais que devemos avaliar para tomar essa decisão, todos esses sinais vão determinar o protagonismo da entidade no seu sistema, vamos avaliar a entidade de endereço como exemplo.

  1. Precisamos compartilhar aquele endereço entre vários colaboradores? Caso não precise compartilhar o endereço, é um bom sinal de que o correto seria embedar esse dado, se precisarmos compartilhar o endereço pode ser mais inteligente definir uma coleção de endereços pois evita o esforço de atualizar vários colaboradores ao atualizar um único endereço.

  2. Onde precisamos exibir o endereço, sempre presicamos exibir a entidade no qual ele pertence(colaborador)? Aqui você pode argumentar que estamos deixando o layout guiar a modelagem, mas de fato o layout diz muito sobre a importância de uma certa Entidade no sistema, se o endereço sempre é exibido junto com o colaborador devemos suspeitar de que o endereço não tem o protagonismo necessário para compor sua própria coleção.

  3. As rotas(REST) em que o endereço aparecem, são rotas do próprio endereço ou são rotas do colaborador? Se a informação do endereço forem exibidas somente em rotas do colaborador ou em uma rota filha do colaborador(nested resource) então devemos entender isso como um sinal de que o endereço deve ser embedado no colaborador.

  4. Existem muitos procedimentos que levam em consideração apenas o endereço? Vamos entender a busca como um procedimento, em geral não é comum buscarmos colaboradores por endereço, apenas por dados como nome, departamento, cargo, isso também sinaliza o baixo protagonismo da model de Endereço.

  5. Existe muito acesso ao colaborador apenas para ver o Endereço? É possível que todas as afirmações acima te levem a entender que você deve embedar o endereço porém é possível que as pessoas utilizem muito o endereço por algum motivo(envio de correspondência talvez), nesse caso pode ser necessário dar ao endereço sua própria coleção, isso vai impedir as leituras de endereço de concorrerem com as leituras de colaborador, claro que para isso funcionar a tela de endereço deve ser remodelada para não exibir outros dados, óbvio também que o designer do time não vai gostar disso né, mas aí é ver se vale ou não a pena.

Se você confrontar suas entidades com a lista acima vai ver que na maioria dos casos as entidades que devem ser embedadas são o que chamamos de "entidades fracas", são entidades que não tem o porquê de existir sem uma "entidade forte" na qual ela pertence.

Bancos relacionais e campos json

Quase todos os bancos relacionais modernos trazem algum tipo de solução de schema flexível, eu estaria sendo muito parcial se não comentasse sobre isso, o MySQL por exemplo, tem campos json que nos permitem os mesmos resultados citados acima, bem como a mesma análise de "protagonismo da entidade", isso conversaria muito bem com o Laravel e seu recurso de custom casts, sem dúvida você deve levar essa possibilidade em consideração, no nosso caso acabamos fazendo a opção pelo MongoDB por outras vantagens como o aggregation pipeline, que é uma ferramenta muito poderosa para filtrar e transformar os resultados, feature essa que necessita de um único post somente para ela.

Conclusão

O MongoDB é uma ferramenta muito completa e com certeza tem muito a oferecer, no entanto a maioria das aplicações são perfeitamente atendidas com o bom e velho banco relacional, se esse for o seu caso sugiro que você faça a opção pelo conhecido, pois toda nova tecnologia traz uma curva de aprendizado bem como os seus próprios desafios, com o MongoDB não é diferente, pretendo escrever um posto mostrando todos os desafios que passamos com ele, e não foram poucos :)

Espero ter contrinuído de alguma forma, até a próxima!

···

Leonardo Lemos


comments powered by Disqus


Proudly powered by Canvas · Sign In