J'ai pensé que je prendrais une fissure pour répondre à ma propre question. Ce qui suit n'est qu'une façon de résoudre les problèmes 1 à 3 de ma question initiale.
Avertissement: je ne peux pas toujours utiliser les bons termes lors de la description des modèles ou des techniques. Désolé.
Les objectifs:
- Créez un exemple complet d'un contrôleur de base pour l'affichage et l'édition
Users
.
- Tout le code doit être entièrement testable et moquable.
- Le contrôleur ne devrait avoir aucune idée de l'endroit où les données sont stockées (ce qui signifie qu'elles peuvent être modifiées).
- Exemple pour montrer une implémentation SQL (la plus courante).
- Pour des performances optimales, les contrôleurs ne doivent recevoir que les données dont ils ont besoin, pas de champs supplémentaires.
- La mise en œuvre doit tirer parti d'un certain type de mappeur de données pour faciliter le développement.
- L'implémentation doit pouvoir effectuer des recherches de données complexes.
La solution
Je divise mon interaction de stockage persistant (base de données) en deux catégories: R (lecture) et CUD (création, mise à jour, suppression). D'après mon expérience, les lectures sont vraiment ce qui ralentit une application. Et bien que la manipulation des données (CUD) soit en réalité plus lente, elle se produit beaucoup moins fréquemment et est donc beaucoup moins préoccupante.
CUD (Créer, mettre à jour, supprimer) est facile. Cela impliquera de travailler avec des modèles réels , qui sont ensuite transmis à mon Repositories
pour la persistance. Remarque, mes référentiels fourniront toujours une méthode de lecture, mais simplement pour la création d'objets, pas d'affichage. Plus sur cela plus tard.
R (Lire) n'est pas si facile. Pas de modèles ici, juste des objets de valeur . Utilisez des tableaux si vous préférez . Ces objets peuvent représenter un seul modèle ou un mélange de nombreux modèles, n'importe quoi vraiment. Celles-ci ne sont pas très intéressantes en elles-mêmes, mais comment elles sont générées. J'utilise ce que j'appelle Query Objects
.
Le code:
Modèle utilisateur
Commençons simplement avec notre modèle utilisateur de base. Notez qu'il n'y a aucun élément d'extension ou de base de données ORM. Juste pure gloire du modèle. Ajoutez vos getters, setters, validation, peu importe.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Interface de référentiel
Avant de créer mon référentiel d'utilisateurs, je souhaite créer mon interface de référentiel. Cela définira le «contrat» que les référentiels doivent suivre pour être utilisé par mon contrôleur. N'oubliez pas que mon contrôleur ne saura pas où les données sont réellement stockées.
Notez que mes référentiels ne contiendront chacun que ces trois méthodes. La save()
méthode est responsable à la fois de la création et de la mise à jour des utilisateurs, simplement selon que l'objet utilisateur possède ou non un ensemble d'ID.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Implémentation du référentiel SQL
Maintenant, pour créer mon implémentation de l'interface. Comme mentionné, mon exemple allait être avec une base de données SQL. Notez l'utilisation d'un mappeur de données pour éviter d'avoir à écrire des requêtes SQL répétitives.
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 d'objet de requête
Maintenant, avec CUD (Créer, Mettre à jour, Supprimer) pris en charge par notre référentiel, nous pouvons nous concentrer sur le R (Lire). Les objets de requête sont simplement une encapsulation d'un certain type de logique de recherche de données. Ce ne sont pas des générateurs de requêtes. En l'abstrait comme notre référentiel, nous pouvons changer son implémentation et la tester plus facilement. Un exemple d'un objet de requête peut être un AllUsersQuery
ou AllActiveUsersQuery
, ou même MostCommonUserFirstNames
.
Vous pensez peut-être "ne puis-je pas simplement créer des méthodes dans mes référentiels pour ces requêtes?" Oui, mais voici pourquoi je ne fais pas ça:
- Mes référentiels sont destinés à travailler avec des objets de modèle. Dans une application du monde réel, pourquoi aurais-je besoin d'obtenir le
password
champ si je cherche à répertorier tous mes utilisateurs?
- Les référentiels sont souvent spécifiques à un modèle, mais les requêtes impliquent souvent plusieurs modèles. Alors, dans quel référentiel mettez-vous votre méthode?
- Cela rend mes référentiels très simples, pas une classe de méthodes gonflée.
- Toutes les requêtes sont désormais organisées dans leurs propres classes.
- Vraiment, à ce stade, les référentiels existent simplement pour abstraire ma couche de base de données.
Pour mon exemple, je vais créer un objet de requête pour rechercher "AllUsers". Voici l'interface:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implémentation d'un objet de requête
C'est là que nous pouvons à nouveau utiliser un mappeur de données pour accélérer le développement. Notez que j'autorise un ajustement à l'ensemble de données renvoyé - les champs. C'est à peu près autant que je veux aller avec la manipulation de la requête effectuée. N'oubliez pas que mes objets de requête ne sont pas des générateurs de requêtes. Ils effectuent simplement une requête spécifique. Cependant, comme je sais que j'utiliserai probablement celui-ci beaucoup, dans un certain nombre de situations différentes, je me donne la possibilité de spécifier les champs. Je ne veux jamais retourner les champs dont je n'ai pas besoin!
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();
}
}
Avant de passer au contrôleur, je veux montrer un autre exemple pour illustrer sa puissance. J'ai peut-être un moteur de création de rapports et je dois créer un rapport pour AllOverdueAccounts
. Cela pourrait être délicat avec mon mappeur de données, et je souhaiterais peut-être écrire des données réelles SQL
dans cette situation. Pas de problème, voici à quoi pourrait ressembler cet objet de requête:
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...";
}
}
Cela conserve bien toute ma logique pour ce rapport dans une seule classe, et c'est facile à tester. Je peux me moquer de mon contenu, ou même utiliser une implémentation complètement différente.
Le controlle
Maintenant, la partie amusante - réunir toutes les pièces. Notez que j'utilise l'injection de dépendance. Généralement, les dépendances sont injectées dans le constructeur, mais je préfère en fait les injecter directement dans mes méthodes de contrôleur (routes). Cela minimise le graphique d'objet du contrôleur, et je le trouve plus lisible. Notez que si vous n'aimez pas cette approche, utilisez simplement la méthode traditionnelle du constructeur.
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;
}
}
Dernières pensées:
Les points importants à noter ici sont que lorsque je modifie (crée, met à jour ou supprime) des entités, je travaille avec des objets de modèle réels et j'effectue la persistance via mes référentiels.
Cependant, lorsque j'affiche (sélectionne des données et les envoie aux vues), je ne travaille pas avec des objets de modèle, mais plutôt avec des objets de valeur ancienne. Je sélectionne uniquement les champs dont j'ai besoin et il est conçu pour que je puisse maximiser mes performances de recherche de données.
Mes référentiels restent très propres, et à la place, ce "désordre" est organisé dans mes requêtes de modèle.
J'utilise un mappeur de données pour aider au développement, car il est tout simplement ridicule d'écrire du SQL répétitif pour les tâches courantes. Cependant, vous pouvez absolument écrire SQL si nécessaire (requêtes compliquées, rapports, etc.). Et quand vous le faites, il est bien caché dans une classe correctement nommée.
J'adorerais entendre votre point de vue sur mon approche!
Mise à jour de juillet 2015:
On m'a demandé dans les commentaires où je me suis retrouvé avec tout cela. Eh bien, pas si loin en fait. Honnêtement, je n'aime toujours pas vraiment les dépôts. Je les trouve exagérés pour les recherches de base (surtout si vous utilisez déjà un ORM), et désordonnés lorsque vous travaillez avec des requêtes plus compliquées.
Je travaille généralement avec un ORM de style ActiveRecord, donc le plus souvent je vais simplement référencer ces modèles directement dans mon application. Cependant, dans les situations où j'ai des requêtes plus complexes, j'utiliserai des objets de requête pour les rendre plus réutilisables. Je dois également noter que j'injecte toujours mes modèles dans mes méthodes, ce qui les rend plus faciles à simuler dans mes tests.
new Query\ComplexUserLookup($username, $anotherCondition)
. Ou faites-le via des méthodes de définition$query->setUsername($username);
. Vous pouvez vraiment concevoir cela, mais cela a du sens pour votre application particulière, et je pense que les objets de requête laissent beaucoup de flexibilité ici.D'après mon expérience, voici quelques réponses à vos questions:
Q: Comment gérons-nous le retour des champs dont nous n'avons pas besoin?
R: D'après mon expérience, cela se résume vraiment à traiter des entités complètes par rapport aux requêtes ad hoc.
Une entité complète est quelque chose comme un
User
objet. Il a des propriétés et des méthodes, etc. C'est un citoyen de première classe dans votre base de code.Une requête ad hoc renvoie des données, mais nous ne savons rien de plus. Lorsque les données sont transmises autour de l'application, elles le sont sans contexte. Est-ce un
User
? AUser
avec quelquesOrder
informations jointes? Nous ne savons pas vraiment.Je préfère travailler avec des entités complètes.
Vous avez raison de rapporter souvent des données que vous n'utiliserez pas, mais vous pouvez résoudre ce problème de différentes manières:
User
pour le back-end et peut-être unUserSmall
pour les appels AJAX. On pourrait avoir 10 propriétés et on a 3 propriétés.Les inconvénients de travailler avec des requêtes ad hoc:
User
, vous finirez par écrire la même choseselect *
pour de nombreux appels. Un appel obtiendra 8 champs sur 10, un obtiendra 5 sur 10, un obtiendra 7 sur 10. Pourquoi ne pas tout remplacer par un appel qui obtient 10 sur 10? La raison pour laquelle c'est mauvais est que c'est un meurtre à refactoriser / tester / se moquer.User
si lent?" vous finissez par traquer les requêtes ponctuelles et les corrections de bogues ont donc tendance à être petites et localisées.Q: J'aurai trop de méthodes dans mon référentiel.
R: Je n'ai pas vraiment vu d'autre moyen que de consolider les appels. Les appels de méthode dans votre référentiel correspondent vraiment aux fonctionnalités de votre application. Plus il y a de fonctionnalités, plus les appels sont spécifiques aux données. Vous pouvez repousser les fonctionnalités et essayer de fusionner des appels similaires en un seul.
La complexité à la fin de la journée doit exister quelque part. Avec un modèle de référentiel, nous l'avons poussé dans l'interface du référentiel au lieu de peut-être créer un tas de procédures stockées.
Parfois, je dois me dire: "Eh bien, il fallait donner quelque part! Il n'y a pas de balles d'argent."
la source
SELECT *
, sélectionnez plutôt les champs dont vous avez besoin. Par exemple, consultez cette question . Quant à toutes ces requêtes ad-hock dont vous parlez, je comprends certainement d'où vous venez. J'ai une très grande application en ce moment qui en a beaucoup. C'était mon "Eh bien, il fallait donner quelque part!" moment, j'ai opté pour des performances maximales. Cependant, maintenant je traite de BEAUCOUP de requêtes différentes.reads
c'est souvent là que des problèmes de performances se posent, vous pouvez utiliser une approche de requête plus personnalisée pour eux, qui ne se traduit pas en objets métier réels. Ensuite, pourcreate
,update
etdelete
, utiliser un ORM, qui fonctionne avec des objets entiers. Des réflexions sur cette approche?J'utilise les interfaces suivantes:
Repository
- charge, insère, met à jour et supprime des entitésSelector
- recherche des entités basées sur des filtres, dans un référentielFilter
- encapsule la logique de filtrageMon
Repository
est indépendant de la base de données; en fait, il ne spécifie aucune persistance; cela peut être n'importe quoi: base de données SQL, fichier xml, service distant, un étranger de l'espace, etc. Pour les capacités de recherche, lesRepository
constructionsSelector
peuvent être filtréesLIMIT
, triées, triées et comptées. Au final, le sélecteur récupère un ou plusieursEntities
de la persistance.Voici un exemple de code:
Ensuite, une implémentation:
L'idée est que le générique
Selector
utiliseFilter
mais que l'implémentationSqlSelector
utiliseSqlFilter
; l'SqlSelectorFilterAdapter
adapte un génériqueFilter
à un bétonSqlFilter
.Le code client crée des
Filter
objets (qui sont des filtres génériques) mais dans l'implémentation concrète du sélecteur ces filtres sont transformés en filtres SQL.D'autres implémentations de sélecteurs, comme
InMemorySelector
, se transforment deFilter
enInMemoryFilter
utilisant leur spécifiqueInMemorySelectorFilterAdapter
; ainsi, chaque implémentation de sélecteur est livrée avec son propre adaptateur de filtre.En utilisant cette stratégie, mon code client (dans la couche bussines) ne se soucie pas d'une implémentation de référentiel ou de sélecteur spécifique.
PS Ceci est une simplification de mon vrai code
la source
Je vais ajouter un peu à ce sujet car j'essaie actuellement de saisir tout cela moi-même.
# 1 et 2
C'est un endroit parfait pour que votre ORM fasse le gros du travail. Si vous utilisez un modèle qui implémente une sorte d'ORM, vous pouvez simplement utiliser ses méthodes pour prendre soin de ces choses. Créez vos propres fonctions orderBy qui implémentent les méthodes Eloquent si vous en avez besoin. Utiliser Eloquent par exemple:
Ce que vous semblez rechercher, c'est un ORM. Aucune raison pour laquelle votre référentiel ne peut pas être basé sur un seul. Cela nécessiterait que l'utilisateur soit éloquent, mais personnellement, je ne considère pas cela comme un problème.
Si vous voulez cependant éviter un ORM, vous devrez alors "lancer le vôtre" pour obtenir ce que vous cherchez.
# 3
Les interfaces ne sont pas censées être des exigences strictes et rapides. Quelque chose peut implémenter une interface et y ajouter. Ce qu'il ne peut pas faire, c'est de ne pas implémenter une fonction requise de cette interface. Vous pouvez également étendre des interfaces comme des classes pour garder les choses au SEC.
Cela dit, je commence tout juste à comprendre, mais ces réalisations m'ont aidé.
la source
Je ne peux que commenter la façon dont nous (dans mon entreprise) traitons cela. Tout d'abord, les performances ne sont pas trop un problème pour nous, mais avoir un code propre / correct l'est.
Tout d'abord, nous définissons des modèles tels qu'un
UserModel
qui utilise un ORM pour créer desUserEntity
objets. Lorsqu'unUserEntity
est chargé à partir d'un modèle, tous les champs sont chargés. Pour les champs faisant référence à des entités étrangères, nous utilisons le modèle étranger approprié pour créer les entités respectives. Pour ces entités, les données seront chargées à la demande. Maintenant, votre réaction initiale pourrait être ... ??? ... !!! permettez-moi de vous donner un exemple un peu d'un exemple:Dans notre cas, il
$db
s'agit d'un ORM capable de charger des entités. Le modèle demande à l'ORM de charger un ensemble d'entités d'un type spécifique. L'ORM contient un mappage et l'utilise pour injecter tous les champs de cette entité dans l'entité. Pour les champs étrangers, cependant, seuls les identifiants de ces objets sont chargés. Dans ce cas, leOrderModel
crée desOrderEntity
s avec uniquement les identifiants des commandes référencées. QuandPersistentEntity::getField
est appelé par l'OrderEntity
entité, l'entité demande à son modèle de charger paresseusement tous les champs dans leOrderEntity
s. Tous lesOrderEntity
s associés à une UserEntity sont traités comme un seul jeu de résultats et seront chargés à la fois.La magie ici est que notre modèle et ORM injectent toutes les données dans les entités et que les entités fournissent simplement des fonctions d'encapsulation pour la
getField
méthode générique fournie parPersistentEntity
. Pour résumer, nous chargeons toujours tous les champs, mais les champs référençant une entité étrangère sont chargés lorsque cela est nécessaire. Le simple chargement d'un tas de champs n'est pas vraiment un problème de performances. Charger toutes les entités étrangères possibles serait cependant une énorme diminution des performances.Passons maintenant au chargement d'un ensemble spécifique d'utilisateurs, basé sur une clause where. Nous fournissons un package de classes orienté objet qui vous permet de spécifier une expression simple qui peut être collée ensemble. Dans l'exemple de code, je l'ai nommé
GetOptions
. C'est un wrapper pour toutes les options possibles pour une requête de sélection. Il contient une collection de clauses where, une clause group by et tout le reste. Nos clauses where sont assez compliquées, mais vous pourriez évidemment faire une version plus simple facilement.Une version la plus simple de ce système serait de passer la partie WHERE de la requête sous forme de chaîne directement au modèle.
Je suis désolé pour cette réponse assez compliquée. J'ai essayé de résumer notre cadre aussi rapidement et clairement que possible. Si vous avez des questions supplémentaires, n'hésitez pas à les poser et je mettrai à jour ma réponse.
EDIT: De plus, si vous ne voulez vraiment pas charger certains champs immédiatement, vous pouvez spécifier une option de chargement différé dans votre mappage ORM. Parce que tous les champs sont éventuellement chargés par la
getField
méthode que vous pouvez charger des champs de dernière minute lorsque cette méthode est appelée. Ce n'est pas un très gros problème en PHP, mais je ne le recommanderais pas pour d'autres systèmes.la source
Ce sont des solutions différentes que j'ai vues. Il y a des avantages et des inconvénients à chacun d'eux, mais c'est à vous de décider.
Problème n ° 1: trop de champs
Il s'agit d'un aspect important, en particulier lorsque vous prenez en compte les analyses d'index uniquement . Je vois deux solutions pour résoudre ce problème. Vous pouvez mettre à jour vos fonctions pour accepter un paramètre de tableau facultatif qui contiendrait une liste de colonnes à renvoyer. Si ce paramètre est vide, vous renvoyez toutes les colonnes de la requête. Cela peut être un peu bizarre; sur la base du paramètre, vous pouvez récupérer un objet ou un tableau. Vous pouvez également dupliquer toutes vos fonctions afin d'avoir deux fonctions distinctes qui exécutent la même requête, mais l'une renvoie un tableau de colonnes et l'autre renvoie un objet.
Problème n ° 2: trop de méthodes
J'ai brièvement travaillé avec Propel ORM il y a un an et cela est basé sur ce dont je me souviens de cette expérience. Propel a la possibilité de générer sa structure de classe à partir du schéma de base de données existant. Il crée deux objets pour chaque table. Le premier objet est une longue liste de fonctions d'accès similaires à ce que vous avez actuellement répertorié;
findByAttribute($attribute_value)
. L'objet suivant hérite de ce premier objet. Vous pouvez mettre à jour cet objet enfant pour intégrer vos fonctions getter plus complexes.Une autre solution consisterait
__call()
à mapper des fonctions non définies à quelque chose d'actionnable. Votre__call
méthode serait serait en mesure d'analyser les findById et findByName dans différentes requêtes.J'espère que cela aide au moins un peu quoi.
la source
Je suggère https://packagist.org/packages/prettus/l5-repository en tant que fournisseur pour implémenter des référentiels / critères, etc. dans Laravel5: D
la source
Je suis d'accord avec @ ryan1234 que vous devez faire circuler des objets complets dans le code et utiliser des méthodes de requête génériques pour obtenir ces objets.
Pour une utilisation externe / d'extrémité, j'aime vraiment la méthode GraphQL.
la source
Mon instinct me dit que cela nécessite peut-être une interface qui implémente des méthodes optimisées pour les requêtes aux côtés de méthodes génériques. Les requêtes sensibles aux performances doivent avoir des méthodes ciblées, tandis que les requêtes peu fréquentes ou légères sont traitées par un gestionnaire générique, peut-être aux dépens du contrôleur qui jongle un peu plus.
Les méthodes génériques permettraient à toute requête d'être implémentée et empêcheraient ainsi de casser les changements pendant une période de transition. Les méthodes ciblées vous permettent d'optimiser un appel lorsque cela a du sens et il peut être appliqué à plusieurs fournisseurs de services.
Cette approche s'apparenterait à des implémentations matérielles exécutant des tâches optimisées spécifiques, tandis que les implémentations logicielles font le travail léger ou l'implémentation flexible.
la source
Je pense que graphQL est un bon candidat dans un tel cas pour fournir un langage de requête à grande échelle sans augmenter la complexité des référentiels de données.
Cependant, il existe une autre solution si vous ne voulez pas opter pour le graphQL pour l'instant. En utilisant un DTO où un objet est utilisé pour transporter les données entre les processus, dans ce cas entre le service / contrôleur et le référentiel.
Une réponse élégante est déjà fournie ci-dessus, mais je vais essayer de donner un autre exemple que je pense qu'il est plus simple et pourrait servir de point de départ pour un nouveau projet.
Comme indiqué dans le code, nous n'aurions besoin que de 4 méthodes pour les opérations CRUD. la
find
méthode serait utilisée pour lister et lire en passant l'argument objet. Les services principaux peuvent créer l'objet de requête défini en fonction d'une chaîne de requête URL ou en fonction de paramètres spécifiques.L'objet de requête (
SomeQueryDto
) pourrait également implémenter une interface spécifique si nécessaire. et est facile à étendre plus tard sans ajouter de complexité.Exemple d'utilisation:
la source