Accès aux données

Eléphant du PHP | 219 Messages

13 mai 2005, 19:08

Bonjour,

j'ai pas mal travaillé sur mon dernier projet en PHP5 sur l'accès aux données. Suite à ce travail je me suis dit tout d'abord qu'il serait intéressant de partager mon expérience, et j'ai donc proposé à Damien d'écrire un tutoriel. Mais avec un peu de recul je me suis dit que cela était prématuré et un peu prétentieux de ma part étant donné mon expérience.
Ayant discuté avec Cyrano hier, notament sur ce thème, je lui ai exprimé mon désir d'approfondir ce sujet, et s'il pensait que cela était une bonne idée de lancer une discussion à ce sujet. Il m'a suggéré de faire cela dans le forum des développeurs.
Me voilà donc et j'espère avoir vos réactions.

La première idée c'est créer une couche d'abstraction à la base de données, afin de ne pas rendre son code dépendant d'un SGBD spécifique. J'ai donc créé une interface ISgbd qui spécifie toutes les méthodes que l'on doit attendre d'un SGBD :
interface ISgbd {
	/*
	* Effectue une requête sur la base de données.
	*/
	public function query($sql);
	/*
	* Retourne l'enregistrement du resultset
	*/
	public function getRow();
	/**
	* Retourne un enregistrement sous forme d'objet
	* @return object
	*/
	public function getObject();
	/*
	* Retourne un enregistrement sous forme de tableau
	*/
	public function getArray();
	/*
	* Retourne un enregistrement sous forme de tableau associatif
	*/
	public function getAssoc();
	/*
	* Retourne le nombre d'enregistrements du resultset
	*/
	public function getNumRows();
	/*
	* retourne une valeur formattée pour être intégrée au SGBD
	*/
	public function getSQLValue($value, $type);
	/*
	* Retourne l'id du dernier élément inséré
	*/
	public function getLastInsertId();
}
Ensuite pour chaque SGBD on réalise cette interface, ici pour mysql avec mysql_... :
class  MysqlDb implements ISgbd{
	private $cnx = null;
	private $result = null;
	/*
	* Constructeur. Démarrage de la connexion.
	*/
	function __construct($server, $user, $password, $base){
		$this->cnx = mysql_connect($server, $user, $password);
		if($this->cnx == false){
			die("Impossible d'ouvrir la connexion à la base");
		}
		mysql_select_db($base, $this->cnx);
	}
	/*
	* Effectue une requête sur la base de données.
	*/
	public function query($sql){
		$this->result = false;
		if($this->cnx != false){
			try {
				$this->result = mysql_query($sql, $this->cnx);
			}catch (Exception $e) {
				echo "erreur : " . $e->getMessage();
			}
		}
		return $this->result;
	}
	/*
	* Retourne l'enregistrement du resultset
	*/
	public function getRow() {
		$row = null;
		if($this->result != false)
			$row = mysql_fetch_row($this->result);
		return $row;
	}
	...
}
Ensuite pour l'accès aux données, à partir du design pattern DataAccessObject je propose la structure suivante :
Une classe abstraite qui sera héritée par toutes les classes qui permettent l'accès aux données. Chaque classe (ou DAO) représente une table en base de données.
abstract class AbstractDAO {
	/**
	* le type du sgbd, un objet de type IDb
	*/
	protected $db;
	/*
	* Le nom de la table
	*/
	protected $nomTable;
	/*
	* La liste des champs sous forme de tableau
	*/
	protected $arrayChamp;
	/*
	* La liste des types sous forme de tableau
	*/
	protected $arrayType;
	/*
	* Cette méthode permet d'insérer un enregistrement dans la table, 
	* l'id est inséré automatiquement par le sgbd
	*/
	public function insertAutoNum($arrayVal){
		$sql = "INSERT INTO " . $this->nomTable . " (";
		for($i = 0; $i < count($this->arrayChamp); $i++){
			if($i != 0){
				if($i != 1)
					$sql .= ",";
				$sql .= $this->arrayChamp[$i];		
			}
		}
		$sql .= ") VALUES(";
		for($i = 0; $i < count($arrayVal); $i++){
			if($i != 0)
				$sql .= ",";
			$sql .= $this->db->getSQLValue($arrayVal[$i], $this->arrayType[$i + 1]);
		}
		$sql .= ")";
		$this->db->query($sql);
		return $this->db->getLastInsertId();
	}
	/*
	* Cette méthode permet d'insérer un enregistrement dans la table
	* l'id est entré manuellement
	*/
	public function insertAvecId($arrayVal){
		$sql = "INSERT INTO " . $this->nomTable . " (";
		for($i = 0; $i < count($this->arrayChamp); $i++){
			if($i != 0)
				$sql .= ",";
			$sql .= $this->arrayChamp[$i];	
		}
		$sql .= ") VALUES(";
		for($i = 0; $i < count($arrayVal); $i++){
			if($i != 0)
				$sql .= ",";
			$sql .= $this->db->getSQLValue($arrayVal[$i], $this->arrayType[$i + 1]);
		}
		$sql .= ")";
		$this->db->query($sql);
		return $this->db->getLastInsertId();
	}
	/*
	* Retourne un tableau associatif
	*/
	public function select($criteres){
		$sql = "SELECT * FROM " . $this->nomTable;
		$where = "";
		if(count($criteres != 0)){
			$where = " WHERE 1 ";
			foreach($criteres as $critere){
				$where .= " AND " . $critere;
			}
		}
		$orderBy = " ORDER BY " . $this->arrayChamp[0] . " DESC";
		$this->db->query($sql . $where . $orderBy);
		$assoc = $this->db->getAssoc();
		if($assoc == false)
			return null;
		else
			return $assoc;
	}
	/*
	* Efface tous les enregistrements correspondants aux critères
	*/
	public function delete($criteres){
		$sql = "DELETE FROM " . $this->nomTable;
		$where = "";
		if(count($criteres) != 0){
			$where = " WHERE 1 ";
			foreach($criteres as $critere){
				$where .= " AND " . $critere;
			}
		}
		$this->db->query($sql . $where);
	}
        etc...
}
Puis chaque DAO hérite de cette classe. ex :
class SiteDAO extends AbstractDAO{
	
	function __construct(ISgbd $db){
		$this->db = $db;
		$this->nomTable = "Site";
		$this->arrayChamp = array("idSite", "libelle", "domaine");
		$this->arrayType = array("int", "text", "text");
	}
}
Ainsi avec chaque objet qui hérite de AbstractDAO on peut faire toutes les méthodes de base que l'on a défini. Ensuite on ajoute dans chaque classe DAO les méthodes dont on peut avoir besoin, des méthodes spécifiques (notament pour les requêtes avec jointure).
Ici se rejoignent l'abstraction à la base et l'accès aux données, car le DAO reçoit en paramètre de son constructeur un objet de type ISgbd. Il n'est donc pas dépendant d'une base.
Après, pour la mise en oeuvre de tout cela, il y a sûrement plusieurs possibilités, la plus simple étant à mon avis de laisser en état. Ex :
$database = new MysqlDb($server, $user, $password, $base);
$siteDAO = new SiteDAO($database);
$siteDAO->select(array("idSite=1", "domaine='www.phpfrance.com'"));
Dans mon projet j'ai utilisé le design pattern factory, qui permet de déléguer à une classe la création des objets DAO, à voir.

Voilà, j'espère que ce n'est pas trop confus. J'aimerais donc obtenir vos remarques sur ce qui parait mal conçu, inefficace,... donc surtout au niveau conception dans un premier temps (la gestion des erreurs n'est pas bien faites et sera a améliorer par la suite).

merci :)
daoud
Modifié en dernier par daoud le 16 mai 2005, 11:03, modifié 1 fois.

Eléphant du PHP | 52 Messages

16 mai 2005, 10:04

Bonjour,

De part mon boulot j'ai moi aussi été amené à écrire des classes de manipulations de bases, le but étant également de s'affranchir du type de base.

En plus de ce que tu as fait, il faudrait également une fonction supplémentaire:

- Lorsque l'on veut récupérer des données avec une clause LIMIT, la encore cette syntaxe n'existe pas avec SQL Server donc il faut faire abstraction de la syntaxe propre à chaque SGBD.

ViPHP
ViPHP | 1024 Messages

16 mai 2005, 11:37

de mon coté, ma classe n'a qu'une seule fonction pour exécuter les requetes et ça marche dans les 2 cas essayés: MySQL et SQL Server.

A+

Pascal

Eléphant du PHP | 52 Messages

16 mai 2005, 11:45

de mon coté, ma classe n'a qu'une seule fonction pour exécuter les requetes et ça marche dans les 2 cas essayés: MySQL et SQL Server.

A+

Pascal

Et les requetes avec une clause LIMIT fonctionnent correctement ?

Eléphant du PHP | 219 Messages

16 mai 2005, 12:00

Salut :)

Je me demande dans quelle mesure on peut gérer le LIMIT. En effet il n'est pas dans les normes :evil: et cela est très problématique, car il est difficile de s'en passer dans certains cas ! Mais, il n'y a pas forcément d'équivalent pour tous les sgbd, par exemple avec MSServer on peut utiliser TOP, mais cela ne limite que le nombre, et ne permet pas de commencer où l'on veut...
Une idée :?:

daoud

Eléphant du PHP | 52 Messages

16 mai 2005, 12:07

Salut :)

Je me demande dans quelle mesure on peut gérer le LIMIT. En effet il n'est pas dans les normes :evil: et cela est très problématique, car il est difficile de s'en passer dans certains cas ! Mais, il n'y a pas forcément d'équivalent pour tous les sgbd, par exemple avec MSServer on peut utiliser TOP, mais cela ne limite que le nombre, et ne permet pas de commencer où l'on veut...
Une idée :?:

daoud
Ben je dirais que quand tu veux utiliser une requete avec limit, tu n'utilise pas la fonction classique de ta classe mais une fonction spéciale type : execRequeteAvecLimite($requete, $start, $nb)
Et cette fonction te retourne tous les résultats voulus (sans passer manuellement par fetchRow ou fetchArray de la classe).
Donc si t'as une bdd MySQL, ca utilisera le limit, si t'as une bdd SQL Server, ca fait la requete sans clause limitante, ca récupère tous les résultats et ca ne te renvoit que ceux qui correspondent à tes critères de limites.

ViPHP
ViPHP | 1024 Messages

16 mai 2005, 12:13

de mon coté, ma classe n'a qu'une seule fonction pour exécuter les requetes et ça marche dans les 2 cas essayés: MySQL et SQL Server.

A+

Pascal

Et les requetes avec une clause LIMIT fonctionnent correctement ?
oui, ce sont des requetes comme les autres. je ne comprends pas pourquoi on devrait séparer les insert et compagnie... ?

on exécute la requete et on récupère les infos, rien de plus.

A+

Pascal

Eléphant du PHP | 219 Messages

16 mai 2005, 12:17

Oui, c'est sûrement ce qu'il y a de mieux, donc :
- ajout d'un fonction avec limite sur le nombre d'enregistrements
- ajout d'une fonction avec limite sur le nombre d'enregistrements et index de départ.

je découvre tous les jours les joies du polymorphisme ;)
merci
daoud

Eléphant du PHP | 52 Messages

16 mai 2005, 12:18

de mon coté, ma classe n'a qu'une seule fonction pour exécuter les requetes et ça marche dans les 2 cas essayés: MySQL et SQL Server.

A+

Pascal

Et les requetes avec une clause LIMIT fonctionnent correctement ?
oui, ce sont des requetes comme les autres. je ne comprends pas pourquoi on devrait séparer les insert et compagnie... ?

on exécute la requete et on récupère les infos, rien de plus.

A+

Pascal
Je ne parle pas de séparer les différents types de requete, mais une clause LIMIT n'est pas une clause standard (tout comme FROM_UNIXTIME et UNIX_TIMESTAMP ne sont pas fonctionnels avec une base SQL SERVER il me semble).

Il me semblait avoir fait le test et vu qu'une requete avec une clause LIMIT ne marchait pas sur une base SQL SERVER..

Eléphant du PHP | 52 Messages

16 mai 2005, 12:20

Oui, c'est sûrement ce qu'il y a de mieux, donc :
- ajout d'un fonction avec limite sur le nombre d'enregistrements
- ajout d'une fonction avec limite sur le nombre d'enregistrements et index de départ.

je découvre tous les jours les joies du polymorphisme ;)
merci
daoud
Pourquoi ajouter 2 fonctions ? une seule marcherait dans les 2 cas non ?

Eléphant du PHP | 219 Messages

16 mai 2005, 12:23

Bien sûr. Faut que je réflechisse parfois avant de poster :oops:

daoud

ViPHP
ViPHP | 1024 Messages

16 mai 2005, 12:27

Je ne parle pas de séparer les différents types de requete, mais une clause LIMIT n'est pas une clause standard (tout comme FROM_UNIXTIME et UNIX_TIMESTAMP ne sont pas fonctionnels avec une base SQL SERVER il me semble).

Il me semblait avoir fait le test et vu qu'une requete avec une clause LIMIT ne marchait pas sur une base SQL SERVER..
Avoir du standard à 100% est peut être possible, mais est-ce fonctionnel de devoir passer par un tableau de valeur pour faire un insert ? toujours faire "SELECT *" ?

quel est le positionnement de la classe par rapport au mapping objet-relationnel?

A+

Pascal

Eléphant du PHP | 52 Messages

16 mai 2005, 12:33

Je ne parle pas de séparer les différents types de requete, mais une clause LIMIT n'est pas une clause standard (tout comme FROM_UNIXTIME et UNIX_TIMESTAMP ne sont pas fonctionnels avec une base SQL SERVER il me semble).

Il me semblait avoir fait le test et vu qu'une requete avec une clause LIMIT ne marchait pas sur une base SQL SERVER..
Avoir du standard à 100% est peut être possible, mais est-ce fonctionnel de devoir passer par un tableau de valeur pour faire un insert ? toujours faire "SELECT *" ?

quel est le positionnement de la classe par rapport au mapping objet-relationnel?

A+

Pascal
On a mal du se comprendre.
Pour ma part ma SEULE remarque concernait le fait que tu annonces qu'une clause LIMIT fonctionne sous SQL SERVER et c'est ce qui me surprend.

Pour le reste, effectivement il faudrait le bonne équilibre entre rendre le code générique et lourdeur d'execution.

En ce qui concerne les select * c'est facile de s'en affranchir:
Distinguer 2 fonctions dans la classe mere:
-une qui permet de récupérer une liste d'élements
-une qui permet de récupérer les infos sur 1 élement.

La première reconstruit le select à partir du variable de la classe contenant tous les champs à récupérer lorsque l'on veut lister les élements, et une seconde variable utilisée pour le 2eme cas qui contient la listes des champs à récupérer pour les infos sur un élément.
Modifié en dernier par julien le 16 mai 2005, 12:52, modifié 1 fois.

Eléphant du PHP | 219 Messages

16 mai 2005, 12:44

Salut pascaltje,

Je ne prétends pas avoir présenté quelque chose d'abouti. Je lance un débat, une reflexion sur ce thème et je suis preneur de tous les avis pour m'aider à ce sujet. Donc oui il y a beaucoup de choses à faire et améliorer, et l'idée est justement d'avancer, de modifier,...
mais est-ce fonctionnel de devoir passer par un tableau de valeur pour faire un insert ? toujours faire "SELECT *" ?
Que proposes-tu ?
quel est le positionnement de la classe par rapport au mapping objet-relationnel?
Peux-tu détailler ?

Merci pour tes apports ;)

daoud

ViPHP
ViPHP | 1024 Messages

16 mai 2005, 13:36

oups, on s'est mal compris:

je disais qu'une clause limit marchait parfaitement en faisant
$sql->query($requete)
pour MySQL.

pour SQL Server, la syntaxe "SELECT TOP n champs" marche aussi en requete simple.

ce que je propose c'est:
- un constructeur qui se connecte au serveur et sélectionne la base, via des données de config passées en global (serveur, login, pass et base)
- une méthode pour exécuter une requete
- des méthodes pour avoir les résultats
- quelquechose pour compter le nombre de résultats
et c'est tout.

(KISS: Keep It Simply Stupid, faisons simple! )
après on peut étoffer avec des méthodes de débuggage etc... .

pour le mapping objet relationnel, c'est une architecture qui fait correspondre objet <=> table (link: http://www.dotnetguru.org/articles/Pers ... apping.htm )

ça permet de séparer le code pour des sites avec DB:
dans les pages on ne met pas de code procédural, mais une création d'un objet puis un appel aux méthodes qui vont bien (remplir l'objet avec les données POST, tester la validité des données, enregistrer/supprimer les données )

ça allège le code, centralise la définition des traitements et permets de modifier le code rapidement (avec mes utilisateurs qui changent d'avis comme de chemise, c'est indispensable).

A+

Pascal