Stratégies pour éviter SQL dans vos contrôleurs… ou combien de méthodes dois-je avoir dans mes modèles?

17

Donc, une situation que je rencontre assez souvent est celle où mes modèles commencent à:

  • Devenez des monstres avec des tonnes et des tonnes de méthodes

OU

  • Vous permettre de leur passer des morceaux de SQL, afin qu'ils soient suffisamment flexibles pour ne pas nécessiter un million de méthodes différentes

Par exemple, disons que nous avons un modèle de "widget". Nous commençons par quelques méthodes de base:

  • get ($ id)
  • insérer ($ record)
  • mise à jour ($ id, $ record)
  • supprimer ($ id)
  • getList () // récupère une liste de Widgets

C'est très bien et dandy, mais alors nous avons besoin de quelques rapports:

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

Et puis le reporting commence à devenir complexe:

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

Vous pouvez voir où cela se développe ... finalement, nous avons tellement d'exigences de requête spécifiques que j'ai besoin d'implémenter des tonnes et des tonnes de méthodes, ou une sorte d'objet "requête" que je peux transmettre à une seule -> requête (requête $ query), méthode ...

... ou mordez simplement la balle et commencez à faire quelque chose comme ceci:

  • list = MyModel-> query ("start_date> X AND end_date <Y AND en attente = 1 AND customer_id = Z")

Il y a un certain attrait à avoir une seule méthode comme celle-ci au lieu de 50 millions d'autres méthodes plus spécifiques ... mais il semble parfois "mal" de bourrer une pile de ce qui est fondamentalement SQL dans le contrôleur.

Existe-t-il une «bonne» façon de gérer des situations comme celle-ci? Semble-t-il acceptable de bourrer des requêtes comme celle-ci dans une méthode générique -> query ()?

Y a-t-il de meilleures stratégies?

Keith Palmer Jr.
la source
Je traverse ce même problème en ce moment dans un projet non MVC. La question ne cesse de se poser: la couche d'accès aux données devrait-elle abstraire chaque procédure stockée et laisser la base de données de la couche logique métier agnostique, ou la couche d'accès aux données devrait-elle être générique, au détriment de la couche métier sachant quelque chose sur la base de données sous-jacente? Une solution intermédiaire consiste peut-être à avoir quelque chose comme ExecuteSP (chaîne spName, params object [] parameters), puis à inclure tous les noms de SP dans un fichier de configuration pour la couche métier à lire. Mais je n'ai pas vraiment de très bonne réponse à cela.
Greg Jackson

Réponses:

10

Les modèles d'architecture d'application d'entreprise de Martin Fowler décrivent un certain nombre de paterns liés à l'ORM, y compris l'utilisation de l'objet de requête, ce que je suggère.

Les objets de requête vous permettent de suivre le principe de responsabilité unique, en séparant la logique de chaque requête en objets de stratégie gérés et gérés individuellement. Soit votre contrôleur peut gérer directement leur utilisation, soit déléguer cela à un contrôleur secondaire ou à un objet d'assistance.

En aurez-vous beaucoup? Certainement. Certains peuvent-ils être regroupés en requêtes génériques? Oui encore.

Pouvez-vous utiliser l'injection de dépendances pour créer les objets à partir de métadonnées? C'est ce que font la plupart des outils ORM.

Matthew Flynn
la source
4

Il n'y a pas de bonne façon de procéder. Beaucoup de gens utilisent des ORM pour résumer toute la complexité. Certains des ORM les plus avancés traduisent les expressions de code en instructions SQL compliquées. Les ORM ont également leurs inconvénients, mais pour de nombreuses applications, les avantages l'emportent sur les coûts.

Si vous ne travaillez pas avec un ensemble de données massif, la chose la plus simple à faire est de sélectionner la table entière en mémoire et de filtrer en code.

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

Pour les applications de reporting internes, cette approche est probablement appropriée. Si l'ensemble de données est vraiment volumineux, vous commencerez à avoir besoin de nombreuses méthodes personnalisées ainsi que d'index appropriés sur votre table.

dan
la source
1
+ 1 pour "Il n'y a pas de bonne façon de procéder"
ozz
1
Malheureusement, le filtrage en dehors de l'ensemble de données n'est pas vraiment une option avec même les plus petits ensembles de données avec lesquels nous travaillons - c'est tout simplement trop lent. :-( Heureux d'entendre que d'autres rencontrent mon même problème. :-)
Keith Palmer Jr.
@KeithPalmer par curiosité, quelle est la taille de vos tables?
dan
Des centaines de milliers de lignes, sinon plus. Trop nombreux pour filtrer avec des performances acceptables en dehors de la base de données, SURTOUT avec une architecture distribuée où les bases de données ne sont pas sur la même machine que l'application.
Keith Palmer Jr.
-1 pour "Il n'y a pas de bonne façon de procéder". Il existe plusieurs façons correctes. Le doublement du nombre de méthodes lorsque vous ajoutez une fonctionnalité comme le faisait l'OP est une approche non évolutive, et l'alternative suggérée ici est également non évolutive, uniquement en ce qui concerne la taille de la base de données plutôt que le nombre de fonctionnalités de requête. Des approches évolutives existent, voir les autres réponses.
Theodore Murdock
4

Certains ORM vous permettent de construire des requêtes complexes à partir de méthodes de base. Par exemple

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

est une requête parfaitement valide dans l' ORM Django .

L'idée est que vous disposez d'un générateur de requêtes (dans ce cas Purchase.objects) dont l'état interne représente des informations sur une requête. Des méthodes telles que get, filter, exclude, order_bysont valides et renvoient un nouveau générateur de requêtes avec un état mis à jour. Ces objets implémentent une interface itérable, de sorte que lorsque vous les parcourez, la requête est exécutée et vous obtenez les résultats de la requête construite jusqu'à présent. Bien que cet exemple soit tiré de Django, vous verrez la même structure dans de nombreux autres ORM.

Andrea
la source
Je ne vois pas quel avantage cela a sur quelque chose comme old_purchases = Purchases.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Vous ne réduisez pas la complexité ou n'abstenez rien en transformant simplement les AND et OR SQL en AND et OR de méthode - vous changez simplement la représentation des AND et des OR, n'est-ce pas?
Keith Palmer Jr.
4
Pas vraiment. Vous éloignez le SQL, ce qui vous offre de nombreux avantages. Tout d'abord, vous évitez l'injection. Ensuite, vous pouvez modifier la base de données sous-jacente sans vous soucier des versions légèrement différentes du dialecte SQL, car l'ORM gère cela pour vous. Dans de nombreux cas, vous pouvez également mettre un backend NoSQL sans le remarquer. Troisièmement, ces générateurs de requêtes sont des objets que vous pouvez faire circuler comme n'importe quoi d'autre. Cela signifie que votre modèle peut construire la moitié de la requête (par exemple, vous pouvez avoir certaines méthodes pour les cas les plus courants) et ensuite il peut être affiné dans le contrôleur pour gérer le ..
Andrea
2
... les cas les plus spécifiques. Un exemple typique est la définition d'un ordre par défaut pour les modèles dans Django. Tous les résultats de la requête suivront cet ordre, sauf indication contraire. Quatrièmement, si vous avez besoin de dénormaliser vos données pour des raisons de performances, il vous suffit de modifier l'ORM plutôt que de réécrire toutes vos requêtes.
Andrea
+1 Pour les langages de requête dynamiques comme celui mentionné et LINQ.
Evan Plaice
2

Il y a une troisième approche.

Votre exemple spécifique montre une croissance exponentielle du nombre de méthodes nécessaires à mesure que le nombre de fonctionnalités requises augmente: nous voulons pouvoir offrir des requêtes avancées, en combinant toutes les fonctionnalités de requête ... si nous le faisons en ajoutant des méthodes, nous avons une méthode pour un requête de base, deux si nous ajoutons une fonctionnalité facultative, quatre si nous en ajoutons deux, huit si nous en ajoutons trois, 2 ^ n si nous ajoutons n fonctionnalités.

C'est évidemment impossible à maintenir au-delà de trois ou quatre fonctionnalités, et il y a une mauvaise odeur de beaucoup de code étroitement lié qui est presque copié-collé entre les méthodes.

Vous pouvez éviter cela en ajoutant un objet de données pour contenir les paramètres et avoir une seule méthode qui construit la requête en fonction de l'ensemble de paramètres fourni (ou non fourni). Dans ce cas, l'ajout d'une nouvelle fonctionnalité telle qu'une plage de dates est aussi simple que l'ajout de setters et de getters pour la plage de dates à votre objet de données, puis l'ajout d'un peu de code dans lequel la requête paramétrée est construite:

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

... et où les paramètres sont ajoutés à la requête:

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

Cette approche permet une croissance de code linéaire à mesure que des fonctionnalités sont ajoutées, sans avoir à autoriser des requêtes arbitraires et non paramétrables.

Theodore Murdock
la source
0

Je pense que le consensus général est de garder autant d'accès aux données que possible dans vos modèles dans MVC. L'un des autres principes de conception consiste à déplacer certaines de vos requêtes plus génériques (celles qui ne sont pas directement liées à votre modèle) à un niveau supérieur et plus abstrait où vous pouvez également autoriser son utilisation par d'autres modèles. (Dans RoR, nous avons quelque chose appelé framework). Il y a aussi une autre chose que vous devez considérer et c'est la maintenabilité de votre code. Au fur et à mesure que votre projet se développe, si vous avez accès aux données dans les contrôleurs, il deviendra de plus en plus difficile de le retrouver (nous sommes actuellement confrontés à ce problème dans un projet énorme). pourrait finir par interroger les tables. (Cela peut également conduire à une réutilisation du code qui est à son tour bénéfique)

Ricketyship
la source
1
Exemple de ce dont vous parlez ...?
Keith Palmer Jr.
0

Votre interface de couche de service peut avoir de nombreuses méthodes, mais l'appel à la base de données peut n'en avoir qu'une.

Une base de données comporte 4 opérations majeures

  • Insérer
  • Mise à jour
  • Supprimer
  • Requete

Une autre méthode facultative peut consister à exécuter une opération de base de données qui ne relève pas des opérations de base de la base de données. Appelons cela Exécuter.

L'insertion et les mises à jour peuvent être combinées en une seule opération, appelée Enregistrer.

Beaucoup de vos méthodes sont des requêtes. Vous pouvez donc créer une interface générique pour répondre à la plupart des besoins immédiats. Voici un exemple d'interface générique:

 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;
    }

L'objet de transfert de données est générique et contiendrait tous vos filtres, paramètres, tri, etc. La couche de données serait chargée d'analyser et d'extraire cela et de configurer l'opération dans la base de données via des procédures stockées, sql paramétré, linq etc. Ainsi, SQL n'est pas transmis entre les couches. C'est généralement ce que fait un ORM, mais vous pouvez lancer le vôtre et avoir votre propre mappage.

Donc, dans votre cas, vous avez des widgets. Les widgets implémenteraient l'interface IPOCO.

Donc, dans votre modèle de couche de service, getList().

Aurait besoin d'une couche de mappage pour gérer la transformation getListen

Search<Widget>(DataTransferObject<Widget> Dto)

et vice versa. Comme d'autres l'ont mentionné, cela se fait parfois via un ORM, mais finalement vous vous retrouvez avec beaucoup de code de type passe-partout, surtout si vous avez des centaines de tables. L'ORM crée par magie du SQL paramétré et l'exécute sur la base de données. Si vous lancez le vôtre, en plus dans la couche de données elle-même, des mappeurs seraient nécessaires pour configurer le SP, linq etc. (Fondamentalement, le sql va à la base de données).

Comme mentionné précédemment, le DTO est un objet composé par composition. Peut-être que l'un des objets qu'il contient est un objet appelé QueryParameters. Ce seraient tous les paramètres de la requête qui seraient configurés et utilisés par la requête. Un autre objet serait une liste d'objets retournés à partir de requêtes, mises à jour, ext. Ceci est la charge utile. Si ce cas, la charge utile serait une liste de liste de widgets.

Donc, la stratégie de base est:

  • Appels de couche service
  • Transformer l'appel de la couche de service à la base de données à l'aide d'une sorte de référentiel / mappage
  • Appel de base de données

Dans votre cas, je pense que le modèle pourrait avoir de nombreuses méthodes, mais de manière optimale, vous voulez que l'appel à la base de données soit générique. Vous vous retrouvez toujours avec beaucoup de code de mappage passe-partout (en particulier avec les SP) ou du code ORM magique qui crée dynamiquement le SQL paramétré pour vous.

Jon Raynor
la source