Conception de modèle de référentiel approprié en PHP?


Answers

Basé sur mon expérience, voici quelques réponses à vos questions:

Q: Comment pouvons-nous faire pour ramener les champs dont nous n'avons pas besoin?

R: D'après mon expérience, cela se résume à traiter des entités complètes par rapport à des requêtes ad-hoc.

Une entité complète est quelque chose comme un objet User . Il a des propriétés et des méthodes, etc. C'est un citoyen de première classe dans votre code.

Une requête ad hoc renvoie des données, mais nous ne connaissons rien de plus. Lorsque les données sont transmises autour de l'application, cela est fait sans contexte. Est-ce un User ? Un User avec des informations de Order pièce jointe? Nous ne savons pas vraiment.

Je préfère travailler avec des entités complètes.

Vous avez raison de dire que vous ramènerez souvent des données que vous n'utiliserez pas, mais vous pouvez y remédier de différentes manières:

  1. Cachez de manière agressive les entités afin de ne payer le prix de lecture qu'une fois dans la base de données.
  2. Passez plus de temps à modéliser vos entités afin qu'elles aient de bonnes distinctions entre elles. (Envisager de diviser une grande entité en deux entités plus petites, etc.)
  3. Envisagez d'avoir plusieurs versions d'entités. Vous pouvez avoir un User pour le back end et peut-être un UserSmall 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:

  1. Vous vous retrouvez avec essentiellement les mêmes données dans de nombreuses requêtes. Par exemple, avec un User , vous finirez par écrire essentiellement la même select * pour de nombreux appels. Un appel obtiendra 8 des 10 champs, un obtiendra 5 sur 10, un obtiendra 7 sur 10. Pourquoi ne pas remplacer tous avec un appel qui obtient 10 sur 10? La raison en est que c'est un meurtre pour re-factoriser / tester / simuler.
  2. Il devient très difficile de raisonner à un niveau élevé sur votre code au fil du temps. Au lieu de déclarations comme "Pourquoi l' User si lent?" vous finissez par retrouver des requêtes uniques et les corrections de bugs ont tendance à être petites et localisées.
  3. Il est vraiment difficile de remplacer la technologie sous-jacente. Si vous stockez tout dans MySQL maintenant et que vous souhaitez passer à MongoDB, il est beaucoup plus difficile de remplacer 100 appels ad-hoc que par une poignée d'entités.

Q: J'aurai trop de méthodes dans mon dépôt.

R: Je n'ai pas vraiment vu d'autre moyen que de consolider les appels. La méthode appelle dans votre référentiel mappage vers les fonctionnalités de votre application. Plus il y a de fonctionnalités, plus il y a d'appels 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 faire un tas de procédures stockées.

Parfois, je dois me dire: "Eh bien, il a dû donner quelque part! Il n'y a pas de balles d'argent."

Question

Préface: J'essaie d'utiliser le modèle de référentiel dans une architecture MVC avec des bases de données relationnelles.

J'ai récemment commencé à apprendre TDD en PHP, et je me rends compte que ma base de données est couplée beaucoup trop étroitement avec le reste de mon application. J'ai lu des dépôts et utilisé un conteneur IoC pour "l'injecter" dans mes contrôleurs. Trucs très cool. Mais maintenant, posez quelques questions pratiques sur la conception du dépôt. Considérez l'exemple suivant.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Issue # 1: Trop de champs

Toutes ces méthodes de recherche utilisent une approche de sélection de tous les champs ( SELECT * ). Cependant, dans mes applications, j'essaie toujours de limiter le nombre de champs que je reçois, car cela ajoute souvent des frais généraux et ralentit les choses. Pour ceux qui utilisent ce modèle, comment gérez-vous cela?

Question n ° 2: Trop de méthodes

Bien que cette classe soit agréable en ce moment, je sais que dans une application du monde réel, j'ai besoin de beaucoup plus de méthodes. Par exemple:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Comme vous pouvez le voir, il pourrait y avoir une très, très longue liste de méthodes possibles. Et puis, si vous ajoutez le problème de sélection des champs ci-dessus, le problème s'aggrave. Dans le passé, je mettais normalement toute cette logique dans mon contrôleur:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')->byCountry('Canada')->orderBy('name')->rows()

        return View::make('users', array('users' => $users))
    }

}

Avec mon approche de référentiel, je ne veux pas finir avec ceci:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Problème n ° 3: impossible de faire correspondre une interface

Je vois l'avantage d'utiliser des interfaces pour les référentiels, donc je peux échanger ma mise en œuvre (à des fins de test ou autre). Ma compréhension des interfaces est qu'elles définissent un contrat qu'une implémentation doit suivre. C'est génial jusqu'à ce que vous commenciez à ajouter des méthodes supplémentaires à vos dépôts comme findAllInCountry() . Maintenant, j'ai besoin de mettre à jour mon interface pour avoir aussi cette méthode, sinon d'autres implémentations peuvent ne pas l'avoir, et cela pourrait casser mon application. Par cela se sent fou ... un cas de la queue qui remue le chien.

Modèle de spécification?

Cela m'amène à croire que repository ne devrait avoir qu'un nombre fixe de méthodes (comme save() , remove() , find() , findAll() , etc). Mais alors comment puis-je exécuter des recherches spécifiques? J'ai entendu parler du modèle de spécification , mais il me semble que cela ne fait que réduire un ensemble complet d'enregistrements (via IsSatisfiedBy() ), ce qui IsSatisfiedBy() clairement des problèmes de performance si vous utilisez une base de données.

Aidez-moi?

Il est clair que j'ai besoin de repenser un peu les choses lorsque je travaille avec des dépôts. Quelqu'un peut-il nous éclairer sur la meilleure façon de le faire?




J'ajouterai un peu à cela car j'essaie actuellement de comprendre 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 de ORM, vous pouvez simplement utiliser ses méthodes pour prendre soin de ces choses. Faites votre propre commandePar les fonctions qui implémentent les méthodes Eloquent si vous en avez besoin. En utilisant Eloquent par exemple:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

Ce que vous semblez rechercher, c'est un ORM. Aucune raison que votre référentiel ne peut pas être basé autour d'un. Cela demanderait à l'utilisateur d'être éloquent, mais personnellement, je ne vois pas cela comme un problème.

Si vous voulez cependant éviter un ORM, vous devrez alors "rouler 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 est de ne pas implémenter une fonction requise de cette interface. Vous pouvez également étendre les interfaces comme les classes pour garder les choses au sec.

Cela dit, je commence tout juste à comprendre, mais ces réalisations m'ont aidé.




Je ne peux que commenter la façon dont nous (mon entreprise) traitons cela. Tout d'abord, la performance n'est pas un problème pour nous, mais le fait d'avoir un code propre / correct est.

Tout d'abord, nous définissons des modèles tels qu'un UserModel qui utilise un ORM pour créer des objets UserEntity . Lorsqu'un UserEntity est chargé à partir d'un modèle, tous les champs sont chargés. Pour les champs référençant 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 à nouveau. Maintenant, votre réaction initiale pourrait être ... ??? ... !!! laissez-moi vous donner un exemple d'un exemple:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

Dans notre cas, $db est un ORM capable de charger des entités. Le modèle indique à 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, seuls les ID de ces objets sont chargés. Dans ce cas, le OrderModel crée des OrderEntity avec uniquement les ID des ordres référencés. Lorsque PersistentEntity::getField est appelé par OrderEntity l'entité indique à son modèle de charger par chargement tous les champs dans OrderEntity . Tous les OrderEntity associés à un UserEntity sont traités comme un ensemble de résultats et seront chargés en même temps.

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 wrapper pour la méthode générique getField fournie par PersistentEntity . Pour résumer, nous chargeons toujours tous les champs, mais les champs référençant une entité étrangère sont chargés si nécessaire. Charger juste un tas de champs n'est pas vraiment un problème de performance. Charger toutes les entités étrangères possibles, mais serait une énorme baisse de performance.

Maintenant sur le chargement d'un ensemble spécifique d'utilisateurs, basé sur une clause where. Nous fournissons un ensemble de classes orientées objet qui vous permettent 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 select. Il contient une collection de clauses where, une clause group by et tout le reste. Nos clauses where sont assez compliquées mais vous pouvez évidemment faire une version plus simple facilement.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

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 le plus rapidement et le plus clairement possible. Si vous avez d'autres questions n'hésitez pas à les poser et je mettrai à jour ma réponse.

EDIT: De plus, si vous ne voulez pas charger certains champs immédiatement, vous pouvez spécifier une option de chargement paresseux dans votre mapping ORM. Étant donné que tous les champs sont éventuellement chargés via la méthode getField , vous pouvez charger certains champs à la 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.






Links