Plánuje se Exploreru přidat podporu datových typů, jako to mají formuláře a latte?
- Bulldog
- Člen | 110
Ahoj,
chci se zeptat, jestli je v plánu do budoucna přidat i možnost zvolit si
datový typ, který vrátí Selection
podobně jako to mají
formuláře/latte.
Jde mi o to, že ORM, které existují mi přijdou pomalé/krkolomné atp. Většinou si tvořím vlastní řešení. No Nette databáze mi přijde hodně fajn, protože vlastně nejde o žádné sofistikované řešení, ale jen o jednoduchý ‚spojovník‘, nebo jak bych to řekl a můžu si upravit výpisy přímo na míru jak potřebuji.
Už dlouho ale přemýšlím, jestli když se Nette pohybuje ke striktnímu Objektovému návrhu a datovému typování, jestli by nebylo od věci, aby toto uměla i DB. Nemyslím tím tvořit z toho ORM, ale jen aby výsledná data nebyla reprezentována jen polem, Row, nebo nějakým obecným ActiveRow, ale abych mohl výsledky jednotlivých dotazů rovnou čerpat v mnou zvolené třídě a ne to později konvertovat.
Pokud nic takového v plánu není, tak se chci zeptat, jestli se toho mám ujmout a zkusit to do Nette databáze přidat?
- neznamy_uzivatel
- Člen | 115
Felix napsal(a):
Udelal jsem si misto active row vlastni “active row per tabulka”.
Nebyla by prosím malá ukázka jak na to? Vůbec si to nedovedu
představit..
Já si primitivně posílám ActiveRow do konstruktoru objektu, kde foreachem
projdu data a nasypu to do public properties, takže když potřebuju něco
upravit při výpisu, tak to dělám v getteru..
- Marek Znojil
- Člen | 70
neznamy_uzivatel napsal(a):
Felix napsal(a):
Udelal jsem si misto active row vlastni “active row per tabulka”.
Nebyla by prosím malá ukázka jak na to? Vůbec si to nedovedu představit..
Já si primitivně posílám ActiveRow do konstruktoru objektu, kde foreachem projdu data a nasypu to do public properties, takže když potřebuju něco upravit při výpisu, tak to dělám v getteru..
Dělám to stejně. :D
- Bulldog
- Člen | 110
No já to dělám tak, že si vytvořím třídu konkrétní entity,
například User
, která v konstruktoru bere ActiveRow, které
může být nullable a má privátní proměnnou typu array
s názvem třeba $data
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Setter uloží novou hodnotu do $data
pod klíčem z db tabulky a
getter se podívá, jestli tato hodnota existuje v poli $data
a
pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený
ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null,
nebo vyhodí error neinicializovaná proměnná.
Tím je to editovatelná entita, která umí pracovat s ActiveRow.
No a dále moje entita User
se chová jako takový osekaný
PROXY nad ActiveRow, takže má třeba metody ref
,
related
, update
, delete
, které jen
forwarduje na ten vnitřní ActiveRow.
Například metoda update
udělá to, že zavolá vnitřně:
$this->activeRow->update($this->data)
a následně:
$this->data = []
Samozřejmě tam probíhá nějaká kontrola, jestli je ActiveRow nastaven,
případně když je pole $data
prázdné, tak to dotaz vůbec
nedělá atp., ale princip fungování je podobný.
Akorát mi to přijde takové složité, takže bych uvítal něco podobného přímo v Exploreru, nebo jako doplněk, jak psal @Felix
Editoval Bulldog (12. 11. 2021 12:18)
- Kamil Valenta
- Člen | 695
Bulldog napsal(a):
No já to dělám tak, že si vytvořím třídu konkrétní entity, například
User
, která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typuarray
s názvem třeba$data
Takže to není ani zapouzdřené, ani to nenapovídá atributy. K čemu je takový kontejner dobrý?
- Felix
- Nette Core | 1173
Ja to udelal takto. Rad bych to probral s @DavidGrudl a nebo pripadne hodil do contributte/database.
- Mrknul jsem, kde se tvori ActiveRow (
new ActiveRow
). A to je trida Selection (https://github.com/…election.php#L556). Metoda se jmenujecreateRow
a je protected. Lze pretizit. - Kouknul jsem se, kde se tvori trida Selection. Jedna se o Explorer (https://github.com/…Explorer.php#…).
Tak jsem si ho podedil. Tedka je moznost predelat metodu
table
a nebo si udelat svoji vlastnitableXyz()
. - Kdyz mame vlastni tridy, ktere umozni zmenu ActiveRow na vlastni tridu, tak
lze poresit jeste automatickou detekci podle tabulky, napriklad.
// Misto ActiveRow se pouzije nas objekt $context->setRowClass(CustomActiveRow::class); // Misto ActiveRow se pouzije nas objekt podle tabulky $context->setRowMapping([ 'user' => UserRow::class, 'address' => AddressRow::class, ]);
- Posledni skladacka je prepsani defaultniho Exploreru (ve starsich verzich se
jmenuje Context).
To se najde tak, ze je potreba dohledat registraci tridy Explorer
v DatabaseExtension (https://github.com/…xtension.php#L122).
Extension
se stara o to, aby se do Nette DI
containeru zaregistrovali vsechny potrebne sluzby a clovek si je nemusel
v souboru
config.neon
registrovat sam.# 1. Registrace nove tridy. Ktera se pak pouzije jako zavislost ve vasich tridach. services: - App\CustomExplorer(@database.default.connection, @database.default.structure, @database.default.conventions) # 2. Prepis originalnich exploreru z nette/database. V pripade vice connection zaregistrovat prepsat vice exploreru/contextu. services: database.default.context: App\CustomExplorer(@database.default.connection, @database.default.structure, @database.default.conventions)
Trida CustomContext
. Resi, jestli se pouzije vlastni trida pro
ActiveRow a nebo se pouzije trida podle nazvu tabulky.
use Nette\Caching\IStorage;
use Nette\Database\Connection;
use Nette\Database\Context;
use Nette\Database\Conventions\StaticConventions;
use Nette\Database\IConventions;
use Nette\Database\IStructure;
final class CustomContext extends Context
{
/** @var string */
private $rowClass;
/** @var string[] */
private $rowMapping = [];
public function setRowClass(string $class): void
{
$this->rowClass = $class;
}
/**
* @param string[] $mapping
*/
public function setRowMapping(array $mapping): void
{
$this->rowMapping = $mapping;
}
/**
* @param string $table
*/
public function table($table): CustomSelection
{
$selection = new CustomSelection($this, $this->conventions, $table, $this->cacheStorage);
if ($this->rowClass) {
$selection->setRowClass($this->rowClass);
}
if ($this->rowMapping) {
$selection->setRowMapping($this->rowMapping);
}
return $selection;
}
}
Trida CustomSelection
. Resi jaka trida se pouzije misto
ActiveRow.
use Nette\Database\Table\ActiveRow;
use Nette\Database\Table\Selection;
final class CustomSelection extends Selection
{
/** @var string */
private $rowClass;
/** @var string[] */
private $rowMapping = [];
public function setRowClass(string $rowClass): void
{
$this->rowClass = $rowClass;
}
/**
* @param string[] $mapping
*/
public function setRowMapping(array $mapping): void
{
$this->rowMapping = $mapping;
}
protected function createRow(array $row)
{
$className = $this->resolveClass();
return new $className($row, $this);
}
private function resolveClass(): string
{
if ($this->rowClass) {
return $this->rowClass;
}
if (isset($this->rowMapping[$this->getName()])) {
return $this->rowMapping[$this->getName()];
}
return ActiveRow::class;
}
}
Ukazkovy ActiveRow per tabulka.
/**
* @property int $id
* @property string $name
* @property string $surname
*/
class User extends ActiveRow
{
}
- Bulldog
- Člen | 110
@KamilValenta
Kamil Valenta napsal(a):
Bulldog napsal(a):
No já to dělám tak, že si vytvořím třídu konkrétní entity, například
User
, která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typuarray
s názvem třeba$data
Takže to není ani zapouzdřené, ani to nenapovídá atributy. K čemu je takový kontejner dobrý?
Bulldog napsal(a):
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Tím je myšleno, že entita vypadá například takto:
/**
* @property string $email
* @property string $password
*/
class User extends Entity
{
public function getEmail(): string
{
return $this->getColumn('email');
}
public function setEmail(string $email): void
{
$this->changedColumns['email'] = $email;
}
public function getPassword(): string
{
return $this->getColumn('password');
}
public function setPassword(string $password): void
{
if (empty($password)) {
return;
}
$this->changedColumns['password'] = $password;
}
}
a Entity.php:
/**
* @property-read int|string|null $id Primary key identifier. Null if key does not exists (not inserted into the database yet)
*/
abstract class Entity implements IteratorAggregate, ArrayAccess
{
use SmartObject;
protected array $changedColumns = [];
protected ActiveRow|null $row = null;
public function getIterator(): Traversable|Iterator
{
return new ArrayObject($this->toArray());
}
public function offsetExists($offset): bool
{
return (bool) $this->offsetGet($offset);
}
public function offsetGet($offset)
{
return ($this->$offset ?? null);
}
public function offsetSet($offset, $value)
{
$this->$offset = $value;
}
public function offsetUnset($offset)
{
unset($this->changedColumns[$offset]);
}
public function update(): int
{
// ověřování
return (int) $this->row->update($this->changedColumns);
}
public function related(BaseRepository $repository): DataCollection
{
// ověřování
return new Selection($this->row->related($repository::getTableName()), $repository::getEntityClass());
}
public function delete(): int
{
// ověřování
$retVal += $this->row->delete();
$this->changedColumns = [];
return $this->setActiveRow(null);
}
public function toArray(): array
{
// tady trošku pracuju s reflexí, aby jsem měl v návratové hodnotě klíče podle properties, ne podle sloupců v DB
}
public function __toArray(): array
{
return $this->toArray();
}
public function getId(): int|string|null
{
return $this->getColumn('id');
}
public function setActiveRow(ActiveRow|null $row): void
{
$this->row = $row;
}
public function getActiveRow(): ActiveRow|null
{
return $this->row;
}
protected function getColumn(string $columnName): mixed
{
return $this->changedColumns[$columnName] ?? ($this->row->$columnName ?? '');
}
}
Pro samotnou Selection to mám podobně, aby jsem forwardoval funkčnost
přes PROXY, jelikož chci využívat fičury lazy načítání atp.
Takže je to zapouzdřené i napovídá.
Editoval Bulldog (12. 11. 2021 12:38)
- Bulldog
- Člen | 110
@Felix
Tak jsem to zkoušel taky, ale zasekl jsem se na tom, že nevím,
jak zaregistrovat můj poděděný Explorer do DI, aby ho bralo klasické
nastavení připojení k databázi.
Můžu to udělat normální registrací jako službu, ale rád bych používal
i nadále výchozí nastavení připojení, což není moc
kompatibilní ne?
- Polki
- Člen | 553
Dobrá práce hoši.
Možná by to chtělo ještě nějak vymyslet, aby ty třídy byly volitelně
editovatelné co?
Když budu mít třídu Article
, kterou budu cpát do
formuláře, tak nechci mít druhou třídu Article
, kterou budu
cpát do databáze a mezi nima muset ručně přelévat data.
Představoval bych si to nějak takto:
public function articleFormSucceeded(Article $data): void
{
$this->articleRepository->save($data);
}
ArticleRepo:
public function save(Article $data): Article
{
if ($data->id) {
$data->update();
// Nebo
$this->db->table('article')->get($data->id)->update($data);
// Nebo
$this->db->table('article')->where('id', $data->id)->update($data);
// Nebooooooooooo jelikož $data obsahují už ID, tak
$this->db->table('article')->update($data);
} else {
$data = $this->db->table('article')->insert($data);
}
return $data;
}
Samozřejmě třída Article by se mohla vnitřně starat o validaci dat
Jo a taky místo $this->db->table('article')
by byla nějaká
metoda, která by nastavovala typ té třídy, případně by se dalo možná
zapsat něco jako $this->db->update($data)
a
$this->db->insert($data)
, přičemž to, do jaké tabulky se
to má uložit by se četlo podle typu třídy, co se tam pošle, jelikož
třída je svým způsobem identifikátor tabulky?
Editoval Polki (12. 11. 2021 13:29)
- Polki
- Člen | 553
Nápad na použití
Třída:
/**
* @property string $name name // $name je název property pro appku a name je název sloupce v DB
* @property string $content // pokud poslední parametr neuvedeme, tak je překlad 1:1, ale CamelCase vs snake_case
* @property int $authorId author_id // tady je jasně vidět překlad
*/
class Article implements ActiveRow // nebo jiný rozhraní případně dědit třídu. Důležité je, aby byla editovatelná
{ }
config:
database:
mapping:
article: App\Model\Entity\Article # formát nazev_tabulky: trida
user: App\Model\Entity\User
- App\Model\Entity\Comment # V případě, že neexistuje klíč, tak název tabulky je název entity ale v snake_case, tedy comment
Čtení:
$article = $this->db->table(Article::class)
->where() ... // tak jak to známe nad klasickou Selection
->fetch(); // Vrátí instanci třídy Article naplněnou daty z tabulky article
$author = $article->author; // Vrátí instanci třídy User, naplněnou daty z tabulky user
$comments = $article->rel(Comment::class); // Vrátí GrouppedSelection, která v případě fetch() bude vracet instance třídy Comment z tabulky comment
//////////////////////
$user = $this->db->table('user')->get(5); // Vrátí instanci třídy User
$setting = $this->db->table('setting')->get(5); // Vrátí instanci třídy ActiveRow, jelikož není v configu specifikováno mapování.
Zápis:
$this->db->insert($article); // provede insert dat z instance uložené v proměnné $article do databázové tabulky, kterou zjistí z configu podle $article::class
$this->db->update($article); // provede update...
$this->db->delete($article); // provede delete...
$this->db->save($article); // Provede insert nebo update na základě toho, jestli je v $article nastavena proměnná reprezentující PK v tabulce article
$this->db->insert($setting) // Error, setting je instance ActiveRow a neví se, do které tabulky se má uložit
$this->db->insert([
'name' => 'Robert',
'surname' => 'Pěšinka',
]) // Error posíláme pole, takže zase nevíme, kam data uložit
$this->db->table('setting')->insert($setting); // OK je specifikovaná i tabulka
$this->db->table('article')->insert($article); // OK sice je specifikovaná tabulka zbytečně, ale pro zpětnou kompatibilitu. :)
$thsi->db->table(Comment::class)->insert($article); // Error snažím se něco, co patří do tabulky article vložit do tabulky comment. Díky tomu není třeba ani dělat dotaz, jelikož tato chyba se dá odchytit již v insertu.
Editoval Polki (12. 11. 2021 21:15)
- Kamil Valenta
- Člen | 695
Bulldog napsal(a):
Takže je to zapouzdřené i napovídá.
Ano, tohle už ano. Já jsem vycházel z popisu „která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typu array s názvem třeba $data“, protože zaslaná ukázka tomuto popisu neodpovídá.
- Polki
- Člen | 553
Já si myslím, že mu popis odpovídá docela dobře. To, že si místo do
konstruktoru udělal setter a getter pro ten ActiveRow a že se ta privátní
proměnná jmenuje changedColumns
místo data
a je
protected díky dědění základních fičur si myslím, že je jen detail.
Klidně si tam ten konstruktor, který očekává ActiveRow můžeš přidat a
proměnnou changedColumns
přejmenovat a bude to přesně
odpovídat.
A taky si myslím, že odpovídá i zbytek…
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Z toho je podle mě jednoznačně jasný, že ta entita má v sobě properties, přes které se přistupuje k obsahu proměnné data, nebo toho ActiveRow, takže napovídání, integrita dat apod. je zajištěná ne?
Setter uloží novou hodnotu do $data pod klíčem z db tabulky a getter se podívá, jestli tato hodnota existuje v poli $data a pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null, nebo vyhodí error neinicializovaná proměnná.
Tady je to dokonce detailněji popsaný, díky čemuž už musí být tutově jasný, že ta třída vystavuje jako rozhraní gettery a settery nějakých properties a když je specifikováno, že data jsou privátní a AR se bere v konstruktoru, ale nepíše se, že by bylo zpřístupněno veřejně, tak to imho dává smysl.
No a dále moje entita User se chová jako takový osekaný PROXY nad ActiveRow, takže má třeba metody ref, related, update, delete, které jen forwarduje na ten vnitřní ActiveRow.
A tímhle dokonce říká, že se ta třída chová jako PROXY nad ActiveRow a doufám, že všichni, co se naučili s Nette umí i návrhové vzory, z čehož by taky mělo být jasný, že nejde přistupovat napřímo k ActiveRow uvnitř, takže nutně musí taky být privátní a musí se k němu (jeho hodnotám) přistupovat přes ty Gettery/Settery.
Za mě je to úplně v pohodě popis, který reflektuje zaslaný
příklad.
Schválně jsem podle popisu teď dal 2 kolegům napsat výsledek a vypadal
stejně jako na příkladu, jen místo getteru a setteru byl konstruktor a
vnitřní proměnná changedColumns
se samozřejmě
jmenovala data
Editoval Polki (12. 11. 2021 21:17)
- Kamil Valenta
- Člen | 695
Humorné je, Polki, jak Tvé ego dosud nerozdýchalo, jak Ti nikdo nedal za pravdu ve vláknu o db Exploreru a CRUD operacích a od té doby mínusuješ cokoliv, co napíšu :) Ale pokud to Tvé psychice pomůže, rád tuto úlohu na sobě ponechám.
Polki napsal(a):
Já si myslím, že mu popis odpovídá docela dobře.
Já myslím, že ne, a tak jsem se raději zeptal. Rozdíl mezi námi je, že já si za druhé nedomýšlím a raději se konkrétně ptám, aby si někdo nezvolil cestu antipatternu.
Z toho je podle mě jednoznačně jasný, že ta entita má v sobě properties, přes které se přistupuje k obsahu proměnné data, nebo toho ActiveRow, takže napovídání, integrita dat apod. je zajištěná ne?
Ne nutně, dokud jsme neviděli ukázku.
$entita->data->column
poskytuje všechny (pseudo)properties, ale nenapovídá.
O zajištění integrity nemůžeme vůbec hovořit, dokud jsme kód neviděli. Integritu nezajišťuje jen a pouze přítomnost setteru/getteru, ale jejich konkrétní podoba…
Setter uloží novou hodnotu do $data pod klíčem z db tabulky a getter se podívá, jestli tato hodnota existuje v poli $data a pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null, nebo vyhodí error neinicializovaná proměnná.
Tady je to dokonce detailněji popsaný, díky čemuž už musí být tutově jasný, že ta třída vystavuje jako rozhraní gettery a settery
Popisuje právě jeden setter a getter na data, z toho tutově nevyplývá, že ta classa má setterů / getterů více…
A tímhle dokonce říká, že se ta třída chová jako PROXY nad ActiveRow a doufám, že všichni, co se naučili s Nette umí i návrhové vzory
Pouč mne prosím, myslel jsem, že všechny návrhové vzory znám, ale „osekaná proxy“ mne minula.
Je to opět zbytečná diskuze, dal jsem dotaz, tazatel to upřesnil ukázkou. Tvá intervence je dost zbytečná :)
- Polki
- Člen | 553
@MarekBartoš no jo nekdy musis proste vyjadrit co myslis presne a
kvuli tomu to je pak dlouhe. Proto treba nektere matematicke dukazy, ktere
popisuji uplnou banalitu jsou nekolikastrankove.
Skoda je, jen to, ze i s detailnim popisem jsou lidi, kteri to i tak
nepochopi a nevidi souvislosti. To je pak zbytecne psat to no 😄
- Bulldog
- Člen | 110
Hoši hoši. To člověk chce úplně ráno zapnou práci, kouknout se jak
dopadlo vlákno na fóru a vidět tohle…
@Polki Vždyť je to jedno měl jsem za to sice, že jsem použil dost
implikace ve svých výrocích, ale může se stát, že jsem to napsal
špatně, nebo to někdo nevidí. Zeptat se je podle mě v pohodě. Jak se
říká líná huba holý neštěstí.
@KamilValenta Ohledně toho, co jsi napsal Polkimu, tak třeba my ve firmě jedem podle toho, jak to psal on v tom příspěvku už pěkně dlouho. Jediný, proč jsme mu nedali zapravdu je, že nám přišlo zbytečný se kvůli tomu registrovat na fóru. Ostatně si můžeš na netu sám dohledat, že stored procedures vznikly jen proto, že programátoři neumí psát efektivní SQL dotazy a díky tomu potom dotazy dost zhoršovaly performance aplikace, tak experti na databáze přidali do DB systémů procedury, díky kterým oni sami mohli napsat efektivní dotazy na straně DB a programátorům zprostředkovávali jen ‚API‘ s těmito procedurami, což je podle tvého popisu v tom vlákně i tvůj případ.
Každopádně bych ocenil, kdyby jsme řešili jen to, o čem téma je. Svoje problémy si můžete říct soukromě díky. :)
- Kamil Valenta
- Člen | 695
Bulldog napsal(a):
že stored procedures vznikly jen proto, že programátoři neumí psát efektivní SQL dotazy
Nebo chtěli snížit traffic, nebo chtěli cachovat prováděcí plán, nebo
chtěli grantovat oprávnění jen na část věty.
Každopádně to vlákno není jen o SP, je o tom, že DB má spoustu
„non-crud“ dotazů, kterých se musíš zříct jen proto, aby sis mohl
říct, že používáš jen Explorer a paranoidně se nebál poslat query
ad-hoc.
programátorům zprostředkovávali jen ‚API‘ s těmito procedurami, což je podle tvého popisu v tom vlákně i tvůj případ.
Což vůbec není můj případ, popisoval jsem API 3. stran, kdy prostě nemáš jinou možnost. Nebo ano, můžeš klienta odmítnout, ale stále nechápu proč.