Návrh databázové třídy - díl II: Základní metody

 |  PHP pro pokročilé  |  4 051x
Objekty v PHP5 - Návrh databázové třídy - díl II: Základní metody

V úvodu svého seriálu o databázi a objektech jsem nakousnul základní témata, kterými bude směřovat postup realizace. V pokračování uvedu elementární metody na nečastější typy SQL dotazů. Naším cílem bude již zmiňované omezení otravných operací spolu s maximálním zjednodušení zápisů konkrétních typů dotazů. Nepodstatný výčet členských proměnných přeskočím a přejdu rovnou ke konstruktoru třídy.

Obecné nastavení

Než se vůbec dostaneme k samotné práci s daty, je potřeba nastavit potřebné parametry a provést spojení. U jednotlivých knihoven s oblibou využívám kombinaci nastavení přes globální proměnné a metody typu set. Princip je takový, že nastavení, které je společné pro všechny potencionální instance dané třídy nechám v globální proměnných, naproti tomu specifická nastavení uložím do členských proměnných až po vytvoření instance. V našem případě ale zůstanu pouze u globálních proměnných, a to z důvodu bezpečnosti, jelikož chceme používat výjimky v PHP5 a nechceme riskovat nechtěný výpis hesla k databázi.

Parametrem v tomto případě bude klíč ke globálnímu poli, kde bude veškeré nastavení uloženo. Nastavím všechny 4 potřebné údaje, kódování, prefix tabulek a znění SQL dotazu pro porovnávání.

public function __construct($key = 'db'){
	global $config;
	
	try {
		if(!empty($config[$key]['connected'])){
			throw new DbException('Connection from $config['.$key.'] already exists.');
		}
		
		if(!isset($config[$key]['host'], $config[$key]['user'], $config[$key]['pass'], $config[$key]['name'])){
			throw new DbException('One of global vars $config['.$key.'][host] // user | pass | name is undefined.');
		}
		
		$this->database = $config[$key]['name'];
		
		$this->connection = @mysql_connect($config[$key]['host'], $config[$key]['user'], $config[$key]['pass']);
		if(!$this->connection){
			throw new DbException();
		}
		
		$this->selectedDb = @mysql_select_db($this->database, $this->connection);
		if(!$this->selectedDb){
			throw new DbException();
		}
		
		$this->magicQuotes = (get_magic_quotes_gpc() ? true : false);
		$this->setNames = (!empty($config[$key]['charset']) ? $config[$key]['charset'] : 'SET NAMES utf8');
		$this->prefix = (!empty($config[$key]['prefix']) ? $config[$key]['prefix'] : '');
		
		// pokud tabulka obsahuje jak klic [name] tak klic [title]
		// pokud je title prazdny, priradi se hodnota sloupce name
		// kvuli SEO : name pro <h1> title pro <title> aby mohly byt odlisne
		if(isset($config[$key]['title_replace'])){
			Query::$titleReplace = (bool)$config[$key]['title_replace'];
		}
		
		$this->query($this->setNames);
		
	}catch(Exception $e){
		$e->show();
	}
	
	// reset aby se v nejakem dumpu nahodou neobjevilo heslo k databazi
	$config[$key]['host'] = 'true';
	$config[$key]['user'] = 'true';
	$config[$key]['pass'] = 'true';
	$config[$key]['name'] = 'true';
	$config[$key]['connected'] = true;
}

Od pohledu je asi jasné, co všechno konstruktor řeší. O některých funkcích zde ale vůbec mluvit nebudu, a ušetřený čas raději věnuji podrobnějšímu popisu, proč tohle všechno děláme: automatizace, ušetření práce. Jen ještě připomenu, že řádek, ve kterém se zjišťuje aktuální nastavení magických uvozovek si bere za úkol kompletně se postarat o tuto problematiku. Nás už následně nebude zajímat žádné slashování či escapování.

Dotazy typu SELECT

Právě metoda Db::query() bude zajišťovat veškeré dotazy na databázi. Provede náhradu prefixu tabulek, ošetří veškerý vstup proti MySQL Injection a čistá data pošle třídě Query. Mimo to i změří kolik času SQL dotaz sebral. Automatické ošetření vstupu provedeme pomocí takzvaných placeholderů, které budou ve funkci nahrazeny.

public function query($query, $pholders = null, $replacePrefix = true){
	$time = $this->startTime();
	if($replacePrefix){
		$query = $this->replacePrefix($query);
	}
	
	if(!empty($pholders)){
		$q = explode('?', $query);
		$count = count($pholders);
		$query = '';
		for ($i = 0; $i < $count; $i++){
			$query .= $q[$i].$this->escape($pholders[$i]);
		} 
		$query .= $q[$i];
	}

	$result = new Query($this, $query);
	$this->queries[] = $result.'; '.$this->stopTime($time);
	return $result;
}

Díky tomu dosáhneme naprosto jednoduchého zápisu:

$q = $db->query("SELECT * FROM ?_tabulka WHERE id = '?' AND category_id = '?'", array($id, $category_id));

Nyní k vysvětlení otazníků: ?_ znamená prefix tabulek. Význam začíná mít právě ve chvíli, kdy nemáme možnost vytvoření nové databáze pro projekt na stejném systému. A samotný otazník už je placeholder, obdobně jako například v PDO. Metody startTime(), stopTime() a replacePrefix() doufám popisovat nemusím.

Trojice funkcí pro dotazy typu INSERT, UPDATE a DELETE

Jak napoví nadpis, funkce si budou něčím podobné. V tomto případě totiž už nebudeme psát celé znění dotazu, ale pouze předáme jméno tabulky, pole s daty a případnou podmínku.

Vložení nového řádku

Jak jsem slíbil, tak učiním. Nejjednodušší z nadepsaných funkcí, metoda Db::insert() bere 2 parametry: jméno tabulky + pole s hodnotami a vrací ID vloženého záznamu. Je samozřejmě nutné, aby seděly klíče a jména sloupců vůči tabulce.

public function insert($table, array $items){
	try {
		if(empty($items)){
			throw new DbException('No values to insert.', var_export($items, true));
		}
		
		$keys = array();
		$vals = array();
		
		foreach ($items as $key => $t){
			if($t === 0 || $t === '0'){
				$vals[$key] = "'0'";
				$keys[]     = "`{$key}`";
			}elseif(!empty($t)){				
				$vals[$key] = "'".$this->escape($t)."'";
				$keys[]     = "`{$key}`";
			}
		}

		$qtable  = $this->replacePrefix($table);
		$final_query     = "INSERT INTO {$qtable} ( ".implode("\n,", $keys)." ) VALUES ( ".implode(",\n ", $vals)." )";
		
		$insertId = $this->query($final_query, array(), false)->insertId();
		
		return $insertId;
	}catch(Exception $e){
		$e->show();
	}
}

UPDATE

Nejčastěji prováděná aktualizace záznamu je jeden řádek podmíněný jedním ID. Právě tuto akci si automatizujeme. Výsledek bude obdobná funkce akorát s odlišně vygenerovaným řetězcem.

public function update($table, $id, array $items, $col = 'id'){
	try {
		if(empty($items)){
			throw new DbException('No values to update.', var_export($items, true));
		}
		if(empty($id)){
			throw new DbException('No id.', var_export($id, true));
		}
		
		$update = array();
		foreach ($items as $key => $item){
			$update[$key] = "`{$key}` = '".$this->escape($item)."'";
		}
		
		$qtable = $this->replacePrefix($table);
		$final_query     = "UPDATE {$qtable} SET ".implode(",\n ", $update)." WHERE `{$col}` = '{$id}'";
		
		return $this->query($final_query, array(), false)->affectedRows();
	}catch(Exception $e){
		$e->show();
	}
}

DELETE

Mazání přecijen provádíme méně často než vkládání nových řádků a jejich editace, a tak by nám do základu měla stačit metoda, která bude mazat řádek / řádky pouze s porovnáním v podmínce. Metoda vezme dva argumenty: jméno tabulky a pole s podmínkou.

public function delete($table, array $cond){
	try {
		if(empty($cond)){
			throw new DbException('No condition.', var_export($cond, true));
		}
		
		$c = array();
		foreach ($cond as $key => $t){
			$c[] = "`{$key}` = '".$this->escape($t)."'";
		}
		
		$qtable = $this->replacePrefix($table);
		$string = implode(' AND ', $c);
		$final_query     = "DELETE FROM {$qtable} WHERE {$string}";

		return $this->query($final_query, array(), false)->affectedRows();
	}catch(Exception $e){
		$e->show();
	}
}

Přínos

Ač se výše uvedené metody mohou zdát na první pohled zbytečně složité, splňují na 100% vše, co od nich očekáváme a navíc zcela správně implementují princip knihovny: složité vnitřní operace s vlastním reportem chyb a jejich jednoduchá aplikace. Po vyladění těchto metod (což už samozřejmě vyladěné mám) nás tělo funkce přestane zajímat a budeme se starat pouze o vstup a návratové hodnoty. A pokud si honem nevzpomeneme, jaké že to argumenty musíme předat, mrkneme na interface.

Pro náročnější dotazy už je samozřejmě třeba volat klasickou metodu Db::query(), ale existují věci, které prostě zobecnit nelze.

Ukázka

$insert_id = $db->insert('?_categories', array(
	'name' => $_POST['name'],
	'text' => $_POST['text'],
));

$db->update('?_categories', $_GET['id'], array(
	'name' => $_POST['name'],
	'text' => $_POST['text'],
));

$deleted = $db->delete('?_articles', array(
	'parent_id' => 1,
));
Facebook Twitter Google+

Komentáře k článku "Návrh databázové třídy - díl II: Základní metody"

Gravatar
Petr Kramář 22. 7 2010, 15:22
1/4 Čtvrtek 22. Července 2010, 15:22  |  Chrome, Windows 7

Ahoj Michale,
já ti moc nejsem nadšený z toho tvýho užití try, catch v takových třídách...jsou to obecné třídy na které by se dál v projektu sahat nebude a není žádoucí aby DbException::show() vyhazovala nějaký výstup, ano určitě si jí každý může modifikovat aby dělala to co chce ale je tam naprosto zbytečně, protože každý kdo dělá webové aplikace a uživá v nich objekty tak případné výjímky si odchytává nějakým handlerem, který to buď jen tak uloží do nějakého logu nebo zašle na e-mail, ale tím že ty je zachytíš okamžitě ve své třídě znemožňuješ aby je nějaký defaultní handler zpracoval, čili DbException bych osobně asi také udělal, ale jen ve stylu "class DbException extends Exception {}", ať v logu můžeš jasně identifikovat kde se nějakej průser stal.

Gravatar
Mike 22. 7 2010, 18:25
2/4 Čtvrtek 22. Července 2010, 18:25  |  Opera, Windows XP

@: Ahoj, díky za trefný komentář. Máš naprostou pravdu v tom co říkáš, ale troufnu si říct, že je to trochu utopie. Abych svou myšlenku trochu rozvedl:

* v případě, že už se projekt stará o zpracování všemožných chyb svým vlastním handlerem, předpokládám, že má vyřešenou už i databázi - tam bych tedy svou třídu nenasazoval.

* ostatní projekty, se kterými jsem se setkal byly zpravidla šílené slátaniny, kde vývojáři takové "malichernosti", jako je odchytávání vyjímek vůbec neřešili.

* závislost na error handleru tam samozřejmě mám, právě v metodě DbException::show() Až v něm probíhá zaslání informací o chybě na email, popřípadě logování - samozřejmě se z toho dozvím vše - znění dotazu, soubor a číslo řádku, znění chyby. Navíc ale také chci, aby mi to vypisovalo nějaké chyby, pokud error handler nenajde.

* další možnost také je, že try { } catch { } vůbec obsaženo nebude a třída bude jen vyhazovat vyjímky - to jsem ale nechtěl, protože chci mít použití co nejjednodušší a nemyslet na to, aby vše bylo v try - právě pro případy, kdy nasazuji třídu nějakou výše zmíněnou slátaninu.

Metodu show() tu zatím uvedenou nemám, vypadá takto (viz níže). Právě umožňuje dvojí zpracování chyb - výchozí (proto tam ta třída je) + pokročilé, závislé na další knihovně.

public function show() {
if (defined('USER_ERROR_HANDLER')) {
handle_db_error($this, $this->mysqlError, (int)$this->mysqlErrno, $this->errString);
} else {
$output = '';
$output .= '<pre style="border:1px solid red;background:#fff;font-size:13px;padding:10px;font-family:\'Courier New\';">';
$output .= ($this->mysqlError ? '<em>'.$this->mysqlErrno.': '.$this->mysqlError."</em>\n\n" : '');
$output .= (parent::getMessage() ? '<em>message: '.parent::getMessage()."</em>\n\n" : '');
$output .= '<strong>Query</strong>: '.$this->errString."\n\n";
$output .= parent::getTraceAsString();
$output .= '</pre>';
}

@header('HTTP/1.1 500 Internal Server Error');

exit($output);
}

Gravatar
Petr Kramář 23. 7 2010, 00:43
3/4 Pátek 23. Července 2010, 00:43  |  MSIE, Windows XP

Snad vyargumentuju /<< to je otřesný slovo/ většinu z tvých hvězdiček, kromě tý druhý :-)

ad 1.*
Tyto třídy sem považoval za tvou jakousi sbírku tříd nevím v jakém to máš stavu možná už tomu sám říkáš framework :-) tak je hloupé tam mít try, catch aby ti to obsluhoval nějaký lokální nástroj v rámci té třidy, když to může udělat nějaký globální nástroj v rámci celé tvé "sbírky".

ad 3.*
závislost lokálního error handleru v rámci té třídy právě není žádoucí, protože ti to právě cpe try, catch do skriptu tam kde si myslím, že se to absolutně nehodí a taky to prostě může spíš měl řešit onen globální handler.

ad 4.*
když tam try, catch nebude což je dle mě naprosto správně tak, a o nějakém obalovaní následného produkčního kódu nemůže být řeč, protože onen "globální odchytávač" nastavíš pomocí set_exception_handler(); a do něj vlezou vlastně všechny nezachycené vyjímky, které následně zpracuješ zaloguješ, pošleš e-mailem...

Ono kdy /ne/použít try, catch je otázka subjektivního citu, názoru programování, který má samozřejmě každý jiný.., ale je to na delší debatu.

Měj se :-)

Gravatar
Mike 23. 7 2010, 16:03
4/4 Pátek 23. Července 2010, 16:03  |  Opera, Windows Vista

@: Ještě zareaguji :-)

1) Tím jsem nemyslel vlastní framework (ano, mám a už mu tak říkám), ale případy, kdy své knihovny nasazuji na jiné projekty.

3) Částečně máš pravdu, možná by to šlo vyřešit lépe, ale když už to má člověk hotové a jede to, proč to překopávat, že. Navíc jistý účel tu je: rozlišuji totiž klasické výjimky od těch databázových - proto se odchytávají rovnou, aby mohlo proběhnout odlišné zpracování databázových chyb a ostatních výjimek.

4) Tímto principem samozřejmě exceptiony řešené mám (malinko jinak, ale to je fuk). Každopádně jak jsem psal v předcházejícím odstavci, probíhá trochu jiné zpracování, navíc chci, aby se ty výjimky "nějak" zpracovaly, i když nejou přítomny ostaní knihovny frameworku (čiže nakopíruji na projekt pouze onu databázovu třídu).

Když bych to měl shrnout: v mnoha bodech máš pravdu, ale není důvod vymýšlet jiné řešení, když tohle už funguje a chová se přesně tak, jak potřebuji :-)

Přidat komentář







Nevím, kolik to je
Parak simati, Muballit mitte, Nergal allatu mellamu mesaru, La tapallah Annuaki, Kettu Puluthu qillatua