La classe SQuerySet
La classe SQuerySet vous permet de récupérer des enregistrements dans votre base de données. Une instance de cette classe représente une collection d’objets issus de la base, filtrés par certains critères. En fait, cette classe permet d’opérer des SELECT, et les filtres sont des clauses de type WHERE ou LIMIT.
Vous récupérez une instance de SQuerySet via l’instance de la classe SManager de votre classe métier, stockée dans la propriété statique $objects. Par exemple, pour retourner tous les enregistrements d’une table :
$all_posts = Post::$objects->all();
Filtrage des objets
L’instance de SQuerySet retournée par le SManager de votre classe métier “contient” tous les objets de votre classe. Toutefois, vous n’avez en général besoin que d’un sous-ensemble de ces objets. Vous devez pour cela ajouter des filtres à votre instance de SQuerySet, en appellant les méthodes suivantes :
filter($args)
Retourne une nouvelle instance de SQuerySet contenant les objets correspondant aux paramètres fournis.
exclude($args)
L’inverse !
Par exemple, si vous souhaitez récupérer tous les billets publiés le 1er janvier 2007 :
$posts = Post::$objects->filter("created_on = '2007-01-01'");
Vous pouvez mettre plusieurs conditions dans le même appel :
$posts = Post::$objects->filter("created_on = '2007-01-01'", "user = 'raphael'");
Ou bien chaîner les appels :
$posts = Post::$objects->filter("created_on = '2007-01-01'") ->filter("user = 'raphael'") ->exclude("published = '0');
A quel moment la requête SQL est-elle effectuée ?
Créer une nouvelle instance de SQuerySet n’entraîne aucune activité dans la base. Vous pouvez ajouter de nouveaux filtres à volonté, la requête ne sera effectuée que quand votre SQuerySet est évalué :
- Itération : un
SQuerySetest itérable, et la requête n’est exécutée que lors de la première itération (le résultat est mis en cache).
foreach (Post::$objects->all() as $post) { echo $post->title; }
- to_array() : cette méthode force l’exécution de la requête, et vous retourne un tableau contenant l’ensemble des enregistrements.
Méthodes disponibles
- filter($args)
Retourne un SQuerySet représentant tous les objets correspondant aux conditions fournies. Des conditions multiples sont jointes par un AND dans la requête correspondante. Vous pouvez, afin de vous protéger des risques d’injection SQL, utiliser des parametres liés avec 4 syntaxes distinctes :
$emp = Employe::$objects->filter("firstname = ?", "lastname = ?", array('John', 'Doe')); // OU : $emp = Employe::$objects->filter("firstname = '%s'", "lastname = '%s'", array('John', 'Doe')); // OU : $emp = Employe::$objects->filter("firstname = :first", "lastname = :last", array('first' => 'John', 'last' => 'Doe')); // OU : $emp = Employe::$objects->filter("firstname = :first", "lastname = :last", array(':first' => 'John', ':last' => 'Doe'));
- exclude($args)
Retourne un SQuerySet représentant tous les objets ne correspondant pas aux conditions fournies. Des conditions multiples sont jointes par un AND dans la requête correspondante, et le tout est inclus dans un NOT().
Cet exemple exclut tous les employés dont le prénom est John ET dont le nom est Doe.
$emp = Employe::$objects->exclude("firstname = 'John'", "lastname = 'Doe'");
Equivaut à :
SELECT ... WHERE NOT (firstname = 'John' AND lastname = 'Doe')
Cet exemple exclut tous les employés dont le prénom est John OU dont le nom est Doe. Il est donc plus restrictif.
$emp = Employe::$objects->exclude("firstname = 'John'")->exclude("lastname = 'Doe'");
Equivaut à :
SELECT ... WHERE NOT firstname = 'John' AND NOT lastname = 'Doe'
- order_by($fields)
Permet d’ordonner les résultats retournés selon un ou plusieurs champs. Cette méthode retourne un SQuerySet.
Exemple :
$posts = Post::$objects->all()->order_by('-created_on', 'title');
Les billets retournés seront classés par date de création décroissante (la décroissance est indiquée par le signe - devant le nom de champ) puis par titre en ordre ascendant.
- limit($limit, $offset = 0)
Permet de limiter votre requête à un certain nombre de résultats. Cette méthode retourne un SQuerySet.
- count()
Retourne le nombre d’enregistrements retournés par votre requête. Cette méthode réagit différemment selon le contexte de l’appel : si la requête a déjà été exécutée (mais que tous les enregistrements n’ont pas été fetchés), elle appelle la méthode row_count() de la connection DB en cours ; si les enregistrements sont en cache, elle en retourne le nombre ; enfin, si la requête n’a pas encore été exécutée, elle fait une requête de type SELECT COUNT(*).
- distinct()
Retourne un SQuerySet utilisant un SELECT DISTINCT pour sa requête, ce qui permet d’éliminer les doublons dans les enregistrements. Cette méthode est en général utilisée en conjonction avec la méthode values() décrite plus bas.
Exemple :
$auteurs = Post::$objects->distinct()->values('author');
Retourne tous les auteurs distincts ayant publié des billets.
- values($fields)
Retourne une instance de SValuesQuerySet, c.a.d un SQuerySet qui retourne des tableaux de valeurs plutôt que des objets. Le paramètre optionnel $fields permet de limiter les champs retournés à ceux spécifiés.
Exemple :
Post::$objects->all(); // -> retourne des instances de la classe Post Post::$objects->all()->values(); // -> retourne des tableaux du type : array('title' => 'Hello world', 'body' => 'bla bla bla', 'author' => 'raphael', ...) Post::$objects->all()->values('title', 'author'); // -> retourne des tableaux du type : array('title' => 'Hello world', 'author' => 'raphael')
Un SValuesQuerySet est utile lorsque vous n’avez besoin que d’un petit nombre de champs, ou que vous n’avez pas l’utilité des fonctionnalités d’une instance de votre classe métier (par exemple pour afficher un select dans un formulaire). La requête correspondante sera plus efficiente.
- includes($associations)
Vous permet de récupérer des enregistrements correspondant à une certaine classe ainsi qu’à certaines de ses associations en une seule requête. C’est une façon de résoudre le problème de l’antipattern N+1 : récupérer 100 billets pour lesquels vous avez besoin pour chacun d’afficher leur auteur entraîne l’exécution de 101 requêtes SQL !
Par exemple :
class Post extends SActiveRecord { public static $objects; public static $relationships = array( 'author' => 'belongs_to', 'comments' => 'has_many' ); }
L’exécution de cette boucle :
foreach (Post::$objects->all() as $post) { echo 'Titre: '.$post->title; echo 'Author: '.$post->author->name; echo 'Last comment on: '.$post->comments->first()->created_on; }
Entraine l’exécution de 201 requêtes SQL ! En l’optimisant à l’aide de la méthode includes() :
// notez bien que les paramètres fournis sont les noms des associations déclarées dans la classe Post foreach (Post::$objects->all()->includes('author', 'comments') as $post) { ...
Une seule requête sera nécessaire grâce à l’utilisation des jointures suivantes :
LEFT OUTER JOIN comments ON comments.post_id = posts.id LEFT OUTER JOIN authors ON authors.id = posts.author_id
Etant donné que ce mécanisme extrait des données issues de plusieurs tables, vous devrez, afin d’éviter de probables ambiguités, préfixer vos noms de champs par la table correspondante dans vos conditions.
Par exemple :
$posts = Post::$objects->includes('comments')->filter("posts.created_on = '2007-01-01'")->order_by('posts.id');
- get($args)
Cette méthode permet de récupérer un objet d’après la valeur de sa clé primaire, ou d’après les valeurs de certains paramètres.
$post = Post::$objects->get(5);
Retourne l’objet dont l’id est 5, si il existe. Dans le cas contraire, une exception de type SActiveRecordDoesNotExist est levée.
$emp = Employe::$objects->get("firstname = 'John'", "lastname = 'Doe'");
Retourne l’objet correspondant à ces valeurs, ou lève une exception de type SAssertionError si il y en a plusieurs.
$emp = new Employe(); $emp->firstname = 'John'; $emp->lastname = 'Doe'; $employe = Employe::$objects->get($emp);
Remarque importante : si vous fournissez à cette méthode une valeur numérique, la condition résultante sera id = 5. Si par contre la table correspondante n’utilise pas le type integer pour stocker les ids, vous devez préciser la condition complète : id = ‘zefzerf‘.
$post = Post::$objects->get(5); // sera converti automatiquement en : $post = Post::$objects->get('id = 5'); // mais attention : $post = Post::$objects->get('ertertert'); // ne sera pas converti, et la requête SQL correspondante sera du type : SELECT * FROM table WHERE ertertert // ce qui bien sûr débouchera sur une exception. // vous devez donc préciser : $post = Post::$objects->get("id = 'ertertert'"); // ou mieux, afin d'éviter les risques d'injection SQL : $post = Post::$objects->get("id = ?", array('ertertert'));
- in_bulk($ids)
Cette méthode prend comme argument un tableau d’IDs et retourne un tableau d’objets indexés par la valeur de leur clé primaire. Si le tableau fourni est vide, elle retourne un tableau vide.
$post = Post::$objects->in_bulk(array(5, 6, 7)); // -> array(5 => object Post, 6 => object Post, 7 => object Post)
Requêtes et associations
Stato vous offre un moyen pratique d’utiliser les associations dans vos requêtes, en générant les jointures pour vous.
Exemple :
// retourne tous les employés de la World company $employes = Employe::$objects->filter("company->name = 'World company inc.'");
