Nette\Database chyba v případě vazební tabulky s dvou-sloupcovým primárním klíčem

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

Mám následující kód:

$db = $this->context->database;

$applications = $db->table('application');
$application = $applications[1];

foreach ($application->related('application_tag') as $application_tag) {
	Debugger::dump($application_tag->tag->name);
}

Při prvním spuštění správně vydumpuje:

"PHP" (3)
"MySQL" (5)

Při následném spuštění (tj. s vygenerovanou cache) už nevydumpuje nic.

Schema databáze je z NotORM balíčku – soubor tests\software.sql.

Pokud upravím tabulku application_tag tak, že ji přidám sloupec id, ze kterého udělám primární klíč, vše funguje, jak má.

hrach
Člen | 1834
+
0
-

Na toto je napsan prochazejici test. Kde je tedy chyba? Typ db? Cache? Jaka? Verze nette?

duke
Člen | 650
+
0
-

Procházení samo (bez cache) funguje (proto zřejmě projde i ten test). Nefunguje to až s použitím cache (tj. v případě opakovaného requestu).

  • PHP: 5.3.8
  • DB: MySQL 5.1.58
  • Cache: výchozí (Nette\Caching\Cache, Nette\Caching\Storages\FileStorage)
  • Nette: 2.1-dev

Editoval duke (27. 2. 2012 18:34)

Aearsis
Člen | 57
+
0
-

Potvrzuju chybu, Mysql 5.1.61, Nette 2.0 – jde o to, že v ActiveRow se v ->access dá do id pole $this->data, a pak se zavolá offsetGet s polem, což zůstalo možná z NotORM, který tohle přechroustá, Database ale ne – přímo vrací $this->data[$key], což skončí Illegal offset type warningem.

hrach
Člen | 1834
+
0
-

Divné, je na to test:
https://github.com/…e.cache.phpt#L41

Zitra odjizdim na 2dny, bohuzel se k tomu nedostanu driv nez v patek.

duke
Člen | 650
+
0
-

Několik dalších poznatků:

$tags = array();
foreach ($connection->table('book') as $book) {
	foreach ($book->related('book_tag') as $book_tag) {
		$tags[] = array($book->title => $book_tag->tag->name);
	}
}

Debugger::dump($tags);

Při prázdné cache, vydumpuje:

array(6) [
   0 => array(1) {
      "1001 tipu a triku pro PHP" => "PHP" (3)
   }
   1 => array(1) {
      "1001 tipu a triku pro PHP" => "MySQL" (5)
   }
   2 => array(1) {
      JUSH => "JavaScript" (10)
   }
   3 => array(1) {
      Nette => "PHP" (3)
   }
   4 => array(1) {
      Dibi => "PHP" (3)
   }
   5 => array(1) {
      Dibi => "MySQL" (5)
   }
]

Při naplněné cache, vydumpuje:

array(3) [
   0 => array(1) {
      Dibi => "PHP" (3)
   }
   1 => array(1) {
      Dibi => "MySQL" (5)
   }
   2 => array(1) {
      JUSH => "JavaScript" (10)
   }
]

A dále jsem objevil zajímavý rozdíl v chování u následujícího kódu (viz komentáře):

$books = $connection->table('book');
$book = $books[1]; // při použití tohoto řádku nebude fungovat s cache
//$book = $books->where('id', 1)->fetch(); // při použití tohoto bude fungovat i s cache

$tags = array();
foreach ($book->related('book_tag') as $book_tag) {
	$tags[] = $book_tag->tag->name;
}

Debugger::dump($tags);
hrach
Člen | 1834
+
0
-

Tento kod:

$tags = array();
foreach ($connection->table('book') as $book) {
        foreach ($book->related('book_tag') as $book_tag) {
                $tags[] = array($book->title => $book_tag->tag->name);
        }
}

Debugger::dump($tags);

mi funguje i při zapnutí cache. Jediný rozdíl mám pak v pořadá položek v $tags, ale vazby title a tag->name jsou správně.

Ohledně volání

$book = $books[1];

tak to určitě není věřejně podporované, nicméně mně také funguje. Nevím, která cache ti v danou chvíli nemá fungovat.

Jsem z toho jelen.

Editoval hrach (3. 3. 2012 14:35)

Honza Marek
Člen | 1664
+
0
-

Ahoj, posílám nějaký kód pro reprodukci chyby.

CREATE TABLE `tag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(60) NOT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE `article` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) NOT NULL,
  `text` text,
  PRIMARY KEY (`id`)
);

CREATE TABLE `article_tag` (
  `article_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`article_id`,`tag_id`),
  CONSTRAINT `article_tag_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`),
  CONSTRAINT `article_tag_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`)
);

INSERT INTO `article` (`id`, `name`, `text`) VALUES
(1,	'Článek',	'Obsah'),
(2,	'Článek 2',	'taky obsah');

INSERT INTO `tag` (`id`, `name`) VALUES
(1,	'kategorie');

INSERT INTO `article_tag` (`article_id`, `tag_id`) VALUES
(1,	1),
(2,	1);
// stačí narvat do bootstrapu
foreach ($container->database->table('article') as $article) {
	echo $article->name . ': ';
	foreach ($article->related('article_tag') as $at) {
		echo $at->tag->name . ' ';
	}
	echo '<br>';
}

Poprvý to vypíše kategorii u obou, při druhém přístupu jen u druhého článku.

duke
Člen | 650
+
0
-

Zkoušel jsem trochu experimentovat a ačkoliv nemám zcela přehled nad kódem NDB, mám za to, že problém je v tom, že se do cache neukládá informace o tom, že tabulka book_tag nemá jednosloupcový primární klíč (naopak se tam později nějak – nevím jak – dostane nesprávná informace: sloupec tag_id).

Jako řešení mi fungovalo nahradit v souboru Nette\Database\Reflection\DiscoveredReflection.php kód na řádku 91:

if ($primaryCount !== 1)
	return NULL;

kódem:

if ($primaryCount !== 1)
	$primary = '';

Záměrně jsem použil '' místo NULL, kvůli výše použitému isset. Netestoval jsem ale, zda to neporuší něco jiného, takže to berte spíš jako nasměrování, než jako hotové řešení.

hrach
Člen | 1834
+
0
-

@duke: jo, to bude přesně ono, proto sem chtěl ten dump db, očividně tam primary je spravně. už jsem zkoušel i situaci, kdy není žádný primary a to funguje taky správně. Teď otázka, proč se vám to chová špatně a mně dobře.

hrach
Člen | 1834
+
0
-

@HonzaMarek: diky, nasimulovano, jdu studovat :))

hrach
Člen | 1834
+
0
-

Tak, uz vim proc mi testy v poradku prochazeli. Mam nastavenou cache jen pro Connection a ne pro Reflection → necachoval se primary klic, ktery se spatne cachoval, na coz prisel Duke. Diky moc vsem. Hned pripravim testy a opravu :)

H

David Růžička
Člen | 43
+
0
-

Potvrzuji, že mám také problém s tabulkou se dvěma primárními klíči. Projevuje se jen když mám prázdnou keš, pak stačí obnovit stránku a vše začne fungovat.

hrach
Člen | 1834
+
0
-

@David Růžička: to je něco jineho, než se tu řešilo, to už jsem opravil, ale ještě to není v nightly. Tady toto bohužel při refreshi nezačlo fungovat, ale naopak nefungovat. Založ prosím nové vlákno.

OscarHanzely
Člen | 7
+
0
-

Potvrzuji puvodni chybu stale aktivni i po vice nez roce !

Mam takoveto parametry:
Nette 2.0.7
PHP 5.4.3
MySql 5.5.24
Cache: výchozí

Mam demonstracni schema, kde je vztah M:N pres vazebni tabulku s indexem pres dva sloupce. Zatim nemam projekt, takze tohle je obdoba toho book-book_tag-tag. Napadlo me to jako jednoduche schema user-user_rights_binding-user_rights. Chtel jsem si vyzkouset totoiz praci s Nette\Database

CREATE TABLE user (
	user_id              INT UNSIGNED NOT NULL AUTO_INCREMENT,
	login                VARCHAR( 20 ) NOT NULL,
	password             CHAR( 60 ) NOT NULL,
	name                 VARCHAR( 20 ) NOT NULL,
	CONSTRAINT pk_user PRIMARY KEY ( user_id ),
	CONSTRAINT login UNIQUE ( login )
 );

CREATE TABLE user_rights (
	right_token          VARCHAR( 150 ) NOT NULL,
	description          TEXT,
	CONSTRAINT idx_user_rights PRIMARY KEY ( right_token )
 );

CREATE TABLE user_rights_binding (
	user_id              INT UNSIGNED,
	right_token          VARCHAR( 150 ),
	CONSTRAINT fk_user_to_rights FOREIGN KEY ( right_token ) REFERENCES user_rights( right_token ) ON DELETE CASCADE ON UPDATE CASCADE,
	CONSTRAINT fk_rights_to_user FOREIGN KEY ( user_id ) REFERENCES user( user_id ) ON DELETE CASCADE ON UPDATE CASCADE
 );
CREATE INDEX idx_user_rights_binding ON user_rights_binding ( right_token );
CREATE INDEX idx_user_rights_binding_0 ON user_rights_binding ( user_id );

Vyber skrze tabulky jsem zkousel dle prikladu pres (vybiram popisky prav k uzivateli):

foreach( $this->connection->table( 'user' ) as $oUser ) {
            echo $oUser->login;
            foreach( $oUser->related('user_rights_binding', 'user_id') as $oRight ) {
                echo $oRight->right_token; // tohle funguje z prvni vazebni tabulky
                echo $oRight->user_rights->description; // tohle uz nefunguje
                foreach( $oRight->related('user_rights','right_token') as $result) { // tohle uz taky nefunguje
                    Debugger::dump($result->description);
                }

            }
        }

Skousel jsem nekolim variant a pri prvnim spusteni to hlasilo:
PDOException
No reference found for $user_rights_binding->

a obcas kdyz jsem zakomentoval spatny radek Nette si udelalo indexy pres fk najednou, tak pak uz furt hlasi:
Warning
Illegal offset type
na stejnych mistech kodu.

Slo to pak az do ...\libs\Nette\Database\Table\ActiveRow.php:244
a finalne to kleklo na:
...\libs\Nette\Database\Table\ActiveRow.php:289

Jak se mam tedy dostat pres vazebni tabulku se dvema FK na data ? Podotykam, ze pridani bezvyznamneho primary key do vazebni tabulky prolbem nevyresilo, jen se to pak snazilo pres nej navazat na tabulku user_rights a uplne ignorovalo i to related() :(

Jako zacatecnik v Nette jsem tyto slozite postupy jinak uz nenasel popsane a dost me to odrazuje. Rozhodne Nette\Database asi zavrhnu radeji na zacatku a zkusim dibi, ale byla by to skoda.

Jen podotykam, ze jediny zpusob jak jsem to dostal z databaze je prachsproste ciste SQL, k cemu nepotrebujeme cele Nette\Database :

$aResult = $this->connection->query('SELECT user.login, user_rights.description FROM user LEFT JOIN user_rights_binding ON user.user_id = user_rights_binding.user_id LEFT JOIN user_rights ON user_rights_binding.right_token = user_rights.right_token')->fetch();
echo $aResult->login . ' ' . $aResult->description;

Editoval OscarHanzely (19. 12. 2012 0:06)

vvoody
Člen | 910
+
0
-

Fungovať by malo:

$oRight->ref('user_rights','right_token')->description;

Tak ako si to napísal, to s Discovered reflection ani nemôže fungovať.

Ten následný pokus s related ani netreba opravovať, buď nechápeš typ väzby medzi tvojimi tabuľkami, alebo ti nieje jasné, ktorú metódu použiť na akú väzbu.

OscarHanzely
Člen | 7
+
0
-

vvoody napsal(a):

Fungovať by malo:

$oRight->ref('user_rights','right_token')->description;

Tak ako si to napísal, to s Discovered reflection ani nemôže fungovať.

Ten následný pokus s related ani netreba opravovať, buď nechápeš typ väzby medzi tvojimi tabuľkami, alebo ti nieje jasné, ktorú metódu použiť na akú väzbu.

To co si napisal, mi hadze chybu
Notice
Trying to get property of non-object

Znamena to, ze nevidi v tabulke data na previazanie ?

Data su nasledovne:

INSERT INTO user( user_id, login, password, name) VALUES ( 1, 'oscar', '', 'Oscar' );

INSERT INTO user_rights( right_token, description ) VALUES ( 'admin', 'Admin' );
INSERT INTO user_rights( right_token, description ) VALUES ( 'user', 'Basic user' );

INSERT INTO user_rights_binding( user_id, right_token ) VALUES ( 1, 'admin' );
INSERT INTO user_rights_binding( user_id, right_token ) VALUES ( 1, 'user' );

Vazby v tabulkach user<->user_rights_binding<->user_rights su 1:N:1

Nechapem asi spravne pouzitie metod ref a related. Ale preco v priklade s book-book_tag-tag pouzivaly $Book->tag->name co je podla dokumentacie related a moje $User->user_rights->description je rozdielne.

hrach
Člen | 1834
+
0
-

Nejak nevidim, ze by definice tabulky CREATE TABLE user_rights_binding obsahovala dvojty primarni klic.

OscarHanzely
Člen | 7
+
0
-

Omlouvam se, ja uz zkousim uplne vsechno, tak jsem ho odebral. Samozrejme kdyz zmenim definici tabulky s dvojitym PK takto:

CREATE TABLE user_rights_binding (
	user_id              INT UNSIGNED NOT NULL,
	right_token          VARCHAR( 150 ) NOT NULL,
	CONSTRAINT user_rights_binding_pk PRIMARY KEY ( user_id, right_token ),
	CONSTRAINT fk_user_to_rights FOREIGN KEY ( right_token ) REFERENCES user_rights( right_token ) ON DELETE CASCADE ON UPDATE CASCADE,
	CONSTRAINT fk_rights_to_user FOREIGN KEY ( user_id ) REFERENCES user( user_id ) ON DELETE CASCADE ON UPDATE CASCADE
 );

Tak se chyba nezmeni. :( Uz vazne nevim, co je na tom v zasade uplne spatne.

Dokonce uz jsem mel i pridany sloupec s nazvem id, ktery byl PK, protoze jsem si rikal, ze treba to ocekava na kazdou tabulku jeden hlavni jednosloupcovi PK a pak to hlasilo ty chyby nahore, ze nemuze vubec najit reference pres user_rights_binding. je to tak nestandartne navrzeno ? Dyt jsem okopiroval priklad z tutorialu s knihou, book_tag a tagem.

Editoval OscarHanzely (19. 12. 2012 18:48)

OscarHanzely
Člen | 7
+
0
-

Tak jsem na to konecne asi kapnul:

Vypis vsechna prava(jejich popisky) ktere jsou nabindovane k uzivateli oscar:

foreach( $this->connection->table( 'user' )->where( 'login', 'oscar' ) as $oUser ) {
       // ActiveRow
       echo $oUser->login;
       foreach( $oUser->related( 'user_rights_binding', 'user_id' ) as $oBinding ) {
           // aciveRow
           echo $oBinding->ref('user_rights', 'right_token')->description;
       }

}

Ale musim rict, ze je to tezce neintuitivni co se kde vrati … pac related skoci jen o uroven vis, ale ref vytahne uz rovnou data. Nebo spise se musim zzit s tema castyma foreach, na ktere nejsem zvyklej pri dotazovani do db, ale az pri zpracovani dat.

Jo a stejne je tu jeden problem, bez tech parametru v related a ref, to nedokaze najit vubec zadny zavislosti, protoze bindovaci tabulka ma dvojity primary key. A to nedokaze pak ani ty FK na tabulku user_rights prechroupat :(

Jinak polozilo to nakonec 4 dotazy, misto jednoho JOINu pres dva tabulky. Je to vazne tak brutalne efektivni ?

Pro porovnani rucni dotaz:

SELECT user.login, user_rights.description
FROM user
LEFT JOIN user_rights_binding ON (user.user_id = user_rights_binding.user_id)
LEFT JOIN user_rights ON (user_rights_binding.right_token = user_rights.right_token)
WHERE user.login="oscar"

Cas potrebyny pro ty 4 dotazy byl dle ladenky: 3.602ms
Cas pro vytazeni vseho v jednom dotazu s JOIN: 0.868ms

V cem je vlastne Nette\Database tedy lepsi ?

Editoval OscarHanzely (19. 12. 2012 20:12)