Active Record

Même si Yii DAO peut gérer n'importe quelle tache relative à la base de données, il est fort probable qu'une grande partie du temps de développement soit consacré à l'écriture de requêtes SQL pour faire des opérations CRUD (create, read, update and delete) courantes. De plus il est difficile de maintenir un code quand celui est parsemé de requêtes SQL. Active Record est là pour régler ces problèmes.

Active Record (AR) est une technique très utilisée de Object-Relational Mapping (ORM). Chaque classe AR représente une table dans la base, dont les attributs représente une propriété de cette classe AR, et une instance AR représente une ligne de cette table. Les opérations couramment utilisées sont implémentées dans des méthodes de cette classe. Ainsi il est possible d'accéder aux données d'une manière orientée objet. Par exemple, il est possible d'utiliser le code suivant pour insérer une nouvelle ligne dans la table tbl_post:

$post=new Post;
$post->title='post de test';
$post->content='contenu du post de test';
$post->save();

Nous allons maintenant voir comment configurer AR et comment l'utiliser pour effectuer des opérations CRUD. Nous verrons dans la prochaine section comment utiliser AR pour régler les cas de clés étrangères entre tables. Pour simplifier les choses, la table suivante sera utilisée. Attention, si vous utilisez une base MySQL, il faut remplacer AUTOINCREMENT par AUTO_INCREMENT dans le code SQL suivant.

CREATE TABLE tbl_post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    create_time INTEGER NOT NULL
);

Note: AR n'est pas fait pour régler tous les problèmes liés aux tâches touchant à la base. AR convient le mieux pour modéliser des tables en PHP et pour effectuer des requêtes n'impliquant pas de code SQL complexe. Yii DAO est préférable dans ces cas de requêtes SQL complexes.

1. Etablir une connection à la BD

AR à besoin d'une connection à une BD pour effectuer les tâches relatives à la base. Par défaut, l'application component db sera utilisé pour récupérer l'instance CDbConnection qui servira pour la connection à la base. Voici un exemple de configuration:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // turn on schema caching to improve performance
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

Tip: Comme Active Record dépend des metadata décrivant les tables pour connaitre les informations d'une colonne, cela prend du temps lors de la lecture des metadata et de leurs analyse. Si il est peu probable que le schéma de votre base change, il est conseillé d'activer le cache sur le schéma en positionnant la propriété CDbConnection::schemaCachingDuration sur une valeur supérieure à 0.

Le support pour AR est limité par le SGBD. Actuellement, seulement les SGBD suivant le supportent:

Note: Le support de Microsoft SQL Server est disponible depuis la version 1.0.4; et le support pour Oracle est disponible depuis la version 1.0.5.

Si vous voulez utiliser une application component autre que db, ou si vous voulez travailler sur plusieurs bases avec AR, vous pouvez surcharger CActiveRecord::getDbConnection(). La classe CActiveRecord est la classe de base pour toutes les classes AR.

Tip: Il existe 2 moyens de gérer plusieurs BD avec AR. Si les schémas des bases sont différents, il est possible de créer différentes classes AR de base avec différentes implémentations de getDbConnection(). Dans le cas contraire, il est préférable de changer dynamiquement la variable statique CActiveRecord::db.

2. Définition d'une classe AR

Pour accéder à une table en base, il faut tout d'abord définir une classe AR en héritant de CActiveRecord. Chaque classe AR représente une seule table, et une instance AR représente une ligne dans cette table. L'exemple suivant montre le code minimum requit pour la classe AR représentant la table tbl_post.

class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return 'tbl_post';
    }
}

Tip: Comme les classes AR sont souvent référencées à de nombreux endroits, il est possible d'importer tout le répertoire contenant les classes AR, plutôt que de les inclure une par une. Par exemple, si tous vos fichiers de classes AR sont dans protected/models, il est possible de configurer l'application comme suivant:

return array(
  'import'=>array(
      'application.models.*',
  ),
);

Par défaut, le nom de la classe AR est le même que celui de la table en base. Il est possible de surcharger la méthode tableName() si ceux ci sont différents. La méthode model() est déclarée dans chaque classe AR (explication peu après).

Info: Pour utiliser les préfixes de table introduit dans la version 1.1.0, la méthode tableName() pour une classe AR peut etre surchargée comme suivant,

public function tableName()
{
    return '{{post}}';
}

Ainsi, à la place de retourner le nom de la table complète, le nom de la table sans le préfixe et entre doubles accolades est renvoyé.

Les valeurs en colonne d'une ligne de table sont accessibles en tant que propriétés de l'instance AR correspondante. Par exemple, le code suivant écrit dans la colonne (attribut) title:

$post=new Post;
$post->title='un post de test';

Bien que la propriété title ne soit jamais déclarée explicitement dans la classe Post, il est tout de même possible d'y accéder dans le code précédant. Cela s'explique car title est une colonne dans la table tbl_post, et que CActiveRecord la rend accessible en tant que propriété grâce aux méthodes PHP __get(). Une exception sera levée si un accès est tenté sur une colonne inexistante.

Info: Dans ce guide, tous les noms des tables et des colonnes sont en minuscules. La raison étant que les SGBD ont différentes manières de gérer la casse. Par exemple, PostgreSQL est par défaut insensible à la casse pour les noms des colonnes, et il faut mettre des guillemets dans les conditions pour les colonnes contenant différentes casses. L'utilisation des minuscules permet d'éliminer ce problème.

AR dépend de la définition correcte des clés primaires sur les tables. Si une table n'a pas de clé primaire, il est nécessaire que la classe AR correspondante spécifie quelle(s) colonne(s) doivent être la clé primaire en surchargeant la méthode primaryKey(),

public function primaryKey()
{
    return 'id';
    // Pour les clés primaires composites, retourne un tableau, par exemple:
    // return array('pk1', 'pk2');
}

3. Création des enregistrements

Pour insérer une nouvelle ligne dans une table, une instance de la classe AR correspondante est créée, les propriétés sont écrites, et la méthode save() est appelée.

$post=new Post;
$post->title='post de test';
$post->content='contenu du post de test';
$post->create_time=time();
$post->save();

Si la clé primaire de la table est en auto incrément, après l'insertion, l'instance AR contiendra la clé primaire mise à jour. Dans l'exemple ci dessus, la propriété id sera égale à la valeur de la clé primaire du nouveau post inséré, même si elle n'est jamais changée implicitement.

Si une colonne a été défini avec une valeur statique par défaut dans le schéma de la table, la propriété correspondante dans l'instance AR aura automatiquement cette valeur suite à la création de cette instance. Une façon de changer cette valeur par défaut est de déclarer explicitement la propriété dans la classe AR:

class Post extends CActiveRecord
{
    public $title='veuillez entrer un titre';
    ......
}
 
$post=new Post;
echo $post->title;  // cela afficherait: veuillez entrer un titre

A partir de la version 1.0.2, une valeur de type CDbExpression peut être assignée à un attribut avant que l'enregistrement soit enregistré (en insertion ou mise à jour) dans la base. Par exemple, pour sauvegarder le timestamp retourné par la fonction MySQL NOW(), il est possible d'utiliser le code suivant:

$post=new Post;
$post->create_time=new CDbExpression('NOW()');
// $post->create_time='NOW()'; ne fonctionnera pas car
// 'NOW()' sera traité comme une chaine de caractères
$post->save();

Tip: bien que AR permette des opérations sans avoir à écrire de requêtes SQL, il est intéressant de savoir quelles requêtes SQL sont exécutées par AR. Pour cela, il est possible d'activer la fonctionnalité de log de Yii. Par exemple, en activant CWebLogRoute dans la configuration de l'application, les requêtes SQL seront affichées à la fin de chaque page web. Depuis la version 1.0.5, il est possible de mettre CDbConnection::enableParamLogging à true dans la configuration de l'application pour que les valeurs en paramètres liées aux requêtes SQL apparaissent aussi dans le log.

4. Lecture des enregistrements

Pour lire des données d'une table, on appel une des méthodes find comme suivant.

// trouve la première ligne satisfaisant les conditions
$post=Post::model()->find($condition,$params);
// trouve la ligne avec la clé primaire spécifiée
$post=Post::model()->findByPk($postID,$condition,$params);
// trouve la ligne avec les valeurs d'attributs spécifiées
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// trouve la première ligne en utilisant la requête SQL spécifiée
$post=Post::model()->findBySql($sql,$params);

Ci dessus, l'appel de la méthode find est effectué au travers de Post::model(). Ne pas oublier que la méthode statique model() est indispensable pour toutes les classes AR. La méthode retourne une instance AR qui est utilisée pour accéder aux méthodes de la classe (de manière similaire aux méthodes des classes statiques) dans un contexte object.

Si la méthode find trouve une ligne satisfaisant les conditions de la requête, elle retournera une instance de Post dont les propriétés contiendront les valeurs des colonnes correspondantes de la ligne de la table. Il est alors possible de lire les valeurs chargées de la même manière que pour un objet classique, par exemple, echo $post->title;.

La méthode find retournera null si rien n'a été trouvé dans la base.

Lors de l'appel à find, on utilise $condition et $params pour spécifier les conditions à la requête. Ici $condition est une string représentant la clause WHERE d'une requête SQL, et $params est un tableau de paramètres dont les valeurs devront être liées aux marques dans $condition. Par exemple,

// trouve la ligne avec postID=10
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

Note: Dans le code ci dessus, il est possible que vous deviez échapper la référence à la colonne postID pour certain SGBD. Par exemple, si vous utilisez PostgreSQL, vous devez écrire la condition "postID"=:postID, car PostgreSQL traite par défaut les colonnes comme insensible à la casse.

Il est aussi possible d'utiliser $condition pour spécifier des conditions plus complèxes. A la place d'une chaine de caractères, $condition pourra être une instance de CDbCriteria, ce qui permettra de spécifier des conditions autres que la clause WHERE. Par exemple,

$criteria=new CDbCriteria;
$criteria->select='titre';  // selectionne seulement la colonne 'title'
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $params n'est pas nécessaire

Notez que quand CDbCriteria est utilisé dans une condition de requête, le paramètre $params n'est plus requit puisqu'il peut être spécifié dans CDbCriteria, comme on peut le voir ci dessus.

Une autre méthode que l'utilisation de CDbCriteria est le passage d'un tableau à la méthode find. Les clés et valeurs du tableau correspondent alors respectivement aux noms et valeurs des critères. Ainsi l'exemple ci dessus peut être réécrit comme suivant,

$post=Post::model()->find(array(
    'select'=>'titre',
    'condition'=>'postID=:postID',
    'params'=>array(':postID'=>10),
));

Info: Quand une condition à pour but de faire correspondre des colonnes avec des valeurs, il est possible d'utiliser findByAttributes(). Le paramètre $attributes est alors un tableau de valeurs avec pour clés les noms des colonnes. Dans certains frameworks, cette tâche est accomplie en appelant des méthodes telles que findByNameAndTitle. Bien que cette approche semble intéressante, elle sème souvent la confusion, et fait apparaitre des conflits et problèmes comme la sensibilité à la casse des noms de colonnes.

Quand plusieurs lignes de données correspondent à la condition de la requête, il est possible de les ramener d'un coup en utilisant les méthodes findAll, chacune d'entres elles possédant sa méthode équivalente find, décrites précédemment.

// trouve toutes les lignes satisfaisant la condition spécifiée
$posts=Post::model()->findAll($condition,$params);
// trouve toutes les lignes possédant les clés primaires spécifiées
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// trouve toutes les lignes possédant les valeurs des attributs spécifiés
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// trouve toutes les lignes en utilisant la requête SQL spécifiée
$posts=Post::model()->findAllBySql($sql,$params);

Si aucun enregistrement ne correspond à la condition de la requête, findAll retournera un tableau vide. Ce comportement est différent en cela de find qui retournera null is aucun enregistrement ne correspond.

En plus des méthodes find et findAll décrites ci dessus, les méthodes suivantes sont aussi incluses:

// retourne le nombre d'enregistrements correspondant à la condition
$n=Post::model()->count($condition,$params);
// retourne le nombre d'enregistrements de la requête SQL spécifiée
$n=Post::model()->countBySql($sql,$params);
// regarde si il y a au moins un enregistrement satisfaisant la condition spécifiée
$exists=Post::model()->exists($condition,$params);

5. Mise à jour d'un enregistrement

Une fois qu'une instance AR contient les valeurs des colonnes, il est possible de les changer et de les sauvegarder dans la table.

$post=Post::model()->findByPk(10);
$post->title='nouveau titre du post';
$post->save(); // enregistrement dans la base

Comme l'on peut le voir, la même méthode save() est utilisée pour les opérations d'insertion et de mise à jour. Si une instance AR est créée en utilisant l'opérateur new, l'appel à save() insèrera une nouvelle ligne dans la table; si l'instance AR provient du résultat de méthodes telles que find ou findAll, l'appel à save() mettrais à jour la ligne existante dans la table. En fait, il est possible d'utiliser CActiveRecord::isNewRecord pour savoir si une instance AR existe ou non.

Il est aussi possible de mettre à jour une ou plusieurs lignes dans une table sans les charger au préalable. AR inclut les méthodes suivantes au niveau de la classe dans ce but:

// met à jour les lignes correspondant à la condition spécifiée
Post::model()->updateAll($attributes,$condition,$params);
// met à jour les lignes correspondant à la condition et aux clés primaires spécifiées
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// met à jour les colonne de comptage pour les lignes satisfaisant les conditions spécifiées
Post::model()->updateCounters($counters,$condition,$params);

Ci dessus, $attributes est un tableau de valeurs de colonnes dont les clés correspondent aux noms des colonnes; $counters est une tableau de valeurs incrémentales dont les clés sont les noms des colonnes; et $condition et $params sont les mêmes que dans les sections précédentes.

6. Suppression des enregistrements

Il est aussi possible de supprimer une ligne de données si une instance AR à été chargée avec cette ligne.

$post=Post::model()->findByPk(10); // on suppose qu'un post avec un ID à 10 existe
$post->delete(); // supprime la ligne dans la table

Attention, après suppression, l'instance AR reste inchangée, même si la ligne correspondante dans la table à déjà été effectivement supprimée.

Les méthodes suivantes de la classe sont incluses afin de pouvoir supprimer des lignes sans les avoir chargées auparavant:

// supprime les lignes correspondant à la condition spécifiée
Post::model()->deleteAll($condition,$params);
// supprime les lignes correspondant à la condition et aux clés primaires spécifiées
Post::model()->deleteByPk($pk,$condition,$params);

7. Validation des données

Durant une insertion ou mise à jour d'une ligne, il est souvent nécessaire de vérifier si les valeurs des colonnes suivent bien certaines règles. C'est particulièrement important si ces valeurs proviennent des utilisateurs finaux. En général, il ne faut jamais faire confiance aux données provenant du coté client.

AR va effectuer cette validation de données automatiquement quand la méthode save() est appelée. La validation se base sur les règles définies dans la méthodes rules() de la classe AR. Pour plus de détails sur comment écrire ces règles de validation, veuillez consulter la section Declaration des règles de validation. Voici ci dessous le déroulement typique lors de l'enregistrement d'une ligne:

if($post->save())
{
    // données valides et insertion/mise à jour sans erreur
}
else
{
    // données invalides. Un appel à getErrors() retournera les messages d'erreur
}

Quand les données à insérer ou mettre à jour sont fournis par les utilisateurs finaux sous forme HTML, il faut les assigner aux propriétés correspondantes de l'AR. Voici comment il est possible de faire cela:

$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();

Si il y avait de nombreuses colonnes, il faudrait faire de nombreuses assignations. Cela peut être évité en utilisant la propriété attributes comme montré ci dessous. Plus de détails sont disponibles dans les sections Securing Attribute Assignments et Creating Action.

// on suppose que $_POST['Post'] est un tableau de valeurs
// dont les clés sont les noms de colonnes
$post->attributes=$_POST['Post'];
$post->save();

8. Comparaison d'enregistrements

Tout comme les enregistrements d'une table, les instances AR sont identifiées de manière unique grâce à la valeur de leur clé primaire. Ainsi, afin de comparer deux instances AR, il suffit de comparer leur clé primaire. Cependant une façon plus simple est d'appeler la méthode CActiveRecord::equals().

Info: A la différence des implémentations de AR dans les autres frameworks, Yii supporte les clés primaires composites dans les AR. Une clé composite est consistuée de deux colonnes ou plus. Dans ce cas, la valeur de la clé primaire est représentée dans Yii comme un tableau. La propriété primaryKey renvoie la valeur de la clé primaire dans une instance AR.

9. Cas spécifiques

CActiveRecord fourni des méthodes qu'il est possible de surcharger pour modifier le comportement de la classe.

  • beforeValidate et afterValidate: invoquées avant et après la validation.

  • beforeSave and afterSave: invoquées avant et après l'enregistrement d'une instance AR.

  • beforeDelete and afterDelete: invoquées avant et après la suppression d'une instance AR.

  • afterConstruct: invoquée pour chaque instance AR créée avec l'opérateur new.

  • beforeFind: invoquée avant que le AR finder exécute une requête (par exemple find(), findAll()). Disponible depuis la version 1.0.9.

  • afterFind: invoquée après chaque création d'instance AR issue du résultat d'une requête.

10. Utilisation des transactions avec AR

Chaque instance AR contient une propriété nommée dbConnection qui est une instance de CDbConnection. Il est donc possible d'utiliser la fonctionnalité de transaction fourni par Yii DAO si besoin:

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // find et sqve sont des actions qui peuvent se produirent dans une autre requête
    // il faut donc utiliser une transaction pour garantir l'intégrité des données
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
}
catch(Exception $e)
{
    $transaction->rollBack();
}

11. Named Scopes

Note: Le support pour les named scopes est disponible depuis la version 1.0.5. L'idée originelle des named scopes provient de Ruby on Rails.

Un named scope représente un critère named d'une requête qui peut etre combiné avec d'autres named scopes et appliqués ensemble dans une requête sur un active record.

Les Named scopes sont normalement déclarés dans la méthode CActiveRecord::scopes() comme des paires de noms-critères. Le code suivant déclare deux named scopes, published et recently, dans la classe modélisant Post:

class Post extends CActiveRecord
{
    ......
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'create_time DESC',
                'limit'=>5,
            ),
        );
    }
}

Chaque named scope est déclaré comme un tableau qui est utilisé pour initialiser une instance CDbCriteria. Par example, le named scope recently indique que la propriété order sera create_time DESC et que la propriété limit sera 5, ce qui donne un critère de requête qui retournera les 5 posts les plus récents.

Les Named scopes vont principalement être utilisés pour altérer les appels aux méthodes find. Plusieurs named scopes peuvent être chainés ensemble pour restreindre les résultats de la requête. Par exemple, pour trouver les posts récemment publiés, il est possible de faire:

$posts=Post::model()->published()->recently()->findAll();

En général, les named scopes doivent se situer à gauche de l'appel à la méthode find. Chacun fournit un critère pour la requête, qui sera combiné avec les autres critères, dont celui passé en paramètre de la méthode find. Finalement cela revient à ajouter une liste de filtre à la requête.

A partir de la version 1.0.6, les named scopes peuvent aussi être utilisés avec les méthodes update et delete. Par exemple, le code suivant supprime tous les posts récemment publiés:

Post::model()->published()->recently()->delete();

Note: Les Named scopes peuvent seulement être utilisé avec les méthodes du niveau de la classe. Ainsi la méthode doit être appelée en utilisant ClassName::model().

Named Scopes avec paramètres

Les Named scopes sont paramètrables. Par exemple, nous pourrions avoir besoin de configurer le nombre de posts indiqué dans le named scope recently. Pour cela, au lieu de déclarer le named scope dans la méthode CActiveRecord::scopes, il faut le faire dans une nouvelle méthode dont le nom est le même que celui du named scope:

public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'create_time DESC',
        'limit'=>$limit,
    ));
    return $this;
}

Il est alors possible d'utiliser le code ci dessous pour récupérer les 3 posts récemment publiés:

$posts=Post::model()->published()->recently(3)->findAll();

Si le paramètre 3 ci dessus n'est pas spécifié, les 5 derniers posts publiés serait renvoyés par défaut.

Named Scope par défaut

Une classe peut avoir un named scope par défaut qui serait appliqués à toutes les requêtes (dont les relationelles) sur un modèle. Par exemple, un site web supportant plusieurs langues souhaiterait peut etre ne vouloir afficher que le contenu correspondant au langage que l'utilisateur à défini. Comme il se peut qu'il y ait beaucoup de requêtes concernant le contenu du site, il est possible de définir un named scope par défaut pour régler ce problème. Pour cela, il faut surcharger la méthode CActiveRecord::defaultScope comme ci dessous,

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

Tous les appels aux méthodes find utiliseront automatiquement le critère défini ci dessus:

$contents=Content::model()->findAll();

Attention car les named scope par défaut ne s'applique que sur les requêtes SELECT. Ils sont ignorés pour les requêtes INSERT, UPDATE et DELETE.

$Id: database.ar.txt 1681 2010-01-08 03:04:35Z qiang.xue $

Total 1 comment

#17845 report it
Leto at 2014/07/29 06:18am
Constructeur

Attention si vous redéfinissez le constructeur de votre modèle à bien remettre le $scenario par défaut à "insert" et à appeler le constructeur parent comme ceci:

public function __construct($scenario = 'insert')
    {
        parent::__construct($scenario);
    }

Si le scénario est à null par exemple, ou que vous n'appelez pas la méthode parente, votre entité ne sera pas enregistrée en base de donnée, car yii ne l'aura pas définie comme "isNewRecord"..

Leave a comment

Please to leave your comment.