laravelbrasil / forum

Ama Laravel? Torne se um Jedi e Ajude outros Padawans
GNU General Public License v3.0
251 stars 13 forks source link

Busca com chave de outra Model #39

Closed rogerio-pereira closed 7 years ago

rogerio-pereira commented 7 years ago

Pessoal meu problema é o seguinte:

Tenho 3 Models Agenda Prontuário Pacientes

Agenda é a tabela principal, as outras duas tem um relacionamento OneToOne cada.

Quero buscar todos os Prontuários que sejam do Paciente 1, mas não quero que a busca parta de agenda...

Qual seria o melhor modo de fazer isso?

fabiosperotto commented 7 years ago

Como foi definida a modelagem no geral (além da Agenda)? Vou supor que a Agenda tenha FK de Prontuário que por sua vez tenha FK de Paciente, poderia ter algumas associações nas Models como:

Ou não?

rogerio-pereira commented 7 years ago

Pensei nisso, só nao sei se seria a melhor abordagem, em questão de desempenho do codigo, por exemplo o sistema sendo usado por varios usuários ao mesmo tempo ou se seria melhor usar um join

fabiosperotto commented 7 years ago

Verdade! Na correria nem me liguei nisso. Acredito sim que para ser mais performático neste caso iria de rawl sql do que com Eloquent. Faço isso em algumas consultas, então, tem projetos com Eloquent e raw sql. Usar Eloquent para ganharmos em leitura, manutenção de código e segurança. DB::raw() quando a prioridade é performance. Poderia quem sabe gerar uns dados nestas tabelas com [1] criar umas funções de teste usando [2] e testar várias consultas pra ver qual o melhor tempo. Eloquent não me deixou na mão até hoje, entretanto os projetos que participei são pequenos a médios (em time requests).

Aguardar ver se alguém possa contribuir aqui. Baita issue! =D

Refs: [1] https://github.com/fzaninotto/Faker [2] https://laravel.com/docs/5.4/database#listening-for-query-events

paulofreitas commented 7 years ago

Quero buscar todos os Prontuários que sejam do Paciente 1

Independente da estrutura que foi modelada, esse requisito por si só implica em um relacionamento 1:n entre Paciente e Prontuário. Um paciente tem vários prontuários.

Logo, Paciente deve ter um relacionamento prontuarios() do tipo hasMany e Prontuario deve ter um relacionamento paciente() do tipo belongsTo.

Agora voltemos ao requisito. Comecemos traduzindo a consulta de maneira literal:

$prontuarios = Prontuario::whereIdPaciente($id_paciente)->get();

Uma única consulta, um simples select com where. Morreu aqui.

Ocorre que, na maioria dos casos, ao listar todos os prontuários de um dado paciente você vai acabar tendo que referenciar quem é o paciente cujos prontuários estão sendo apresentados, isto é, vai acabar tendo de imprimir o nome do paciente na tela.

Da forma atual, se tu tentar acessar qualquer informação do relacionamento do paciente, tu estará executando uma nova consulta para cada um deles:

foreach ($prontuarios as $prontuario) {
    print $prontuario->paciente->nome; // Um novo SELECT implícito na tabela de pacientes
}

Este é um problema comum. Por padrão, no Eloquent todos os relacionamentos são lazy loaded: você só vai carregar a informação quando isso for necessário. Isso implica no custo de consultas N + 1. Se houverem 10 prontuários a serem listados, você terá 10 + 1 consultas, totalizando 11.

Isso, de fato, está longe do ideal. Para contornar isso, o Eloquent provê o recurso de eager-loading. O que o eager-loading faz é já preparar a consulta do relacionamento que será acessado, no caso os dados dos pacientes. Ficando assim:

$prontuarios = Prontuario::with('paciente')->whereIdPaciente($id_paciente)->get();

Internamente, o Laravel reduzirá o custo total para 2 consultas: um select com where na tabela de prontuários e outro na tabela de pacientes.

Note, no entanto, que nestes casos o ideal é que a consulta seja feita de forma inversa:

$paciente = Paciente::with('prontuarios')->find($id_paciente)->get();

print $paciente->nome;

foreach ($paciente->prontuarios as $prontuario) {
    print $prontuario->observacoes;
}

Como dito antes, você vai precisar imprimir na tela qualquer informação do paciente antes de começar a listar os prontuários dele, algo como "Listando prontuários do paciente X". A informação do paciente é mais importante, até porque ele pode não ter nenhum prontuário.

Esse último ponto evidencia uma questão importante: vai precisar de ao menos 2 consultas de qualquer forma.

Ah, mas usando JOINs eu poderia usar apenas uma!

Sim, mas não que isso vá te trazer algum ganho real. Antes de aprofundar, permita-me construir a consulta:

// Query Builder puro
$prontuarios = DB::table('prontuarios')
    ->rightJoin('pacientes', 'prontuarios.id_paciente', '=', 'pacientes.id')
    ->whereIdPaciente($id_paciente)
    ->get();

// Eloquent + Query Builder
$prontuarios = Prontuario::rightJoin('pacientes', 'prontuarios.paciente.id', '=', 'pacientes.id')
    ->whereIdPaciente($id_paciente)
    ->get();

Lembra da situação onde um paciente pode não ter nenhum prontuário? Um INNER JOIN não funcionaria aqui. Precisamos do RIGHT JOIN.

Voltemos ao ponto onde você precisa imprimir o nome do paciente na tela antes de listar os prontuários.

Como você iria fazer isso? Mesmo que não exista nenhum prontuário para o paciente, você pode selecionar o nome dele no SELECT e imprimir algo como $prontuarios[0]->nome. Pois é, a legibilidade do código foi pro espaço. Nome de quem? Você provavelmente vai precisar usar um alias e montar um nome_paciente.

Talvez seja o caso de inverter a relação novamente? Tentemos. Desta vez você vai precisar do LEFT JOIN:

// Query Builder puro
$prontuarios = DB::table('pacientes')
    ->leftJoin('prontuarios', 'pacientes.id', '=', 'prontuarios.id_paciente')
    ->find($id_paciente)
    ->get();

// Eloquent + Query Builder
$prontuarios = Paciente::leftJoin('prontuarios', 'pacientes.id', '=', 'prontuarios.id_paciente')
    ->find($id_paciente)
    ->get();

É, não dá, você vai precisar imprimir $prontuarios[0]->nome_paciente de novo. Você cortou uma consulta mas em contrapartida tornou o código menos legível, um monstrinho difícil de dar manutenção.

Mesmo usando SQL puro, o código ficaria mais legível usando 2 consultas também:

$paciente = DB::table('pacientes')
    ->find($id_paciente)
    ->get();

print $paciente->nome;

$prontuarios = DB::table('prontuarios')
    ->whereIdPaciente($id_paciente)
    ->get();

foreach ($prontuarios as $prontuario) {
    print $prontuario->observacoes;
}

Chegamos ao ponto onde não faz sentido não usar o Eloquent. Note: isso se repete na grande maioria dos casos.

Otimização prematura é a raiz de todos os males. – Donald Knuth

Esta é uma verdade incontestável. Para o usuário final, não vai fazer diferença nenhuma ter 1 consulta a mais - o tempo de execução será ínfimo em ambos os casos.

Uma requisição não é suposta a executar o menor número de consultas possíveis. Ela é suposta a executar o menor número de consultas possíveis levando em conta como a informação retornada será consumida. Nos casos demonstrados, é melhor ter 2 consultas do que apenas 1 em qualquer cenário. :+1:

Porque performance não é sobre executar menos consultas. Performance é sobre modelar o banco adequadamente, ter índices onde eles se fizerem necessários, usar o mecanismo de caching onde for possível, e por aí vai.

A otimização prematura muitas vezes vai te trazer mais problemas (de manutenção do código principalmente) do que efetivamente atacar o problema que ela supõe resolver.

Tirando casos bem específicos de geração de relatórios, você irá querer usar o Eloquent na maior parte do tempo. E ele será otimizado o bastante se você usá-lo adequadamente. Não será um problema ter 10-20 consultas numa página se isso se traduzir em milésimos de segundo. :wink:

Você só deve se preocupar com performance se as consultas efetivamente estiverem levando um tempo inaceitável para trazer os resultados. Se o usuário tiver que esperar mais do que alguns segundos, aí sim seria o caso de se analisar melhor a situação.

E isso muitas vezes é resolvido sem mudar as consultas. É a questão de aplicar índices corretamente, usar caching onde for o caso e etc. Claro que, em alguns casos, também é o caso de diminuir o número de consultas, como é o caso de uso do eager-loading do Eloquent.

Surgiu uma dúvida recentemente por aqui que demonstra ainda melhor porque o Eloquent deveria ser usado sempre que possível: https://github.com/laravelbrasil/forum/issues/35 Acompanhe o caso e como ele pode ser resolvido com o Eloquent. Note a diferença de legibilidade de código entre um e outro e como isso afetaria a manutenção por outra pessoa. A diferença é absurda. Eu nem cheguei a propor outra solução sem o Eloquent porque iria ficar ainda maior que as consultas separadas. 😃

Perceba que os super-poderes do Eloquent vão muito além do eager-loading. Métodos como o has(), whereHas(), whereDoesntHave(), withCount() e companhia simplificam consultas de maneira extraordinária. Muito da popularidade do Laravel se dá pela extraordinariedade do Eloquent: ele é realmente eloquente. 😄

Dito isso tudo, minha recomendação no geral é: só use consultas puras através do Query Builder somente quando for realmente necessário. Porque na maioria das vezes, não é.

Espero que isso te ajude a entender ainda melhor quando pensar em otimização e quando faz sentido usar o Eloquent ou o Query Builder. 😉

cagartner commented 7 years ago

@rogerio-pereira estou fechando essa issue devido a completa explicação do @paulofreitas do problema.