Jak správně řešit N+1 výpis pomocí DBALu

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

Zajímá mě, jak správně řešit N+1 výpis pomocí DBALu, tedy bez použití ORM. Mám například takovou strukturu databáze.

Pomocí Dibi bych vždy vybral z každé tabulky jedním dotazem všechny potřebné záznamy podle ID, podobně jako to dělá NDBT:

$main = new \StdClass;
$main->articles = $dibi->select('a.*')->from('article')->as('a')
    ->orderBy('a.title')
    ->fetchAssoc('id');

$main->comments = $dibi->select('*')->from('comment')
    ->where('article_id IN %l', array_keys($main->articles))
	->orderBy('id ASC')
    ->fetchAssoc('article_id[]');

$main->tags = $dibi->select('at.*, t.*')->from('article_tag', 'at')
    ->innerJoin('tag', 't')->on('at.tag_id=t.id')
    ->where('article_id IN %l', array_keys($main->articles))
	->orderBy('tag ASC')
    ->fetchAssoc('article_id[]');

$main->users = $dibi->select('u.*')->from('user', 'u')
	->where('u.id IN %l', Arrays::mergeUnique(
        Arrays::getNumbersByKeys(['user_id', 'editor_id'], $main->articles, 1, TRUE),
        Arrays::getNumbersByKeys('user_id', [$main->tags, $main->comments], 3, TRUE)
    ))
	->fetchAssoc('id');

Problém je, když je výsledné pole složitější a nemohu použít array_keys(). Napsal jsem si proto metodu Arrays::getNumbersByKeys(), která vyhledá hodnoty vybraných klíčů v zadané hloubce pole. Je to sice trochu prasárna, ale nic lepšího mě nenapadá.

class Arrays extends Nette\Utils\Arrays
{
    public static function mergeUnique(...$arrays): array
    {
        return array_unique(array_merge(call_user_func_array('array_merge', $arrays)));
    }

    public static function getNumbersByKeys($columns, array $array, int $depth, bool $requireKeys = FALSE): array
    {
        return Arrays::mergeUnique(self::getNumbersByKeys_recursion($columns, $array, $depth, $requireKeys));
    }

	protected static function getNumbersByKeys_recursion($columns, &$array, $depth, $requireKeys)
	{
        if ($depth < 0) return [];
		if (!is_array($columns)) $columns = [$columns];
		$values = [];

		if ($depth == 0 && $requireKeys && (is_object($array) || is_array($array)) && !empty((array)$array)) {
			$foundKeys = 0;
			foreach ((array)$array as $key => $val) {
				if (is_scalar($key) && in_array((string)$key, $columns)) {
					$foundKeys++;
				}
			}
			if (count($columns) != $foundKeys) {
				throw new Exception('Na teto urovni nebyl nektery ze zadanych klicu nalezen');
			}
		}
        foreach ((array)$array as $key => $val) {
            if (is_scalar($key) && is_scalar($val) && in_array((string)$key, $columns) && $depth == 0) {
                if ($val > 0) {
                    $values[] = $val;
                }
            } else if (is_array($val) || is_object($val)) {
                $res = self::getNumbersByKeys_recursion($columns, $val, $depth-1, $requireKeys);
                $values = array_merge($res, $values);
            }
        }
		return $values;
	}
}

V šabloně se to pak vypisuje stylem:

{foreach $main->articles as $a}
    <h3>{$a->title}</h3>
	Napsal: {$main->users[$a->user_id]->name}
    {if !empty($main->users[$a->editor_id])} | Upravil: {$main->users[$a->editor_id]->name}{/if}
    | Tagy: {if !empty($main->tags[$a->id])}
        {foreach $main->tags[$a->id] as $t}
            {$t->tag} (#{$t->tag_id}{if !empty($main->users[$t->user_id])} by {$main->users[$t->user_id]->name}{/if}){sep}, {/sep}
        {/foreach}
    {else} Nemá {/if}
{/foreach}
  • Další způsob je údaje z tabulky user vybírat u každýho dotazu joinem, to se mi ale nelíbí.
  • Nebo by šlo vybírat všechny data joinem jako to dělá Doctrine, ale fetchAssoc() tak pokročilé formátování neumí.
  • Použití NDBT zavrhuju, protože se drží své filosofie a nedovolí mi položit libovolný dotaz, navíc vybízí k volání metod v šabloně, což je prasárna.
  • Chci se vyhnout skládání výsledků dotazů cyklem před předáním do šablony.

Editoval David Klouček (28. 2. 2017 22:40)

CZechBoY
Člen | 3608
+
0
-

Nejdriv by ses mel zamyslet co opravdu potrebujes. Vybirat vsechny clanky a k nim vsechny komentare atd. mi neprijde moc uzitecny…
Nebo delas RSS?

David Klouček
Člen | 57
+
0
-

CZechBoY napsal(a):

Nejdriv by ses mel zamyslet co opravdu potrebujes. Vybirat vsechny clanky a k nim vsechny komentare atd. mi neprijde moc uzitecny…
Nebo delas RSS?

Jde jen o příklad a zajímá mě, jak tohle řešit.