Pokus o dibi ORM

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Honza Marek
Člen | 1664
+
0
-

Předně bych chtěl zmínit, že jsem ORM nikdy nepoužíval, ale inspiroval jsem se tím, co jsem o něm slyšel. A taky třídou DibiTable, která je teď zavržená a nevyvíjená.

Co umí:

$page = new Page;
$page->findByUrl("kniha-hostu");
echo $page->name;
$page->visits = $page->visits + 1;
$page->save();

$comments = $page->getComments();
foreach ($comments as $comment) {
	echo $comment->text;
}

// přidat komentář
$page->addComment(array("text" => "Blablabla"))->save();

// přidat stránku
$page2 = new Page;
$page2->name = "Nová";
$page2->setData(array("url" => "nova", "text" => "Nová stránka"));
$page2->save();
// přesměrovat po založení stránky
$presenter->redirect("Page:", $page2->getPk());

což by předpokládalo dvě poděděné třídy Page a Comments:

class Page extends BaseModel {

	/** @var string */
	protected $table = "pages";

	public function __construct($data = null) {
		parent::__construct($data);
		$this
			->setType("visits", dibi::INTEGER)
			->setType("allowed", dibi::BOOL);
	}

	public function addComment($data = null) {
		$comment = new Comment;
		$comment->page = $this->getPk();

		if ($data !== null) {
			$comment->setData($data);
		}

		return $comment;
	}

	public function getComments() {
		$comment = new Comment;
		return $comment->fetchAllByPage($this->getPk());
	}
}

class Comment extends BaseModel {

	/** @var string */
	protected $table = "comments";

}

a samozřejmě třídu BaseModel:

<?php

/**
 * Base model
 *
 * @author Jan Marek, David Grudl
 */

abstract class BaseModel extends Object {

	/** @var DibiConnection */
	protected $db;

	/** @var array */
	private $data = array();

	/** @var array */
	protected $types = array();

	/** @var string */
	protected $table;

	/** @var string */
	protected $pk = "id";

	/** @var string */
	protected $pkType = dibi::INTEGER;

	/** @var int */
	protected $pkValue;

	public function __construct($data = null) {
		$this->db = dibi::getConnection();

		$this->setType($this->pk, $this->pkType);

		if ($data !== null) {
			$this->setData($data);
		}
	}

	/**
	 * Set column type
	 * @param string $name
	 * @param string $type
	 */
	protected function setType($name, $type, $format = null) {
		$this->types[$name] = array("type" => $type, "format" => $format);
		if ($name === $this->pk) {
			$this->pkType = $type;
		}
		return $this;
	}

	/**
	 * Get column type
	 * @param string $name
	 * @return string
	 */
	protected function getType($name) {
		if (isset($this->types[$name])) {
			return $this->types[$name]["type"];
		} else {
			return dibi::TEXT;
		}
	}

	/**
	 * Remove column type
	 * @param string $name
	 */
	protected function removeType($name) {
		unset($this->types[$name]);
	}

	/**
	 * Get primary key value
	 * @return int
	 */
	public function getPk() {
		return $this->pkValue;
	}

	/**
	 * Get field value
	 * @param string $name
	 * @return mixed
	 */
	public function & getValue($name) {
		return $this->data[$name];
	}

	/**
	 * Magic getter for field value
	 * @param string $name
	 * @return mixed
	 */
	public function & __get($name) {
		return $this->getValue($name);
	}

	/**
	 * Magic setter for field value
	 * @param string $name
	 * @param mixed $value
	 */
	public function __set($name, $value) {
		$this->setValue($name, $value);
	}

	/**
	 * Set field value
	 * @param string $name
	 * @param mixed $value
	 */
	public function setValue($name, $value) {
		if ($name === $this->pk) {
			$this->pkValue = $value;
		}

		$this->data[$name] = $value;
	}

	/**
	 * Find row by primary key
	 * @param int|string $id
	 * @return Page
	 */
	public function find($id) {
		$q = $this->db
			->select("*")
			->from($this->table)
			->where("%n = %" . $this->pkType, $this->pk, $id)
			->limit(1);

		$data = $q->execute()->setTypes($this->types)->fetch();
		$this->setData($data);

		return $this;
	}

	/**
	 *
	 * @param array $data
	 * @return Page
	 */
	public function setData($data) {
		foreach ($data as $key => $value) {
			$this->setValue($key, $value);
		}

		return $this;
	}

	/**
	 * Get data
	 * @return array
	 */
	public function getData() {
		return $this->data;
	}

	/**
	 * Get data with type modifiers
	 * @return array
	 */
	protected function getPreparedData() {
		$data = array();

		foreach ($this->data as $key => $value) {
			$data[$key . "%" . $this->getType($key)] = $value;
		}

		return $data;
	}

	/**
	 * Insert row
	 */
	protected function insertAsNew() {
		$this->db
			->insert($this->table, $this->getPreparedData())
			->execute();

		$id = $this->db->getInsertId();
		$this->pkValue = $this->pkType === dibi::INTEGER ? (int) $id : $id;
	}

	/**
	 * Update row
	 */
	protected function update() {
		$this->db
			->update($this->table, $this->getPreparedData())
			->where("%n = %" . $this->pkType, $this->pk, $this->pkValue)
			->execute();
	}

	/**
	 * Insert or update row
	 * @return bool
	 */
	public function save() {
		try {
			if (isset($this->pkValue)) {
				$this->update();
			} else {
				$this->insertAsNew();
			}

		} catch (DibiDriverException $e) {
			return false;
		}

		return true;
	}

	/**
	 * Delete row from table
	 */
	public function remove() {
		$this->db
			->delete($this->table)
			->where("%n = %" . $this->pkType, $this->pk, $this->pkValue)
			->execute();

		$this->pkValue = null;
	}

	/**
	 * Magic fetch.
	 * - $row = $model->fetchByUrl('about-us');
	 * - $arr = $model->fetchAllByCategoryIdAndVisibility(5, TRUE);
	 *
	 * @param  string
	 * @param  array
	 * @return BaseModel|array
	 */
	public function __call($name, $args) {
		if (strncmp($name, 'fetchBy', 7) === 0) { // single row
			$single = true;
			$name = substr($name, 7);

		} elseif (strncmp($name, 'fetchAllBy', 10) === 0) { // multi row
			$single = false;
			$name = substr($name, 10);

		} else {
			parent::__call($name, $args);
		}

		// ProductIdAndTitle -> array('product', 'title')
		$parts = explode('_and_', strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $name)));

		if (count($parts) !== count($args)) {
			throw new InvalidArgumentException("Magic fetch expects " . count($parts) . " parameters, but " . count($args) . " was given.");
		}

		$class = $this->getClass();

		if ($single) {
			$data = $this->db->query(
				'SELECT * FROM %n', $this->table,
				'WHERE %and', array_combine($parts, $args),
				'LIMIT 1'
			)->setTypes($this->types)->fetch();

			return $data ? new $class($data) : false;

		} else {
			$data = $this->db->query(
				'SELECT * FROM %n', $this->table,
				'WHERE %and', array_combine($parts, $args)
			)->setTypes($this->types)->fetchAll();

			if (!$data) return array();

			foreach ($data as &$row) {
				$row = new $class($row);
			}

			return $data;
		}
	}

}

Uvítám nějaké nápady. Sám jsem zvědav, co mi to přinese v praxi. Pokud máte někdo pocit, že je to k ničemu, tak to taky klidně řekněte, třeba se pak naučim nějaké normální ORM a budu taky spokojen :-D

ViliamKopecky
Nette hipster | 230
+
0
-

Vypadá to docela pěkně. Pár poznámek:

  • Co takhle místo __construct udělat metodu init pro setType a podobné?
  • Pozor na to, že DibiConnection::getInsertId() vyhazuje DibiException pokud PK není AUTO_INCREMENT

možná by bylo fajn takto:

        /** @var bool */
        protected $pkAutoIncrement = TRUE;

        protected function insertAsNew() {
                $preparedData = $this->getPreparedData();

                $this->db
                        ->insert($this->table, $preparedData)
                        ->execute();

                if ($this->pkAutoIncrement)
                        $id = $this->db->getInsertId();
                else
                        $id = $preparedData[$this->pk];

                $this->pkValue = $this->pkType === dibi::INTEGER ? (int) $id : $id;
        }

// Anebo bez použití $autoIncrement zachytávat Exception při getInsertId()

Co jsem zatím stihnul, tak posílám, ještě se k tomu vrátím.

  • Dále metoda save také počítá jen s AUTO_INCREMENT PK

Editoval enoice (25. 10. 2009 15:05)

_Martin_
Generous Backer | 679
+
0
-

Vypadá to pěkně, určitě k tomu budu mít nějaké připomínky a nápady, až se na to mrknu lépe. Teď snad jen jednu poznámku:

  • primární klíč: primárním klíčem nemusí být jen jedna entita, na to by bylo dobré taky myslet
ViliamKopecky
Nette hipster | 230
+
0
-

Tak jak pokračuješ Honzo?

deric
Člen | 93
+
0
-

použil bych název BaseRow a BaseTable, je to z toho lépe patrné jestli třída pracuje nad celou tabulkou nebo nad jedním řádkem.

co třeba implementovat lazy-load?

$page = new Page(1);
//nebo $page->setId(1);
echo $page->title;

ad primary key:
optimalizovaná databáze by měla mít jednoznačný identifikátor řádku, ať už je to normální id nebo surrogate key (v oracle tuším existuje vždy rowid). v praxi se to občas používá, ovšem implementace je trochu krkolomná

_Martin_
Generous Backer | 679
+
0
-

deric napsal(a):

ad primary key:
optimalizovaná databáze by měla mít jednoznačný identifikátor řádku, ať už je to normální id nebo surrogate key (v oracle tuším existuje vždy rowid). v praxi se to občas používá, ovšem implementace je trochu krkolomná

Á, dobrovolník na flame=) Osobně si myslím, že složený primární klíč není známka neoptimalizované databáze. Řekněme, že používám tabulku s texty stránek a chci je mít multijazyčné. V takovém případě je vhodné použít jako primární klíč kombinaci ID stránky + ID jazyka (s tím, že jazyky mám v jiné tabulce a i stránky – sitemapu – mám jinde). Jakékoliv další ID je v té překladové stránce nadbytečný údaj.

Honza Marek
Člen | 1664
+
0
-

Moc se nám tu nehádejte. Všechno bude. Dibi je mocné. Dokonce tolik, že příští verze dibi orm se bude podobat tomuto zdrojovému kódu:

/**
 * Super databázový layer
 *
 * @author Jan Marek
 * @license MIT
 */
class DibiOrm extends Dibi
{

}
LuKo
Člen | 116
+
0
-

O nějaké formě ORM jsem už dlouho přemýšlel. Docela se mi líbí přístup zvolený v Symfony: http://www.symfony-project.org/…-Model-Layer – je to skoro stejné, jako Honzův přístup s drobným rozdílem, kdy tabulku mají reprezentovanou zvlášť třídou, kdy se záznamy získávají přes statické metody této třídy.

Místo:

<?php
$page = new Page;
$page->findByUrl("kniha-hostu");
echo $page->name;
?>

mají:

<?php
$page = PagePeer::retrieveByUrl("kniha-hostu");
echo $page->name;
?>

Osobně mi to připadá přehlednější (nad objektem konkrétního záznamu se nehledají další stejné záznamy) a když jsem kdysi se Symfony experimentoval, velmi rychle jsem si na to zvykl.

BTW: Jak to vůbec dopadlo s DibiDataSource?

Honza Marek
Člen | 1664
+
0
-

Souhlasím, že ty statické metody jsou pěkné. Ale blbě se s nima dělá v php < 5.3.

_Martin_
Generous Backer | 679
+
0
-

Prošel jsem si stránku o Symphony a jsem nadšen – tak nějak bych si představoval objektový model databáze. Ono je asi jedno, zda to jsou statické metody (byť by byly vhodnější), či zda to je instance – důležitá je ona myšlenka, že jsou zde zvlášť konkrétní záznamy (=modely) a zvlášť pomocné objekty na jejich získávání.

Jak je myšleno, že je práce se statickými metodami v PHP < 5.3 obtížná (v souvislosti s těmito „továrničkami na modely“)?

Editoval _Martin_ (7. 11. 2009 11:36)

Jan Tvrdík
Nette guru | 2595
+
0
-

_Martin_ napsal(a):
Jak je myšleno, že je práce se statickými metodami v PHP < 5.3 obtížná?

Předpokládám, že je to kvůli chybějící podpoře __callStatic + (možná) Late static bindings

_Martin_
Generous Backer | 679
+
0
-

Jan Tvrdík napsal(a):

Předpokládám, že je to kvůli chybějící podpoře __callStatic + (možná) Late static bindings

Jo, jasně, nešlo by třeba volat getBy<something>. Díky

Honza Marek
Člen | 1664
+
0
-

Tak to by šlo poměrně snadno obelstít přes getBy($array), horší je ta absence late static bindingu. Pomocí self nikdy nedostaneš vlastnosti poděděné třídy, které bys potřeboval.

Odkážu na vrtákův blog: http://www.vrtak-cz.net/…keho-objektu

Ola
Člen | 385
+
0
-

A nešlo by to obejít přidáním proměnný $class do třídy a tu předefinovat v každý poděděný třídě (za cenu jednoho řádku)?

Honza Marek
Člen | 1664
+
0
-

Pokud se bavíme o statických třídách, tak přesně to by nešlo :) Protože pomocí klíčového slova self by se nenačetla ta předefinovaná proměnná, ale ta původní. A viz odkaz výše, ani s tím static to není tak slavné.

Editoval Honza M. (7. 11. 2009 13:09)

Honza Marek
Člen | 1664
+
0
-

To phpactiverecord se hodně podobá tomu, co chci já…

LuKo
Člen | 116
+
0
-

Minimum requirements

php-activerecord only works with PHP 5.3!

Jinými slovy, nemá to stejný problém, jako to, co zde řešíme? V Symfony si s tím uměli poradit již před lety. Nebyla by cesta od YML struktury přes propel generátor po předgenerované třídy, do kterých se jen podle potřeby doplní metody public static function getKdesiCosi()? I když uznávám, je to asi spíš řešení hrubou silou, než elegancí přes magické metody.

Honza Marek
Člen | 1664
+
0
-

LuKo napsal(a):

Minimum requirements

php-activerecord only works with PHP 5.3!

Jinými slovy, nemá to stejný problém, jako to, co zde řešíme?

Už ne. Vyřešili ho tím, že dali minimum PHP 5.3 :-D Když jsem koukal, co to umí, tak to ani jinak udělat nejde.

LuKo
Člen | 116
+
0
-

Honza M. napsal(a):
Už ne. Vyřešili ho tím, že dali minimum PHP 5.3 :-D

Návrh nastavit minimum PHP 5.3 i v našem případe mě sice napadl, ale nedovolil jsem si ho vyslovit nahlas ;-)

Jod
Člen | 701
+
0
-

Prostými slovami, hrozné je to phpko.

Honza Marek
Člen | 1664
+
0
-

Vývojovou verzi si můžete prohlédnout na githubu: https://github.com/…arek/dibiorm


Mám dilema ohledně metody save. Buď bude vracet boolean, jestli uložení bylo úspěšné. Anebo bude vracet $this a vyhazovat výjimky, což by možná mohlo být praktičtější.

Ondřej Brejla
Člen | 746
+
0
-

Save se provede, výjimka se nevyhodí…save se neprovede, výjimka se vyhodí…je to mnohem více sexy, než trapný boolean ;-) A navíc můžeš vracet krásně $this, jak říkáš.

Editoval Warden (8. 11. 2009 16:09)

_Martin_
Generous Backer | 679
+
0
-

Honza M. napsal(a):

Vývojovou verzi si můžete prohlédnout na githubu: https://github.com/…arek/dibiorm

Koukám na to a měl bych pár poznámek:

  1. Metody insert a update? Máme přeci metodu save (čili určitě ne public)
  2. Metoda findAll, create, atd…? Mrkni na to Symphony, myslím, že jít cestou „továrniček na modely“ a „samotné modely“ je lepší, navíc tím nebude třída modelu plněna funkcemi, které s ní nesouvisí.
  3. Hlasuji pro výjimky, všude a vždy – nejlépe tedy nějaké vlastní, ne čistě dibi.
  4. Fluent interface je fajn, ač u metody save mám pocit, že je zbytečný (save je takový konec, co potom s tím objektem dělat dál?)
  5. Připojení k DB může být víc, je nešikovné celé ORM napevno svázat s jedním konkrétním připojením z configu.

Editoval _Martin_ (8. 11. 2009 18:05)

Patrik Votoček
Člen | 2221
+
0
-

Honza M. napsal(a):

Mám dilema ohledně metody save. Buď bude vracet boolean, jestli uložení bylo úspěšné. Anebo bude vracet $this a vyhazovat výjimky, což by možná mohlo být praktičtější.

  • vyjímkám
Honza Marek
Člen | 1664
+
0
-

_Martin_ napsal(a):

Honza M. napsal(a):

Vývojovou verzi si můžete prohlédnout na githubu: https://github.com/…arek/dibiorm

Koukám na to a měl bych pár poznámek:

  1. Metody insert a update? Máme přeci metodu save (čili určitě ne public)

Já to zvážim. Chtěl jsem dát na výběr.

  1. Metoda findAll, create, atd…? Mrkni na to Symphony, myslím, že jít cestou „továrniček na modely“ a „samotné modely“ je lepší, navíc tím nebude třída modelu plněna funkcemi, které s ní nesouvisí.

To rozdělení má nějaké nevýhody, přes které bych se musel přenést. Třeba nastavení typů sloupečků je věc, která je potřeba u obou. A autodetekce selhává na sloupcích boolean, protože na to v mysql není typ (bool je alias pro tinyint(1)). Takže by to vedlo částečně třeba k duplikaci kódu.

Navíc pro třídy reprezentující tabulku bych určitě zavedl statický přístup, takže by se musela podporovaná verze php omezit na 5.3.

  1. Hlasuji pro výjimky, všude a vždy – nejlépe tedy nějaké vlastní, ne čistě dibi.

Jasný.

  1. Fluent interface je fajn, ač u metody save mám pocit, že je zbytečný (save je takový konec, co potom s tím objektem dělat dál?)

Třeba můžeš po uložení získat id, když je auto increment.

  1. Připojení k DB může být víc, je nešikovné celé ORM napevno svázat s jedním konkrétním připojením z configu.

Však jo… je tu udělané tak, aby sis metodu getDb mohl snadno přepsat.

paranoiq
Člen | 392
+
0
-

Honza M. napsal(a):

Mám dilema ohledně metody save. Buď bude vracet boolean, jestli uložení bylo úspěšné. Anebo bude vracet $this a vyhazovat výjimky, což by možná mohlo být praktičtější.

při chybě je vždy lepší výjimka, než nějaké záhadné návratové hodnoty (i když jde o boolean..)

Honza Kuchař
Člen | 1662
+
0
-

jsem pro fluent a výjimky

Ondřej Brejla
Člen | 746
+
0
-

Tak si tak programuju a najednou zjišťuju, že by se mi ORM opravdu velice hodilo. Nicméně sahat po Doctrine apod. se mi opravdu nechce…takže si počkám na Honzův výtvor ;-)

Jinak rozhodně fluent + výjimky.

romansklenar
Člen | 655
+
0
-

Honza Marek napsal(a):

_Martin_ napsal(a):

  1. Fluent interface je fajn, ač u metody save mám pocit, že je zbytečný (save je takový konec, co potom s tím objektem dělat dál?)

Třeba můžeš po uložení získat id, když je auto increment.

IMHO není třeba, pokud to bude fungovat takhle:

$page = new Page;
$page->name = '...';
$page->content = '...';
echo $page->id; // vede k výjimce
$page->save();
echo $page->id; // zobrazí správně id => není důvod získávat ze save()
_Martin_
Generous Backer | 679
+
0
-

Honza Marek napsal(a):

Já to zvážim. Chtěl jsem dát na výběr.

Zvaž. Protože bych ovšem byl velmi rád za kvalitní ORM, doporučuji ponechat jen metodu save a nedělat z toho obal na dibi.

To rozdělení má nějaké nevýhody, přes které bych se musel přenést. Třeba nastavení typů sloupečků je věc, která je potřeba u obou. A autodetekce selhává na sloupcích boolean, protože na to v mysql není typ (bool je alias pro tinyint(1)). Takže by to vedlo částečně třeba k duplikaci kódu.

Navíc pro třídy reprezentující tabulku bych určitě zavedl statický přístup, takže by se musela podporovaná verze php omezit na 5.3.

Můj názor je, že návrh by měl být implementaci podřizován co nejméně – nejlépe vůbec. Ono se vždy nějaké řešení najde.

Třeba můžeš po uložení získat id, když je auto increment.

Jo, ale logické to moc není. Hezky to napsal Roman.

Však jo… je tu udělané tak, aby sis metodu getDb mohl snadno přepsat.

OK, to je pravda. A možná by se časem dal vymyslet nějaký elegantnější způsob.

romansklenar napsal(a):

$page = new Page;
$page->name = '...';
$page->content = '...';
echo $page->id; // vede k výjimce
$page->save();
echo $page->id; // zobrazí správně id => není důvod získávat ze save()

Otázka, zda výjimka či NULL. Každopádně se mi to líbí=)

LuKo
Člen | 116
+
0
-

Bude chování (NULL/Exception) stejné i pro ostatní dosud nenaplněné parametry?

<?php
$page = new Page;
echo $page->id; // NULL || Exception?
echo $page->content; // NULL || Exception?
?>

Nebo bude výjimku vyhazovat pouze NULL-ový primární klíč?

Honza Marek
Člen | 1664
+
0
-

Vracel bych null všude. To romanovo samozřejmě funguje.

paranoiq
Člen | 392
+
0
-

vracení NULL je špatný nápad. pokud funkce něco měla vrátit a hodnotu nezískala, tak je to CHYBA. a chyby by se měly ošetřovat výjimkami. NULL je třeba zbytečně vlastnoručně kontrolovat a to zasírá kód a snadno se na to zapomene

Honza Marek
Člen | 1664
+
0
-

Možná by se daly udělat výjimky pro neexistující sloupce. Ale null jako nenastaveno mi přijde naprosto v pořádku.

Ondřej Brejla
Člen | 746
+
0
-

A co třeba takto:

Pokud přistupuju k hodnotě primárního klíče, která ještě nebyla nastavena, pak je to chyba a vyhazoval bych třeba UndefinedValueException. Pokud přistupuju k hodnotě sloupce, který je definován jako NOT NULL a také nemá nastavenou hodnotu (v podstatě stejný příklad jako s PK), zase je to chyba a opět bych vyhazoval výjimku UnknownValueException, pokud přistupuju k nenastavené hodnotě sloupce, který je definován jako NULL, pak je vše v pořádku a pak vracím NULL.

LuKo
Člen | 116
+
0
-

Ještě to trochu zkomplikuji. $page->keywords; může být prázdný, ale v DB ho mám kvůli indexům nastaven na NOT NULL. Výjimku bych asi vyhodil jen u PK, byla by to jen pojistka. Výjimku (asi také?) bude vyhazovat save() v případě, že se po provedeném insertu nepodaří získat hodnotu PK. Při vkládání PK do FK v jiné tabulce musí být kontrola na správnost vkládané hodnoty. Nějak se nám to komplikuje ;-)

Ondřej Brejla
Člen | 746
+
0
-

„Prázdnost“ sloupce a NOT NULL omezení spolu nesouvisí…pokud máš sloupec prázdný, pak je v něm hodnota (protože není NULL)…pokud do VARCHARu uložim '', pak je v něm hodnota, prázdná a přitom sloupec dodržel NOT NULL omezení.

Já popisuji stav, kdy v NOT NULL sloupci ještě není uložená ani ta „prázdná“ (DEFAULT?) hodnota…pak by přístup k této „nedefinované“ hodnotě měl vyhazovat UnknownValueException. Snad si rozumíme ;-)

LuKo
Člen | 116
+
0
-

Omlouvám se, sypu si popel na hlavu, špatně jsem používal odporné termity ;-) Měl jsem na mysli nedefinovanou hodnotu = NULL. Například:

<?php
$page = new Page;
echo $page->keywords; // v DB nastaveno NOT NULL
?>

by i bez uložení či načtení prázdného řetězce z DB nemělo vyhodit výjimku. Jednoduše by to do stránky nemělo nic vypsat. Jde to sice řešit přes default values

<?php
class Page
{
	private $keywords = '';
// ...
?>

ale to mi kdysi jeden zkušenější programátor z kódu důsledně mazal, že je to chyba :-/

Ondřej Brejla
Člen | 746
+
0
-

Naopak si myslím, že vracení jakési DEFAULT hodnoty, v případě, že je proměnná sloupce ještě nenastavena a přitom je definována jako NOT NULL, je ta správná cesta (ať už je ono DEFAULT implementováno jakkoliv).

$page = new Page;
echo $page->keywords; // v DB nastaveno NOT NULL, ale v ORM mám definovanou defalut hodnotu: vrátí se DEFAULT hodnota

echo $page->id; // v DB je PK: vyhodí UnknownValueException, protože PK ještě nebyl nastaven

echo $page->login // v DB nastaveno NOT NULL: vyhodí UnknownValueException, protože ještě nebyl nastaven

echo $page->name // v DB nastaveno NULL: vrátí NULL

Editoval Ondřej Brejla (9. 11. 2009 13:04)

paranoiq
Člen | 392
+
0
-

Ondřej Brejla napsal(a):

Pokud přistupuju k hodnotě primárního klíče, která ještě nebyla nastavena, pak je to chyba a vyhazoval bych třeba UndefinedValueException. Pokud přistupuju k hodnotě sloupce, který je definován jako NOT NULL a také nemá nastavenou hodnotu (v podstatě stejný příklad jako s PK), zase je to chyba a opět bych vyhazoval výjimku UnknownValueException, pokud přistupuju k nenastavené hodnotě sloupce, který je definován jako NULL, pak je vše v pořádku a pak vracím NULL.

a jak rozliším, zda bylo NULL načteno ze sloupce, který může obsahovat NULL, nebo zda jde o doposud nenačtený záznam z databáze? i když může sloupec obsahovat NULL, není jisté zda ho opravdu obsahuje dokud a) nenaplním nový záznam daty nebo b) nenačtu záznam z DB. takže bych výjimku vyhazoval vždy. u záznamu, který dosud nebyl nějak inicializován InvalidStateException

vlastně ne. existenci neinicializovaného záznamu bych raději vůbec nepřipustil. nový záznam by měla daty naplnit továrnička

Ondřej Brejla
Člen | 746
+
0
-

Teď nevím, jestli ti přesně rozumím. Můžeš to pro jistotu ukázat na nějakém příkladu?

EDIT:

Dosud nenačtený záznam z DB může buď vyhodit výjimku, nebo vrátit NULL. Který sloupec je definován jako NULL, nebo NOT NULL by mělo být dané z nějaké definice tabulky, se kterou pracuji (tedy ve třídě reprezentující tabulku, se kterou pracuji).

Příklad s prázdným objektem:

$page = new Page(); // $page je prázdný objekt...žádná data z db

echo $page->name; // v Page je $name definováno jako NOT NULL, a proto vyhodím vyjímku, protože $page je prázdné

$page->name = 'John Doe';
echo $page->name; // $name je NOT NULL a už MÁ hodnotu, vracím tedy 'John Doe'

echo $page->age; // v Page je $age definováno jako NULL, a proto vrátím NULL, protože $page je prázdné

$page->age = 30;
echo $page->age; // přestože je v Page $age definováno jako NULL, vracím 30, protože už má atribut hodnotu

Příklad s naplněným objektem:

$page = new Page($id); // $page je plný objekt, podle $id načtu hodnoty z db (nebo ho naplním jinak, to je jedno)

echo $page->name; // v Page je $name definováno jako NOT NULL: vrátím načtené jméno, pokud bylo nalezeno, nebo vyhodím vyjímku, pokud DB vrátila NULL (což by neměla, pokud je i sloupec v DB definován jako NOT NULL)

echo $page->age; // v Page je $age definováno jako NULL: vrátím to, co bylo v DB nalezeno...buď věk, a pokud nalezen nebyl, vrátím NULL

Je to trochu srozumitelné?

Editoval Ondřej Brejla (9. 11. 2009 14:41)

_Martin_
Generous Backer | 679
+
0
-

paranoiq napsal(a):

vlastně ne. existenci neinicializovaného záznamu bych raději vůbec nepřipustil. nový záznam by měla daty naplnit továrnička

Nezůstaneme rovnou u dibi::insert?

Tady přece není nic jako „neinicializovaný“ záznam. Mám objekt. Ten nějak nastavím. A pokud uznám za vhodné, tak jej uložím. Uložené objekty mohu znovu vyvolat.

Je to jak s listem papíru. Vemu prázdnej list a něco na něj napíšu. Napsal jsem blbost – ani ho neuložím do zásuvky a rovnou ho vyhodím. A nebo ho uložím. A později ho vyndám.

Jasné?

ID jako NULL mi přijde v pořádku – prakticky znamená, že záznam nebyl uložen.

Editoval _Martin_ (9. 11. 2009 14:37)

paranoiq
Člen | 392
+
0
-

@ondřej et @martin: máte pravdu. pokud může být pole NULL, tak není nutné vyhazovat výjimku (u nepovinného pole nelze určit zda jde o chybu). chybu programátora tak bohužel ošetřit nejde. může si objekt nakrásně uložit a nepovinná data dodat později

@martin:
ID jako NULL mi nepřijde v pořádku – svědčí to o tom, že čtu něco o čem netuším, jestli je to nastaveno – nemám vlastně pod kontrolou tok programu. pokud jsem ještě objekt neuložil, měl bych to vědět, nebo bych to měl zjisti nějakým vhodným způsobem – např. metodou isSaved(). pokud musím při každém načtení hodnoty kontrolovat, jestli není návratová hodnota nějaká magická konstanta (v tomhle případě NULL), určitě to není dobré API a určitě na tu kontrolu někde zapomenu (=BUG)

o tom zahazován rozdělaných objektů si myslím tohle: primárním účelem ORM je ukládání (a načítání) dat. pokud jsem nový objekt za nějakým účelem rozdělal nebo pozměnil a záměrně neuložil je to v pořádku, ale měl bych to říci explicitně (např. $nejakyObjekt->discard()). jinak může dojít k tomu, že ho mohu neuložit omylem. šlo by to možná ošetřit vyhazováním warningu v destruktoru (při neuložení). stejně tak bych od databáze očekával, že když mám nepotvrzenou transakci, tak mi při ukončení spojení pošle nějaký warning. tiché zahození nepotvrzených dat (bez ROLLBACK) může být chyba. to by šlo ošetřit warningem v destruktoru DibiConnection nebo při ručním odpojení.

Editoval paranoiq (9. 11. 2009 17:57)

_Martin_
Generous Backer | 679
+
0
-

paranoiq napsal(a):

@martin:
ID jako NULL mi nepřijde v pořádku – svědčí to o tom, že čtu něco o čem netuším, jestli je to nastaveno – nemám vlastně pod kontrolou tok programu. pokud jsem ještě objekt neuložil, měl bych to vědět, nebo bych to měl zjisti nějakým vhodným způsobem – např. metodou isSaved(). pokud musím při každém načtení hodnoty kontrolovat, jestli není návratová hodnota nějaká magická konstanta (v tomhle případě NULL), určitě to není dobré API a určitě na tu kontrolu někde zapomenu (=BUG)

Naopak, NULL znamená, že to není nastaveno. Co na tom, že jde o hodnotu, která se nastavuje až při uložení? Kdybych měl políčko lastSavedDate, tak by před uložením taky nemělo házet výjimku. Proč taky? Přeci každé čtení dat neošetřím try – catch blokem.

A asi si nerozumíme v jedné věci: to, že z ID = NULL lze odvodit, že záznam nebyl uložen, neznamená, že z ID = NULL mám zjišťovat (ne)uložení záznamu. O žádných magických konstantách nemůže být řeč. Jinak metoda isSaved je dobrej nápad.

o tom zahazován rozdělaných objektů si myslím tohle: primárním účelem ORM je ukládání (a načítání) dat. pokud jsem nový objekt za nějakým účelem rozdělal nebo pozměnil a záměrně neuložil je to v pořádku, ale měl bych to říci explicitně (např. $nejakyObjekt->discard()). jinak může dojít k tomu, že ho mohu neuložit omylem. šlo by to možná ošetřit vyhazováním warningu v destruktoru (při neuložení). stejně tak bych od databáze očekával, že když mám nepotvrzenou transakci, tak mi při ukončení spojení pošle nějaký warning. tiché zahození nepotvrzených dat (bez ROLLBACK) může být chyba. to by šlo ošetřit warningem v destruktoru DibiConnection nebo při ručním odpojení.

Tak to se neshodneme, já si zase myslím, že explicitně bych měl potvrzovat změnu a ne čekat, že se bude objekt automaticky ukládat. V případě nepotvrzené transakce, na kterou nebyl zavolán rollback, na tom asi něco je – ovšem transkace je způsob uložení, takže to nelze srovnávat se samotným uložením.

Jinými slovy: samotná změna property není akce.


Z nás by měl Martin Malý radost…

Honza Marek
Člen | 1664
+
0
-

paranoiq napsal(a):
měl zjistit nějakým vhodným způsobem – např. metodou isSaved().

$object->getState() === BaseModel::STATE_LOADED
Honza Marek
Člen | 1664
+
0
-

_Martin_ napsal(a):

Z nás by měl Martin Malý radost…

Já z vás mám taky radost :-D Nakonec to zřejmě udělám všechno podle sebe, tedy podle zásady, že nejjednodušší řešení je to nejlepší.

Honza Marek
Člen | 1664
+
0
-

Co by mě ale zajímalo, kolik lidí je ochotno oželit podporu php 5.2.

_Martin_
Generous Backer | 679
+
0
-

Já =)

P.S.:

„Všechno by mělo být co nejjednodušší, ale ne jednodušší.“ – Albert Einstein

ViliamKopecky
Nette hipster | 230
+
0
-

Já vám nevím pánové… Já bych se přimluvil raděj za 5.2