Eu pensei em dar uma olhada em responder minha própria pergunta. O que segue é apenas uma maneira de resolver os problemas de 1 a 3 na minha pergunta original.
Isenção de responsabilidade: nem sempre posso usar os termos corretos ao descrever padrões ou técnicas. Desculpe por isso.
Os objetivos:
- Crie um exemplo completo de um controlador básico para visualização e edição
Users
.
- Todo o código deve ser totalmente testável e zombável.
- O controlador não deve ter idéia de onde os dados estão armazenados (o que significa que eles podem ser alterados).
- Exemplo para mostrar uma implementação SQL (mais comum).
- Para desempenho máximo, os controladores devem receber apenas os dados de que precisam - sem campos extras.
- A implementação deve alavancar algum tipo de mapeador de dados para facilitar o desenvolvimento.
- A implementação deve ter a capacidade de executar pesquisas de dados complexas.
A solução
Estou dividindo minha interação de armazenamento persistente (banco de dados) em duas categorias: R (Ler) e CUD (Criar, Atualizar, Excluir). Minha experiência foi que as leituras são realmente o que faz com que um aplicativo desacelere. E embora a manipulação de dados (CUD) seja realmente mais lenta, isso acontece com muito menos frequência e, portanto, é muito menos preocupante.
CUD (Criar, Atualizar, Excluir) é fácil. Isso envolverá o trabalho com modelos reais , que são passados ao meu Repositories
para persistência. Observe que meus repositórios ainda fornecerão um método Read, mas simplesmente para criação de objeto, não para exibição. Mais sobre isso mais tarde.
R (Ler) não é tão fácil. Nenhum modelo aqui, apenas objetos de valor . Use matrizes, se preferir . Esses objetos podem representar um único modelo ou uma mistura de muitos modelos, qualquer coisa realmente. Eles não são muito interessantes por si só, mas como são gerados. Estou usando o que estou chamando Query Objects
.
O código:
Modelo de Usuário
Vamos começar de maneira simples com nosso modelo básico de usuário. Observe que não há nenhum material de extensão ou banco de dados do ORM. Apenas pura glória do modelo. Adicione seus getters, setters, validação, o que for.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Interface do Repositório
Antes de criar meu repositório de usuários, desejo criar minha interface de repositório. Isso definirá o "contrato" que os repositórios devem seguir para serem usados pelo meu controlador. Lembre-se, meu controlador não saberá onde os dados estão realmente armazenados.
Observe que meus repositórios contêm apenas esses três métodos. O save()
método é responsável por criar e atualizar usuários, simplesmente dependendo de o objeto de usuário ter ou não um ID definido.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Implementação de Repositório SQL
Agora, para criar minha implementação da interface. Como mencionado, meu exemplo seria com um banco de dados SQL. Observe o uso de um mapeador de dados para evitar a necessidade de gravar consultas SQL repetitivas.
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
Interface de objeto de consulta
Agora, com o CUD (Criar, Atualizar, Excluir) atendido pelo nosso repositório, podemos nos concentrar no R (Ler). Objetos de consulta são simplesmente um encapsulamento de algum tipo de lógica de pesquisa de dados. Eles não são construtores de consultas. Abstraindo-o como nosso repositório, podemos alterar sua implementação e testá-lo mais facilmente. Um exemplo de um Objeto de Consulta pode ser um AllUsersQuery
ou AllActiveUsersQuery
, ou mesmo MostCommonUserFirstNames
.
Você pode estar pensando "não posso simplesmente criar métodos em meus repositórios para essas consultas?" Sim, mas eis por que não estou fazendo isso:
- Meus repositórios foram criados para trabalhar com objetos de modelo. Em um aplicativo do mundo real, por que eu precisaria entrar em
password
campo se estou procurando listar todos os meus usuários?
- Os repositórios geralmente são específicos do modelo, mas as consultas geralmente envolvem mais de um modelo. Então, em qual repositório você coloca seu método?
- Isso mantém meus repositórios muito simples - não uma classe de métodos inchada.
- Todas as consultas agora estão organizadas em suas próprias classes.
- Realmente, neste momento, os repositórios existem simplesmente para abstrair minha camada de banco de dados.
Para o meu exemplo, criarei um objeto de consulta para pesquisar "AllUsers". Aqui está a interface:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implementação de objeto de consulta
É aqui que podemos usar um mapeador de dados novamente para ajudar a acelerar o desenvolvimento. Observe que estou permitindo um ajuste no conjunto de dados retornado - os campos. Isso é tanto quanto eu quero ir com a manipulação da consulta realizada. Lembre-se de que meus objetos de consulta não são construtores de consultas. Eles simplesmente realizam uma consulta específica. No entanto, como sei que provavelmente usarei muito este, em várias situações diferentes, estou me dando a capacidade de especificar os campos. Eu nunca quero retornar campos que não preciso!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
Antes de passar para o controlador, quero mostrar outro exemplo para ilustrar o quão poderoso isso é. Talvez eu tenha um mecanismo de relatório e precise criar um relatório para AllOverdueAccounts
. Isso pode ser complicado com meu mapeador de dados e talvez eu queira escrever algo real SQL
nessa situação. Não tem problema, aqui está a aparência desse objeto de consulta:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
Isso mantém toda a minha lógica deste relatório em uma classe e é fácil de testar. Eu posso zombar do conteúdo do meu coração ou até mesmo usar uma implementação completamente diferente.
O controlador
Agora a parte divertida - reunir todas as peças. Observe que estou usando injeção de dependência. Normalmente, as dependências são injetadas no construtor, mas eu realmente prefiro injetá-las diretamente nos meus métodos de controlador (rotas). Isso minimiza o gráfico de objetos do controlador e, na verdade, acho mais legível. Observe que, se você não gosta dessa abordagem, basta usar o método tradicional do construtor.
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
Pensamentos finais:
O importante a ser observado aqui é que, quando estou modificando (criando, atualizando ou excluindo) entidades, estou trabalhando com objetos de modelo reais e realizando a persistência nos meus repositórios.
No entanto, quando estou exibindo (selecionando dados e enviando-os para as visualizações), não estou trabalhando com objetos de modelo, mas com objetos de valor antigos simples. Eu seleciono apenas os campos necessários e ele foi projetado para maximizar o desempenho da minha pesquisa de dados.
Meus repositórios permanecem muito limpos e, em vez disso, essa "bagunça" é organizada em minhas consultas de modelo.
Eu uso um mapeador de dados para ajudar no desenvolvimento, pois é ridículo escrever SQL repetitivo para tarefas comuns. No entanto, você pode escrever SQL sempre que necessário (consultas complicadas, relatórios, etc.). E quando você faz isso, está bem escondido em uma classe devidamente nomeada.
Eu adoraria ouvir sua opinião sobre a minha abordagem!
Atualização de julho de 2015:
Me perguntaram nos comentários onde acabei com tudo isso. Bem, na verdade não tão longe assim. Na verdade, ainda não gosto de repositórios. Acho-os um exagero para pesquisas básicas (especialmente se você já estiver usando um ORM) e confuso ao trabalhar com consultas mais complicadas.
Geralmente trabalho com um ORM estilo ActiveRecord, portanto, na maioria das vezes, apenas referencio esses modelos diretamente em todo o meu aplicativo. No entanto, nas situações em que tenho consultas mais complexas, usarei objetos de consulta para torná-las mais reutilizáveis. Também devo observar que sempre injeto meus modelos nos meus métodos, facilitando a zombaria nos meus testes.
new Query\ComplexUserLookup($username, $anotherCondition)
. Ou, faça isso através dos métodos setter$query->setUsername($username);
. Você pode realmente projetar isso, no entanto, faz sentido para seu aplicativo em particular e acho que os objetos de consulta deixam muita flexibilidade aqui.Com base na minha experiência, aqui estão algumas respostas para suas perguntas:
P: Como lidamos com a recuperação de campos de que não precisamos?
R: Da minha experiência, isso realmente se resume a lidar com entidades completas versus consultas ad-hoc.
Uma entidade completa é algo como um
User
objeto. Possui propriedades e métodos, etc. É um cidadão de primeira classe na sua base de código.Uma consulta ad-hoc retorna alguns dados, mas não sabemos nada além disso. À medida que os dados são passados pelo aplicativo, isso é feito sem contexto. É um
User
? AUser
com algumasOrder
informações anexadas? Nós realmente não sabemos.Eu prefiro trabalhar com entidades completas.
Você está certo ao retornar com frequência dados que não usará, mas pode resolver isso de várias maneiras:
User
para o back-end e talvez umUserSmall
para chamadas AJAX. Um pode ter 10 propriedades e um tem 3 propriedades.As desvantagens de trabalhar com consultas ad-hoc:
User
, você terminará escrevendo essencialmente o mesmoselect *
para muitas chamadas. Uma chamada recebe 8 de 10 campos, uma recebe 5 de 10, uma recebe 7 de 10. Por que não substituir todas por uma chamada que recebe 10 de 10? A razão pela qual isso é ruim é que é crime re-fatorar / testar / zombar.User
tão lentas?" você acaba rastreando consultas pontuais e, portanto, as correções de bugs tendem a ser pequenas e localizadas.P: Terei muitos métodos no meu repositório.
R: Realmente não vi outra maneira de consolidar as chamadas. O método que chama no seu repositório realmente mapeia os recursos em seu aplicativo. Quanto mais recursos, mais chamadas específicas de dados. Você pode refazer os recursos e tentar mesclar chamadas semelhantes em uma.
A complexidade no final do dia tem que existir em algum lugar. Com um padrão de repositório, nós o inserimos na interface do repositório, em vez de talvez fazer vários procedimentos armazenados.
Às vezes eu tenho que dizer a mim mesmo: "Bem, tinha que dar em algum lugar! Não há balas de prata".
fonte
SELECT *
seleciona apenas os campos necessários. Por exemplo, veja esta pergunta . Quanto a todas as consultas ad-hock de que você fala, certamente entendo de onde você é. No momento, tenho um aplicativo muito grande que possui muitos deles. Esse foi o meu "Bem, tinha que dar em algum lugar!" momento, optei pelo desempenho máximo. No entanto, agora estou lidando com MUITAS consultas diferentes.reads
geralmente ocorrem problemas de desempenho, você pode usar uma abordagem de consulta mais personalizada para eles, que não se traduz em objetos de negócios reais. Então, paracreate
,update
edelete
, usar um ORM, que trabalha com objetos inteiros. Alguma opinião sobre essa abordagem?Eu uso as seguintes interfaces:
Repository
- carrega, insere, atualiza e exclui entidadesSelector
- localiza entidades baseadas em filtros, em um repositórioFilter
- encapsula a lógica de filtragemMy
Repository
é independente de banco de dados; de fato, não especifica nenhuma persistência; poderia ser qualquer coisa: banco de dados SQL, arquivos xml, serviço remoto, um alienígena do espaço exterior, etc. Para pesquisar capacidades, asRepository
constrói umSelector
que pode ser filtrada,LIMIT
-ed, classificadas e contadas. No final, o seletor busca um ou maisEntities
da persistência.Aqui está um exemplo de código:
Então, uma implementação:
A idéia é que o genérico
Selector
use,Filter
mas a implementaçãoSqlSelector
useSqlFilter
; oSqlSelectorFilterAdapter
adapta um genéricoFilter
a um concretoSqlFilter
.O código do cliente cria
Filter
objetos (que são filtros genéricos), mas na implementação concreta do seletor, esses filtros são transformados em filtros SQL.Outras implementações de seletor, como
InMemorySelector
, transformam deFilter
paraInMemoryFilter
usar seu específicoInMemorySelectorFilterAdapter
; portanto, toda implementação de seletor vem com seu próprio adaptador de filtro.Usando essa estratégia, meu código de cliente (na camada de negócios) não se importa com uma implementação específica de repositório ou seletor.
PS Esta é uma simplificação do meu código real
fonte
Vou acrescentar um pouco sobre isso, pois atualmente estou tentando entender tudo isso sozinho.
# 1 e 2
Este é o local perfeito para o seu ORM fazer o trabalho pesado. Se você estiver usando um modelo que implementa algum tipo de ORM, basta usar seus métodos para cuidar dessas coisas. Faça suas próprias funções orderBy que implementam os métodos Eloquent, se necessário. Usando o Eloquent, por exemplo:
O que você parece estar procurando é um ORM. Não há razão para o seu Repositório não poder se basear em um. Isso exigiria que o Usuário fosse eloquente, mas pessoalmente não vejo isso como um problema.
Se, no entanto, você deseja evitar um ORM, precisará "rodar o seu próprio" para obter o que procura.
# 3
As interfaces não devem ser requisitos rígidos e rápidos. Algo pode implementar uma interface e adicionar a ela. O que ele não pode fazer é deixar de implementar uma função necessária dessa interface. Você também pode estender interfaces como classes para manter as coisas secas.
Dito isto, estou apenas começando a entender, mas essas realizações me ajudaram.
fonte
Só posso comentar sobre como lidamos com isso (na minha empresa). Antes de tudo, o desempenho não é um problema muito grande para nós, mas ter um código limpo / adequado é.
Antes de tudo, definimos modelos como um
UserModel
que usa um ORM para criarUserEntity
objetos. Quando aUserEntity
é carregado de um modelo, todos os campos são carregados. Para campos que referenciam entidades estrangeiras, usamos o modelo estrangeiro apropriado para criar as respectivas entidades. Para essas entidades, os dados serão carregados ondemand. Agora sua reação inicial pode ser ... ??? ... !!! deixe-me dar um exemplo um pouco de exemplo:No nosso caso,
$db
é um ORM capaz de carregar entidades. O modelo instrui o ORM a carregar um conjunto de entidades de um tipo específico. O ORM contém um mapeamento e o usa para injetar todos os campos dessa entidade na entidade. Para campos estrangeiros, no entanto, apenas os IDs desses objetos são carregados. Nesse caso,OrderModel
criaOrderEntity
s com apenas os IDs dos pedidos referenciados. QuandoPersistentEntity::getField
é chamado pelaOrderEntity
entidade, a entidade instrui seu modelo para carregar preguiçosamente todos os campos nosOrderEntity
s. Todos osOrderEntity
s associados a uma UserEntity são tratados como um conjunto de resultados e serão carregados de uma só vez.A mágica aqui é que nosso modelo e ORM injetam todos os dados nas entidades e essas entidades apenas fornecem funções de invólucro para o
getField
método genérico fornecido porPersistentEntity
. Para resumir, sempre carregamos todos os campos, mas os campos que fazem referência a uma entidade estrangeira são carregados quando necessário. Carregar vários campos não é realmente um problema de desempenho. Carregar todas as entidades estrangeiras possíveis, no entanto, seria uma enorme redução de desempenho.Agora, vamos carregar um conjunto específico de usuários, com base na cláusula where. Fornecemos um pacote de classes orientado a objetos que permite especificar expressões simples que podem ser coladas. No código de exemplo, eu o nomeei
GetOptions
. É um invólucro para todas as opções possíveis para uma consulta selecionada. Ele contém uma coleção de cláusulas where, um grupo por cláusula e tudo mais. Nossas cláusulas where são bastante complicadas, mas você pode obviamente criar uma versão mais simples com facilidade.Uma versão mais simples desse sistema seria passar a parte WHERE da consulta como uma string diretamente para o modelo.
Sinto muito por esta resposta bastante complicada. Tentei resumir nossa estrutura o mais rápido e claro possível. Se você tiver outras perguntas, não hesite em perguntar e atualizarei minha resposta.
EDIT: Além disso, se você realmente não deseja carregar alguns campos imediatamente, você pode especificar uma opção de carregamento lento no seu mapeamento ORM. Como todos os campos são eventualmente carregados pelo
getField
método, você pode carregar alguns campos no último minuto quando esse método é chamado. Este não é um problema muito grande em PHP, mas eu não recomendaria para outros sistemas.fonte
Estas são algumas soluções diferentes que eu já vi. Existem prós e contras em cada um deles, mas cabe a você decidir.
Problema nº 1: muitos campos
Esse é um aspecto importante, especialmente quando você considera as verificações somente de índice . Eu vejo duas soluções para lidar com esse problema. Você pode atualizar suas funções para obter um parâmetro de matriz opcional que conteria uma lista de colunas para retornar. Se esse parâmetro estiver vazio, você retornará todas as colunas na consulta. Isso pode ser um pouco estranho; com base no parâmetro, você pode recuperar um objeto ou uma matriz. Você também pode duplicar todas as suas funções para ter duas funções distintas que executam a mesma consulta, mas uma retorna uma matriz de colunas e a outra retorna um objeto.
Problema nº 2: muitos métodos
Trabalhei brevemente com a Propel ORM há um ano e isso se baseia no que me lembro dessa experiência. O Propel tem a opção de gerar sua estrutura de classes com base no esquema de banco de dados existente. Ele cria dois objetos para cada tabela. O primeiro objeto é uma longa lista de funções de acesso semelhante à que você listou atualmente;
findByAttribute($attribute_value)
. O próximo objeto herda deste primeiro objeto. Você pode atualizar esse objeto filho para criar suas funções getter mais complexas.Outra solução seria usar
__call()
para mapear funções não definidas para algo acionável. Seu__call
método seria capaz de analisar o findById e findByName em consultas diferentes.Espero que isso ajude pelo menos alguns o quê.
fonte
Sugiro https://packagist.org/packages/prettus/l5-repository como fornecedor para implementar Repositórios / Critérios etc ... no Laravel5: D
fonte
Concordo com @ ryan1234 que você deve passar objetos completos dentro do código e usar métodos de consulta genéricos para obter esses objetos.
Para uso externo / de ponto final, gosto muito do método GraphQL.
fonte
Meu instinto me diz que isso talvez exija uma interface que implemente métodos otimizados para consultas juntamente com métodos genéricos. As consultas sensíveis ao desempenho devem ter métodos direcionados, enquanto as consultas pouco frequentes ou leves são tratadas por um manipulador genérico, talvez a despesa do controlador fazendo um pouco mais de malabarismo.
Os métodos genéricos permitiriam que qualquer consulta fosse implementada e, assim, evitariam interromper as alterações durante um período de transição. Os métodos direcionados permitem otimizar uma chamada quando faz sentido e podem ser aplicados a vários provedores de serviços.
Essa abordagem seria semelhante às implementações de hardware que executam tarefas otimizadas específicas, enquanto as implementações de software fazem o trabalho leve ou a implementação flexível.
fonte
Eu acho que o graphQL é um bom candidato para fornecer uma linguagem de consulta em larga escala sem aumentar a complexidade dos repositórios de dados.
No entanto, existe outra solução, se você não quiser usar o graphQL por enquanto. Usando um DTO em que um objeto é usado para transportar os dados entre processos, neste caso entre o serviço / controlador e o repositório.
Uma resposta elegante já foi fornecida acima, no entanto, tentarei dar outro exemplo que acho mais simples e poderia servir como ponto de partida para um novo projeto.
Como mostrado no código, precisaríamos de apenas 4 métodos para operações CRUD. o
find
método seria usado para listar e ler passando o argumento do objeto. Os serviços de back-end podem criar o objeto de consulta definido com base em uma string de consulta de URL ou com base em parâmetros específicos.O objeto de consulta (
SomeQueryDto
) também pode implementar uma interface específica, se necessário. e é fácil de ser estendido posteriormente sem adicionar complexidade.Exemplo de uso:
fonte