Estratégias para evitar SQL nos seus controladores ... ou quantos métodos devo ter nos meus modelos?

17

Portanto, uma situação em que me deparo razoavelmente é aquela em que meus modelos começam:

  • Torne-se monstros com toneladas e toneladas de métodos

OU

  • Permite passar partes do SQL para eles, para que sejam flexíveis o suficiente para não exigir um milhão de métodos diferentes

Por exemplo, digamos que temos um modelo "widget". Começamos com alguns métodos básicos:

  • get ($ id)
  • insert ($ record)
  • atualização ($ id, $ record)
  • delete ($ id)
  • getList () // obtém uma lista de Widgets

Tudo bem e elegante, mas precisamos de alguns relatórios:

  • listCreatedBetween ($ start_date, $ end_date)
  • listPurchasedBetween ($ start_date, $ end_date)
  • listOfPending ()

E então os relatórios começam a ficar complexos:

  • listPendingCreatedBetween ($ start_date, $ end_date)
  • listForCustomer ($ customer_id)
  • listPendingCreatedBetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Você pode ver onde isso está crescendo ... eventualmente, temos tantos requisitos de consulta específicos que eu preciso implementar toneladas e toneladas de métodos ou algum tipo de objeto "query" que eu posso passar para uma única -> query (query $ query) método ...

... ou apenas morda a bala e comece a fazer algo assim:

  • list = MyModel-> query ("data_de_início> X AND data_de final <Y AND pendente = 1 AND customer_id = Z")

Há um certo apelo para ter apenas um método como esse, em vez de 50 milhões de outros métodos mais específicos ... mas às vezes parece "errado" colocar uma pilha do que é basicamente SQL no controlador.

Existe uma maneira "certa" de lidar com situações como esta? Parece aceitável incluir consultas como essa em um método genérico -> query ()?

Existem melhores estratégias?

Keith Palmer Jr.
fonte
Estou passando por esse mesmo problema agora em um projeto não MVC. A questão continua aparecendo: a camada de acesso a dados abstrai todos os procedimentos armazenados e deixa o banco de dados da camada lógica de negócios independente ou a camada de acesso a dados deve ser genérica, à custa da camada de negócios, sabendo algo sobre o banco de dados subjacente? Talvez uma solução intermediária seja ter algo como ExecuteSP (string spName, parâmetros de objeto params []) e, em seguida, inclua todos os nomes de SP em um arquivo de configuração para a camada de negócios ler. Eu realmente não tenho uma resposta muito boa para isso, no entanto.
Greg Jackson

Respostas:

10

Os padrões de arquitetura de aplicativos corporativos de Martin Fowler descrevem vários padrões relacionados ao ORM, incluindo o uso do Query Object, que é o que eu sugeriria.

Os objetos de consulta permitem seguir o princípio de responsabilidade única, separando a lógica de cada consulta em objetos de estratégia gerenciados e mantidos individualmente. Seu controlador pode gerenciar seu uso diretamente ou delegá-lo a um controlador secundário ou objeto auxiliar.

Você vai ter muitos deles? Certamente. Alguns podem ser agrupados em consultas genéricas? Sim novamente.

Você pode usar a injeção de dependência para criar os objetos a partir de metadados? É o que a maioria das ferramentas ORM faz.

Matthew Flynn
fonte
4

Não há maneira correta de fazer isso. Muitas pessoas usam ORMs para abstrair toda a complexidade. Alguns dos ORMs mais avançados convertem expressões de código em instruções SQL complicadas. Os ORMs também têm suas desvantagens, no entanto, para muitas aplicações, os benefícios superam os custos.

Se você não estiver trabalhando com um conjunto de dados massivo, a coisa mais simples a fazer é selecionar a tabela inteira na memória e filtrar o código.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

Para aplicativos de relatórios internos, essa abordagem provavelmente é boa. Se o conjunto de dados for realmente grande, você começará a precisar de muitos métodos personalizados, além de índices apropriados em sua tabela.

dan
fonte
1
+ 1 para "Não há maneira correta de fazer isso"
ozz 6/04/12
1
Infelizmente, filtrar fora do conjunto de dados não é realmente uma opção, mesmo com os menores conjuntos de dados com os quais trabalhamos - é muito lento. :-( É bom ouvir que outros correm em meu mesmo problema embora :-).
Keith Palmer Jr.
@KeithPalmer por curiosidade, quão grandes são as suas mesas?
dan
Centenas de milhares de linhas, se não mais. Muitos para filtrar com desempenho aceitável fora do banco de dados, ESPECIALMENTE com uma arquitetura distribuída onde os bancos de dados não estão na mesma máquina que o aplicativo.
Keith Palmer Jr.
-1 para "Não há maneira correta de fazer isso". Existem várias maneiras corretas. Dobrar o número de métodos quando você adiciona um recurso como o OP estava fazendo é uma abordagem não escalonável, e a alternativa sugerida aqui é igualmente não escalonável, apenas no que diz respeito ao tamanho do banco de dados e não ao número de recursos de consulta. Existem abordagens escalonáveis, veja as outras respostas.
Theodore Murdock
4

Alguns ORMs permitem construir consultas complexas a partir de métodos básicos. Por exemplo

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

é uma consulta perfeitamente válida no Django ORM .

A ideia é que você tenha algum construtor de consultas (nesse caso Purchase.objects) cujo status interno represente informações sobre uma consulta. Métodos como get, filter, exclude, order_bysão válidas e retornar um novo construtor de consulta com um status atualizado. Esses objetos implementam uma interface iterável, de modo que, quando você itera sobre eles, a consulta é executada e você obtém os resultados da consulta construídos até o momento. Embora este exemplo seja retirado do Django, você verá a mesma estrutura em muitos outros ORMs.

Andrea
fonte
Não vejo que vantagem isso tem sobre algo como old_purchases = Purchasees.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Você não está reduzindo a complexidade ou abstraindo algo apenas transformando ANDs e ORs do SQL no método ANDs e ORs - você está apenas alterando a representação dos ANDs e ORs, certo?
Keith Palmer Jr.
4
Na verdade não. Você está abstraindo o SQL, o que lhe oferece muitas vantagens. Primeiro, você evita a injeção. Em seguida, você pode alterar o banco de dados subjacente sem se preocupar com versões ligeiramente diferentes do dialeto SQL, pois o ORM lida com isso para você. Em muitos casos, você também pode colocar um back-end NoSQL sem perceber. Terceiro, esses construtores de consulta são objetos que você pode transmitir como qualquer outra coisa. Isso significa que seu modelo pode construir metade da consulta (por exemplo, você pode ter alguns métodos para a maioria dos casos comuns) e então ele pode ser refinado no controlador para lidar com o ..
Andrea
2
... casos mais específicos. Um exemplo típico é definir uma ordem padrão para modelos no Django. Todos os resultados da consulta seguirão essa ordem, a menos que você especifique o contrário. Quarto, se você precisar desnormalizar seus dados por motivos de desempenho, precisará ajustar o ORM em vez de reescrever todas as suas consultas.
Andrea
+1 Para linguagens de consulta dinâmicas como a mencionada e LINQ.
quer
2

Há uma terceira abordagem.

Seu exemplo específico exibe um crescimento exponencial no número de métodos necessários à medida que o número de recursos necessários aumenta: queremos oferecer capacidade de consultas avançadas, combinando todos os recursos de consulta ... se fizermos isso adicionando métodos, teremos um método para consulta básica, dois se adicionarmos um recurso opcional, quatro se adicionarmos dois, oito se adicionarmos três, 2 ^ n se adicionarmos n recursos.

Isso é obviamente impossível de manter além de três ou quatro recursos, e há um cheiro ruim de muitos códigos intimamente relacionados que são quase colados entre os métodos.

Você pode evitar isso adicionando um objeto de dados para armazenar os parâmetros e ter um método único que construa a consulta com base no conjunto de parâmetros fornecidos (ou não). Nesse caso, adicionar um novo recurso, como um período, é tão simples quanto adicionar setters e getters para o período ao seu objeto de dados e adicionar um pouco de código no qual a consulta parametrizada é criada:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... e onde os parâmetros são adicionados à consulta:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Essa abordagem permite o crescimento linear do código à medida que os recursos são adicionados, sem a necessidade de permitir consultas arbitrárias e sem parâmetros.

Theodore Murdock
fonte
0

Eu acho que o consenso geral é manter o máximo possível de acesso a dados em seus modelos no MVC. Um dos outros princípios de design é mover algumas de suas consultas mais genéricas (aquelas que não estão diretamente relacionadas ao seu modelo) para um nível mais alto e abstrato, onde você pode permitir que ele seja usado por outros modelos também. (No RoR, temos algo chamado framework). Há também outra coisa que você deve considerar e que é a manutenção do seu código. À medida que seu projeto cresce, se você tiver acesso a dados nos controladores, será cada vez mais difícil identificá-los (atualmente estamos enfrentando esse problema em um grande projeto). Modelos, embora cheios de métodos, forneçam um único ponto de contato para qualquer controlador que pode acabar consultando as tabelas. (Isso também pode levar a uma reutilização de código que, por sua vez, é benéfico)

Ricketyship
fonte
1
Exemplo do que você está falando ...?
perfil completo de Keith Palmer Jr.
0

A interface da camada de serviço pode ter muitos métodos, mas a chamada para o banco de dados pode ter apenas um.

Um banco de dados possui quatro operações principais

  • Inserir
  • Atualizar
  • Excluir
  • Inquerir

Outro método opcional pode ser executar alguma operação de banco de dados que não se enquadre nas operações básicas do banco de dados. Vamos chamar isso de Executar.

Inserir e atualizações podem ser combinados em uma operação, chamada Salvar.

Muitos dos seus métodos são de consulta. Assim, você pode criar uma interface genérica para satisfazer a maioria das necessidades imediatas. Aqui está uma interface genérica de exemplo:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

O objeto de transferência de dados é genérico e terá todos os seus filtros, parâmetros, classificação etc. contidos nele. A camada de dados seria responsável por analisar e extrair isso e configurar a operação no banco de dados por meio de procedimentos armazenados, sql parametrizado, linq etc. Portanto, o SQL não é passado entre as camadas. Normalmente, isso é o que um ORM faz, mas você pode criar o seu próprio e ter seu próprio mapeamento.

Então, no seu caso, você tem Widgets. Os widgets implementariam a interface IPOCO.

Então, no seu modelo de camada de serviço, teria getList().

Seria necessário uma camada de mapeamento para tranforming pega getListem

Search<Widget>(DataTransferObject<Widget> Dto)

e vice versa. Como já mencionado, em algum momento isso é feito através de um ORM, mas, no final das contas, você acaba com um monte de código padrão, especialmente se você tiver centenas de tabelas. O ORM cria magicamente SQL com parâmetros e executa isso no banco de dados. Se rolando por conta própria, adicionalmente na própria camada de dados, os mapeadores seriam necessários para configurar o SP, o linq etc. (basicamente o sql indo para o banco de dados).

Como mencionado anteriormente, o DTO é um objeto composto por composição. Talvez um dos objetos contidos nele seja um objeto chamado QueryParameters. Esses seriam todos os parâmetros para a consulta que seriam configurados e usados ​​pela consulta. Outro objeto seria uma lista de objetos retornados de consultas, atualizações, ext. Essa é a carga útil. Nesse caso, a carga útil seria uma lista de lista de widgets.

Portanto, a estratégia básica é:

  • Chamadas da camada de serviço
  • Transformar chamada de camada de serviço em banco de dados usando algum tipo de repositório / mapeamento
  • Chamada de banco de dados

No seu caso, acho que o modelo poderia ter muitos métodos, mas, idealmente, você deseja que a chamada ao banco de dados seja genérica. Você ainda acaba com muitos códigos de mapeamento padrão (especialmente com SPs) ou código ORM mágico que está criando dinamicamente o SQL parametrizado para você.

Jon Raynor
fonte