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

Tue, 13 Apr 2010 21:29:21 +1100

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 title pro 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,
));