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,
));
Edit 4. 5. 2020:
Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.
Komentáře k článku:
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.
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 :-)
Přidat komentář