Článek v rubrikách:

»PHP
»MySQL

ORM, dibi, MySQL defaultní hodnoty (pokračování článku)

Článek rozebírá konkrétněji diskusi o nakládání s defaultními hodnotami ve vlastním ORM.


Defaultní hodnoty ORM a CRUD

Co je cílem? Stavíme vlastní ORM. Chceme vymyslet logiku nakládání s defaultními hodnotami, blíže v tomto článku MySQL povinné hodnoty, NULL hodnoty, defaultní hodnoty, prázdné hodnoty a jak by se mělo chovat ORM.

Víceméně se budeme omezovat na PHP 5.3 a vyšší (late static binding se bude hodit).

Potřebujeme zabezpečit klasický CRUD (create, read, update, delete).

Mnou zvolené použití:

Insert

Způsob I1,

  1. $c=new Customer();
  2. $c->name="František Omáčka";
  3. $c->save();

alternativně způsob I2,

  1. $c=new Customer(array('name'=>"František Omáčka"));
  2. $c->save();

alternativně způsob I3,

  1. $c=Customer::create();
  2. $c->name="František Omáčka";
  3. $c->save();

Read

Způsob R1 (tam asi není co řešit, zatím není důležité jak tohle bude realizováno),

  1. $c=Customer::find(1);

Update a Delete

V podstatě řešíme stejný problém, u delete jsou nám defaultní hodnoty lhostejné, ale půjde o stejný problém, přece je zbytečné z databáze načítat řádek, když ho chci smazat.

Při updatu mi záleží na tom, aby nebylo vždy nutné načíst dopředu záznam z databáze před jeho úpravou! A teď kudy do toho, pokud nechci načítat dopředu, nepoužiji find() nebo podobnou metodu. Vytvářet novou instanci new Customer by mohlo být matoucí, co takhle tedy, jak navrhuje Jakub Vrána (getByPrimary), to ale také trochu zavání tím, že se to načítá z databáze (programátor by to tak mohl chápat?) a mohlo by to být matoucí.

Každopádně bude důležité vytvořit instanci tak, aby se nenastavovali defaultní hodnoty. V případě nastavení defaultních hodnot by pak při volání metody save() objekt vždy myslel, že byly nastaveny i hodnoty, které se nastavili jako defaultní a při updatu by je přepsal!

Způsob U1,

  1. $c=Customer::getByPrimary(1);
  2. $c->name="František Omáčka";
  3. $c->save();

alternativně nevhodný způsob U2,

  1. $c=new Customer();
  2. $c->ID=1;
  3. $c->name="František Omáčka";
  4. $c->save();

Tento způsob ani nebude možný! Protože v konstruktoru se automaticky nastaví defaultní hodnoty.

Takže dotaz zní: Kdy a kde nastavovat v ORM defaultní hodnoty a zda vůbec potřebujeme v ORM objektu pracovat s defaultními hodnotami.

UPDATE 9.12.2009

Začal jsem psát ukázku jak by takové ORM mohlo vypadat, přepsal jsem nějaké své staré kódy a nějaký starší pokus, který už jsem zde prezentoval ORM, Zend_Db, Doctrine, DibiTableX a co bych navrhl pro dibi, prošel jsem také Ormion od Honzy Marka (výhodou je, že se zabývá dibi, což je cílem).

Rozhodl jsem se hodně přiblížit kódu Ormionu, i když některé věci bych popsal jinak. Čerpat lze také z www.phpactiverecord.org. To jsou snad jediné věci, které se zabývají PHP 5.3 a ORM. Jde snad o nejnovější pokusy k dnešnímu dni.

Základním souborem pro ORM jsem zvolil továrnu Db.php, která má za úkol pouze vytvářet a vracet konfigurační soubory pro jednotlivé objekty. Nebo vracet připojení k databázi, typicky objekt DibiConnection, případně jiný layer, továrna Db tedy neslouží jen pro ORM, ale pro použití i mimo ORM, naše ORM bude plně svázané s dibi. Jsou zde také uloženy všechny Exceptions, ale asi by měli být ve zvláštním souboru.

Samotný konfigurační soubor jsem definoval např. takto CustomerConfig.php, k vytvoření takového souboru používám skript, který analyzuje mysql tabulku a vytvoří potřebné direktivy, stačí zadat jen název tabulky. Celé ORM se skládá ze třech abstraktních tříd DbRow.php, DbTableConfig.php a DbTable.php (ta zatím není důležitá).

podobné články

09.12.2009DbDibiOrm(67%)
15.12.2009DbDibiOrm - DibiConnection - staticky, instančně, přistupovat přímo k dibi factory?(33%)
15.12.2009ORM, Row Data Gateway, Table Data Gateway, Active Record, Data Mapper(33%)
03.12.2009MySQL povinné hodnoty, NULL hodnoty, defaultní hodnoty, prázdné hodnoty a jak by se mělo chovat ORM(33%)
22.07.2009ORM, Zend_Db, Doctrine, DibiTableX a co bych navrhl pro dibi(33%)

komentáře

RSS Komentáře k článku RSS Komentáře   Add to Google
07.12.2009 20:36 | Anonym (Jakub Vrána) | Lazy loading

Jde o to načíst data až když budou potřeba. Uvádím to v této diskusi už potřetí, ale pořád to asi není pochopitelné. Z pohledu uživatele to je transparentní – když se k datům pokusí přistoupit, tak tam prostě budou. Nástin kódu:

class Customer {
function __get($name) {
$row = mysql_fetch_as­soc(mysql_que­ry(„SELECT * FROM customer WHERE id = $this->id“));
foreach ($row as $key ⇒ $val) {
$this->$key = $val;
}
return $row[$name];
} }

08.12.2009 07:32 | Anonym (v6ak) | Re: Lazy loading

Typicky Jakubův krátký kód, nad kterým se ale člověk musí trošku zamyslet :D Po prvním přístupu k některé vlastnosti se vše uloží do vlastností a pak se už __get nevolá.

Zajímavé by bylo změnit/smazat jiným kódem (třeba i paralelním) ten řádek v DB a pak se dotázat na neexistující vlastnost. To by poněkud překvapivě (pro ty, kdo neznají kód) způsobilo reload z databáze.

Jinak jako nástin dobrý, ale doufám, že v této podobě to nikdo nebude používat.

08.12.2009 10:53 | Administrátor | Re: Re: Lazy loading

tak já se obávám, že __get se bude volat po každé, protože data budou asi uložena jinde než v $this->name, většina ORM, alespoň napsaných v PHP je má uloženy v nějakém poli, tedy typicky $this->_data[‚name‘], hlavně proto, aby poznali při přístupu k hodnotě, že se jedná o hodnotu uloženou v mysql

„Jinak jako nástin dobrý, ale doufám, že v této podobě to nikdo nebude používat.“

co je na podobě špatné?

09.12.2009 12:31 | Anonym (v6ak) | Re: Re: Re: Lazy loading

Začnu od konce. Nelíbí se mi především ta nízká srozumitelnost a výše popsaný side-effect při přístupu k neexistující vlastnosti. O isset teď nemluvím. Kvůli té nízké srozumitelnosti jsi to asi špatně pochopil. Při prvním přístupu k nějaké vlastnosti se zavolá __get, načtou se data, zapíšou se ve foreach do vlastností a vrátí se požadovaná vlastnost. Při každém dalším přístupu (přistupuju-li k existující vlastnosti) se __get nevolá, protože daná vlastnost existuje. Magická metoda __get je jen fallback.

08.12.2009 10:07 | Administrátor | Re: Lazy loading

no asi rozumím jak to myslíš jen mi něco uniká, že nevidíš ten problém (třeba tam není), ale já když provádím save() tak přece vždycky budu k těm hodnotám přistupovat!!! a to je to co nechci! já chci změnit cenu výrobku, aniž bych načítal ten řádek z databáze!

Chci udělat tohle

1. $c=Customer::get­ByPrimary(1);
2. $c->name=„František Omáčka“;
3. $c->save();

aniž bych provedl jakýkoli SELECT do databáze

08.12.2009 13:13 | Anonym (paranoiq) | Re: Re: Lazy loading

myslím, že ‚ORM‘ si bude muset pamatovat, které údaje byly změněny. něco v tomto smyslu:

public function __set($name, $value) {
$this->_data[$name] = $value;
$this->_update[$name] = TRUE; }

potom může udělat při uložení částečný UPDATE (nebo INSERT), aniž by musel načítat ostatní hodnoty z databáze

08.12.2009 13:57 | Administrátor | Re: Re: Re: Lazy loading

to si opět nerozumíme,

1. právě proto, aby si orm pamatovalo co se změnilo, je nutné využívat magické metody a proto nemůže být nikdy nastavena vlastnost/hodnota právě v takto $this->name, tedy public $name;, ale právě v nějakém jiném poli _data[$name] to už sem psal

2. problém je právě v defaultních hodnotách, následující postup

2a. new Customer – v této chvíli mi to nastaví defaultní hodnoty, tedy $this->payerOfVat=0;

tedy ORM si myslí, že payerOfVat byl nastaven, což není úplně pravda

protože ve chvíli, kdy provedu update

  1. $c=Customer::get­ByPrimary(1);
  2. $c->password=„Franta“;
  3. $c->save();

mi to změní i hodnotu payerOfVat což jsem vůbec změnit nechtěl, já chtěl změnit jen jeho heslo!

samozřejmě je možnost, že metoda getByPrimary, by defaultní hodnoty nenastavovala, ale opět nevím jestli je to OK

09.12.2009 09:49 | Anonym (paranoiq) | Re: Re: Re: Re: Lazy loading

zaprvé, ORM přeci nebude tak hloupé, aby si při načtení defaultních hodnot tyto označilo jako změněné. zadruhé, i kdybys je označil a uložil, nic tím nezměníš – jsou přeci defaultní!

myslím, že jsi nepochopil ve které situaci načíst defaultní hodnoty – ty je třeba načítat pouze při vytvoření nového záznamu Customer::create…, nikoliv při načtení existujícího Customer::get…

ještě to shrnu: při vytvoření nového záznamu třeba metodou Customer::cre­ateNew(), je třeba načíst defaultní hodnoty z databáze, tam kde jsou (a dá se to udělat i lazy). při získání existujícího je to samozřejmě nesmysl. tady je na místě načíst celý záznam (pokud neobsahuje nějaké velké sloupce třeba typu blob atd), protože je třeba beztak udělat SELECT – i kdyby jen pro ověření, zda vůbec záznam existuje. pokud potřebuješ dělat nějaký hromadný update a nepotřebuješ u toho znát původní hodnoty , není zřejmě vůbec vhodné vytvářet modelové objekty a měla by se o to postarat nějaká statická metoda modelu, nebo nějaký helper

08.12.2009 13:17 | Anonym (paranoiq) | Re: Re: Lazy loading

přehlédl jsem tohle: „$c = Customer::get­ByPrimary(1);“ to skutečně bez SELECTu nepůjde ^_^

08.12.2009 15:57 | Administrátor | Re: Re: Re: Lazy loading

proč by to nešlo, já právě nechci pokaždé načítat záznam, představte si že děláte tohle

for($i=0;$i<=­40000;$i++) {
$p=Product::get­ByPrimary($i);
$p->salesPrice= „slozity vypocet zalozeny na nekolika funkcich a okolnostech, ktere nedovoluji volat hromadne zpracovani, nebo je hromadne zpracovani z mnoha duvodu problematicke, nebo dokonce chci vratit pouze dotaz, ktery volam pozdeji hromadne“;

$p->save();

alternativně
$update[]=$p->getUpdateSql(); }

alternativně $sql->multi_query(im­plode(„;“, $update));

protože multi_query je tisíckrát rychlejší a z několika minut zpracování dělá několik sekund! ale to už je jiný článek a opravdu je to tak, věřte tomu prosím, je to tak!

09.12.2009 09:34 | Anonym (paranoiq) | Re: Re: Re: Re: Lazy loading

už samotné slovíčko get na začátku názvu metody říká proč to nejde. nemůžeš přece zavolat Customer::get­ByCokoliv() aniž by jsi alespoň ověřil zda takový záznam vůbec existuje

09.12.2009 10:26 | Administrátor | Re: Re: Re: Re: Re: Lazy loading

citace z článku: „Vytvářet novou instanci new Customer by mohlo být matoucí, co takhle tedy, jak navrhuje Jakub (getByPrimary), to ale také trochu zavání tím, že se to načítá z databáze (programátor by to tak mohl chápat?) a mohlo by to být matoucí.“

řekněme tedy jiná metoda než konstruktor, tedy new Customer, máš nápad??

10.12.2009 08:32 | Anonym (paranoiq) | Re: Lazy loading

ano, víc matoucí už to být ani nemůže ^_^

jména musí být naprosto zřejmá (get == SELECT)

10.12.2009 10:02 | Administrátor | Re: Re: Lazy loading

k věci, ať z toho zase není bezvýznamný flame, neodpověděl si na otázku

08.12.2009 18:13 | Anonym (Jakub Vrána) | Re: Re: Lazy loading

Vždyť save() nemusí ukládat vždy všechna data. Jak INSERT, tak UPDATE uloží jenom to, co uživatel explicitně nastavil.

08.12.2009 18:30 | Administrátor | Re: Re: Re: Lazy loading

zkusím na to udělat příklad snad to bude jasnější

10.12.2009 11:03 | Administrátor | Re: Re: Re: Re: Lazy loading

přidal jsem ukázku na git, článek byl updatován a jsou tam odkazy na zdrojáky

co se týče defaultních hodnot budou nestěžejnější metody

<?php
public function & __get($name) {
        return $this->getValue($name);
}
public function __set($name, $value) {
        $this->setValue($name, $value);
}
public function __isset($name) {
        if (isset($this->data[$name])) return true;
        if (isset($this->$name)) return true;
        return false;
}

public function __unset($name) {
        if (isset($this->data[$name])) {
                unset($this->data[$name]);
                unset($this->modified[$name]);
        }
        else unset($this->$name);
}
public function setValue($name, $value) {
        $columns=self::config()->getColumns();
        if (isset($columns[$name])) { // vlastnost se uklada do db
                if (!is_null($value)) { // typova kontrola
                        $type=$s[$string]['type'];
                        if ($type=="%i") $value=(int)$value;
                        elseif ($type=="%f") $value=(float)$value;
                        elseif ($type=="%s" || $type=="%t") $mixed=(string)$value;
                        else throw new DbUndefinedDataTypeException();
                }
                        $this->data[$name]=$value;
                        $this->modified[$name]=1; // byla hodnota zmenena? phpactiverecord to oznacuje jako dirty
                } else { // nejedná se o vlastnost ukladanou do db
                        $this->$name=$value;
                }
        }

public function & getValue($name) {
        if (array_key_exists($name, $this->data)) {
                return $this->data[$name];
                } elseif (array_key_exists($name, $this->defaults)) {
                return $this->defaults[$name];
                } else {
                return $this->$name;
        }
}

?>
  1. se domnívám, že chceme zajistit při každém přístupu k vlastnosti objektu volání metody __get a __set
  2. metoda __set vždy provádí typovou kontrolu, je to vhodné?
  3. je na zvážení jak by se měli chovat hodnoty isset a unset, předpokládám, že defaultní hodnota neznamená že isset bude vracet true,
  4. unset předpokládám, že zruší i informaci o tom, že záznam byl někdy modifikován?

nějaký další názor na kód?

Jméno
Název
Text
Lze používat Texy! syntax. Příklad syntaxe: "text odkazu":odkaz, **tučně**, *kurzíva*, `code`. PHP kód uzavírejte do <?php ... ?> a JavaScript do <script> ... </script>