Opakované načítání dat z modelu v renderu při použití handle metody

Neonode
Člen | 9
+
0
-

Ahoj, nejdřív zjednodušeně co a jak teď mám.

V presenteru si do komponenty prostrčim selection.

   protected function createComponentTable(): TableControl
    {
        return $this->tableControlFactory->create($this->facade->findAll());
    }

V komponentě načtu data.

    public function getData(): Selection
    {
        return $this->items->page($this->page, $this->per_page, $this->total);
    }

A potom v render metodě té komponenty pošlu data do šablony.

   public function render(): void
    {
        $this->template->items = $this->getData();
    }

A nakonec to o co mi jde, handle metoda na otevření modal okna

   public function handleModal(): void
    {
        $this->payload->modalId = 'table';
        $this->payload->showModal = true;
        $this->redrawControl('table_modal');
    }

A všechno funguje dobře. Jen mi hrozně vadí, že jelikož je render metoda té komponenty volaná pokaždé, tedy i při handleModal(), tak i při otevření modal okna se mi znova načítaj data z DB.

Můžu nějak přeskočit překreslení a tim nenačítat data? Nebo mám mít načítání někde jinde, nějak jinak? Asi to můžu nějak oifovat, nebo použít cache, ale doufám existuje nějaké další řešení :)

Díky moc!

Editoval Neonode (8. 10. 2023 12:11)

m.brecher
Generous Backer | 762
+
0
-

@Neonode

V presenteru si do komponenty prostrčim selection.

Doporučuji dodržovat standardní MVC a využívat DI, kód je potom přehlednější. Místo předání Selection z presenteru do komponent factory předat pomocí DI celou modelovou třídu Facade rovnou do komponenty (jako službu) a v komponentě si z této třídy vytahat data jak potřebuješ.

Metodou create() komponent factory používej na předání parametrů z presenteru (typicky $id záznamu v databázi z requestu)

A všechno funguje dobře. Jen mi hrozně vadí, že jelikož je render metoda té komponenty volaná pokaždé, tedy i při handleModal(), tak i při otevření modal okna se mi znova načítaj data z DB.

Obvykle jeden sql dotaz navíc nijak výkonově ničemu nevadí, přeskočit vykreslení asi nepůjde. Co by se dalo udělat je cachování Selection v modelové třídě, ale otázkou je zdali potenciální potíže a práce navíc za to stojí.

Neonode
Člen | 9
+
0
-

Ahoj, díky moc za reakci.
Ta komponenta by ideálně měla být univerzální, rsp tak nějak si to představuju. Takže komponenta co udělá tabulku s výpisem nějakých dat. A co to je za data, by nemělo být definováno v té komponentě, ale měl bych data do té komponenty předat ne? Vůbec se nehádám, jen si představuju, že když budu mít pět stránek, na každé stejná tabulka – jen jiná data, měla by ta komponenta s tabulkou jen vypsat data co dostane.

Obvykle jeden sql dotaz navíc nijak výkonově ničemu nevadí, přeskočit vykreslení asi nepůjde. Co by se dalo udělat je cachování Selection v modelové třídě, ale otázkou je zdali potenciální potíže a práce navíc za to stojí.

Jasný, díky. Mě to vadí protože v kombinaci s tím, že při použití page pro paginaci, to vždy dělá i dotaz na počet stránek, **SELECT COUNT(*) ** . Takže je tam těch dotazů navíc víc.
Ale máš pravdu, že to není to nejdůležitější, jen mě zajímalo jak by se to dalo elegantně vyřešit.

mystik
Člen | 292
+
0
-

Tak on se ten dotaz dela protoze se pocet mezitim mohl zmenit. Pokud to nechces znavo nacitat a nevadi ti ze muze jit o nekatualni pocet tak muzes pridat nejakou cache.

mystik
Člen | 292
+
+1
-

Ted jak to ctu podruhe tobe jde o to ze v te komponente v tom modal ukazujes jen jeden radek takze nechces nacitat vsechna data, kdyz potrebujes jen jeden prvek pro presleni AJAXem? Tak je pak reseni v tom ze handle zjisti zda jde o AJAX a pokud ano tak nacte data jen pro ten jeden prvek (prida podminku na selection) a ulozi do property. A v render data nacitas jen pokud je nenacetl handle tj pokud ta property je null.

m.brecher
Generous Backer | 762
+
0
-

@Neonode

Ta komponenta by ideálně měla být univerzální

OK, pak je to v pořádku, přehlednější zápis by byl třeba takhle (to ale není tak důležité):

// místo:
protected function createComponentTable(): TableControl
{
    return $this->tableControlFactory->create($this->facade->findAll());
}

//takhle:
protected function createComponentTable(): TableControl
{
    $table = $this->tableControlFactory->create();
    $table->setRows($this->facade->findAll());
    return $table;
}

// s použitím pojmenovaných parametrů
protected function createComponentTable(): TableControl
{
    return $this->tableControlFactory->create(
        rows: $this->facade->findAll(),
    );
}

Co se týče duplicity sql dotazů, není mě úplně jasné, jak ta komponenta má fungovat. Máš nějaký tabulkový výpis a po kliknutí na jeden řádek chceš otevřít ajaxem přes tu tabulku modální překryvné okno, ve kterém by byly data jednoho řádku ??

mystik
Člen | 292
+
+1
-

Zhruba takhle nějak:

    private array $rows;

    public function getData(): Selection
    {
        return $this->items->page($this->page, $this->per_page, $this->total);
    }

   public function render(): void
    {
        $this->template->items = $this->rows ?? $this->getData();
    }

   public function handleModal(int $rowId): void
    {
        $this->payload->modalId = 'table';
        $this->payload->showModal = true;
        if($this->isAjax()) {
            $this->rows = $this->items->where('id', $rowId);
        }
        $this->redrawControl('table_modal');
    }

Editoval mystik (8. 10. 2023 14:20)

Neonode
Člen | 9
+
0
-

@mystik

Tak je pak reseni v tom ze handle zjisti zda jde o AJAX a pokud ano tak nacte data jen pro ten jeden prvek (prida podminku na selection) a ulozi do property. A v render data nacitas jen pokud je nenacetl handle tj pokud ta property je null.

To jsem zkoušel, ale on je problém v tom, že když v renderu třeba s použitím if ($this->httpRequest->getQuery(‚do‘) ..... ) { data nenačtu, pokud je to openModal, tak pak se komponenta překreslí jako prázdná.
Takže mám query pro detail by id, query pro list, query pro COUNT – jen tím, že otevřu modal pro editaci itemu.

 private array $rows;

    public function getData(): Selection
    {
        return $this->items->page($this->page, $this->per_page, $this->total);
    }

   public function render(): void
    {
        $this->template->items = $this->rows ?? $this->getData();
    }

   public function handleModal(int $rowId): void
    {
        $this->payload->modalId = 'table';
        $this->payload->showModal = true;
        if($this->isAjax()) {
            $this->rows = $this->items->where('id', $rowId);
        }
        $this->redrawControl('table_modal');
    }

Zkusim, díky moc :)

@mbrecher

Díky moc, zkusím setRows.

Co se týče duplicity sql dotazů, není mě úplně jasné, jak ta komponenta má fungovat. Máš nějaký tabulkový výpis a po kliknutí na jeden řádek chceš otevřít ajaxem přes tu tabulku modální překryvné okno, ve kterém by byly data jednoho řádku ??

Přesně, je tabulka s výpisem. Když kliknu na edit, otevře se modal s formulářem a do formu se načtou default hodnoty.
A otevření modalu znamená render komponenty a to znamená že se znova načtou i data do tabulky.
Ale jak říkáš, ono to vlastně ničemu nevadí, jen mě to trošku vnitřně štve :)

Díky moc!

Editoval Neonode (8. 10. 2023 14:27)

mystik
Člen | 292
+
0
-

Pockat takze problem je ze se ti pri AJAXu prekresli cela komponenta kdyz by se ti mel prekreslit jen modal? Jak mas resene AJAX pozadavky? Dival ses co se ti vraci v AJAX response za snipetty k prekresleni?

Co ma delat ta podminka s httpRequest? Je to dost zvlastni kontstrukce.

Editoval mystik (8. 10. 2023 15:15)

Neonode
Člen | 9
+
0
-

@mystik

Hm, to bude nejspíš ten jedinej a hlavní problém, že mám špatně daný snippety v šabloně. A tak se mi překresluje komponenta s tabulkou, když zobrazuju modal okno.

No a je to proto, že mam v presenteru zapomenuté $this->redrawControl();
Ach jo, díky moc!

Editoval Neonode (8. 10. 2023 15:44)

m.brecher
Generous Backer | 762
+
0
-

@Neonode

Už jsem přišel na to, proč se duplikuje sql dotaz pro celou tabulku. Ajax snippety v Nette totiž tímto způsobem pracují – viz dokumentace. Kdykoliv se v komponentě použije v handleru signálu

$this->redrawControl(snippet: 'mySnippet') ;

tak se vykoná komplet metoda render() komponenty a vykreslí se CELÁ šablona, jenom se do prohlížeče neposílá celá, ale požadovaný snippet.

Takže už v principu se sql pro tabulku vykoná duplicitně, přestože není pro vykreslení modálního okna s formulářem jednoho řádku potřeba.

Jde to celkem snadno vyřešit a to tak, že:

a) vytvoříme v komponentě proměnnou která bude řídit vykreslení tabulky nebo modálního okna
b) v metodě render oifujem kód, kde se získávají data pro šablonu tabulky
c) v šabloně oifujeme kód pro tabulku – bez dat by se vyhodila chyba
d) v šabloně oifujeme kód uvnitř snippetu, aby se ve výchozím stavu vykreslil prázdný snippet, jinak nebude redraw fungovat

Ukázka otestovaného kódu, lze použít libovolnou tabulku – vypisují se všechny sloupce:

Presenter:

final class TestPresenter extends Presenter
{
    public function __construct(private readonly MyTableModel $myTableModel)
    {}

    public function createComponentDataTable(): DataTable
    {
        return new DataTable(dataTableModel: $this->myTableModel);
    }
}

Komponenta pro tabulku a editační panel (modální okno)

class DataTable extends Control
{
    private string $view = 'tableView';
    private int $id;

    public function __construct(private TableModel $tableModel)
    {}

    public function handleOpenEditPanel(int $id)
    {
        $this->view = 'itemView';
        $this->id = $id;
        if($this->presenter->isAjax()){
            $this->redrawControl(snippet: 'editPanelWrapper');
            $this->redrawControl(snippet: 'editPanel');
        }
    }

    public function handleCloseEditPanel()
    {
        if($this->presenter->isAjax()){
            $this->redrawControl(snippet: 'editPanelWrapper');
            $this->redrawControl(snippet: 'editPanel');
        }
    }

    public function render()
    {
        match($this->view){
            'tableView' => $this->renderTable(),
            'itemView' => $this->renderForm(),
        };

        $this->template->view = $this->view;
        $this->template->render(__DIR__.'/dataTable.latte');
    }

    private function renderTable()
    {
        $table = $this->tableModel->getAll();
        $this->template->columns = $this->getTableColumns($table);
        $this->template->rows = $table;
    }

    private function renderForm()
    {
        $currRow = $this->tableModel->getOne($this->id);
        $this->template->currRow = $currRow;

        if(!$this->presenter->isAjax()){
            $this->renderTable();

        }else{
            $this->template->columns = $this->getRowColumns($currRow);
        }
    }

    /* utils */

    private function getRowColumns(ActiveRow $row): array
    {
        return array_keys($row->toArray() ?: []);
    }

    private function getTableColumns(Selection $table): array
    {
        return array_keys($table->fetch()->toArray() ?: []);
    }

Šablona tabulky dataTable.latte:

<h2>Table</h2>

{if $view === 'tableView' || !$presenter->isAjax()}
    <table class="table">
        <tr class="heading">
            {foreach $columns as $column}
                <td>{$column}</td>
            {/foreach}
            <td></td>
        </tr>

        {foreach $rows as $row}
            <tr>
                {foreach $columns as $column}
                    <td>{$row->$column}</td>
                {/foreach}
                <td><a n:href="openEditPanel!, id: $row->id" class="text-link ajax">Editovat</a></td>
            </tr>
        {/foreach}

    </table>
{/if}

{snippetArea 'editPanelWrapper'}
        {include 'editPanel.latte', columns: $columns ?? [], row: $currRow ?? null}
{/snippetArea}

Šablona editačního panelu (modální okno) editPanel.latte

{snippet 'editPanel'}
    {if $view === 'itemView'}
        <h2>Edit panel</h2>

        <p><a n:href="closeEditPanel!" class="text-link ajax">Zavřít panel</a></p>

        <table class="table">
            <tr class="heading">
                <td>Sloupec</td>
                <td>Hodnota</td>
            </tr>

            {foreach $columns as $column}
                <tr>
                    <td>{$column}:</td>
                    <td>{$row->$column}</td>
                </tr>
            {/foreach}
        </table>
    {/if}
{/snippet}

Šablona akce presenteru default.latte:

{block 'content'}

    {control 'dataTable'}

{/block}

{block 'head'}
    {include parent}

    <script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
    <script>
        naja.initialize();
    </script>

{/block}

Modelová třída:

class MyTableModel implements TableModel
{
    public function __construct(private Explorer $database)
    {}

    public function getAll(): Selection
    {
        return $this->database->table('mytable');
    }

    public function getOne(int $id): ActiveRow|null
    {
        return $this->database->table('mytable')->get($id);
    }
}

Pomocný interface pro modelovou třídu – vynutí existenci používaných metod:

interface TableModel
{
    public function getAll(): Selection;

    public function getOne(int $id): ActiveRow|null;
}

Editační panel se otevírá signálem openEditPanel a zavírá signálem closeEditPanel, aniž by se opakovaně odečítaly data pro tabulku.

Kód funguje i s vypnutým javascriptem.

Vypisuje libovolnou tabulku, v editačním panelu je výpis dat řádku, který lze nahradit editačním formulářem.

Editoval m.brecher (9. 10. 2023 3:09)