Problem s Joinováním 3 tabulek
- QwetakCZ
- Člen | 4
Zdravim pánové a dámy, prosím pěkně, už si nevím rady. Mám tento SQL dotaz
SELECT
user.*,
user_employee.*,
IF(user_role.user_role__role_id = 4 AND user_role.user_role__user_active = 1, 1, NULL) AS role_check
FROM
user_employee
JOIN
user ON user.user__id = user_employee.user_employee__user_id
LEFT JOIN
user_role ON user_role.user_role__user_id = user.user__id AND user_role.user_role__role_id = 4 AND user_role.user_role__user_active = 1
WHERE
user_employee.user_employee__active = 1
Potřebuji ho dostat do nette database explorer … aby mě vracel Selection … měl jsem tu pokusy, ale nette mě neustále hlási, že nemám nastavené klíče “z každé na každou” tabulku. Mohl by mi někdo poradit, jak tento problém vyřešit. Budu moc vděčný …
Předem mnohokrát děkuji …
QwetakCZ
- m.brecher
- Generous Backer | 871
@QwetakCZ
nette mě neustále hlási, že nemám nastavené klíče “z každé na každou” tabulku.
A máš v databázi správně nastavené klíče ?? Pošli přehlednou strukturu tabulek a jak máš nastavené cizí klíče! Dále pošli ukázku kódu pro Nette Explorer, který Ti nefungoval.
- QwetakCZ
- Člen | 4
A máš v databázi správně nastavené klíče ?? Pošli přehlednou strukturu tabulek a jak máš nastavené cizí klíče! Dále pošli ukázku kódu pro Nette Explorer, který Ti nefungoval.
Kliče mám nastavene z user_employee na user a z user_role na user …
Struktura tabulek:
CREATE TABLE `user` (
`user__id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user__password` varchar(255) DEFAULT NULL,
`user__email` varchar(254) NOT NULL,
`user__name` varchar(100) DEFAULT NULL,
`user__surname` varchar(100) DEFAULT NULL,
`user__active` tinyint(1) DEFAULT 1,
PRIMARY KEY (`user__id`),
UNIQUE KEY `email` (`user__email`)
) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `user_employee` (
`user_employee__user_id` int(10) unsigned NOT NULL,
`user_employee__employee_phone` varchar(100) DEFAULT NULL,
`user_employee__employee_email` varchar(254) DEFAULT NULL,
`user_employee__active` tinyint(4) DEFAULT 1,
PRIMARY KEY (`user_employee__user_id`),
CONSTRAINT `user_employee_ibfk_1` FOREIGN KEY (`user_employee__user_id`) REFERENCES `user` (`user__id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `user_role` (
`user_role__user_id` int(8) unsigned NOT NULL,
`user_role__role_id` int(2) unsigned NOT NULL,
`user_role__user_active` int(1) unsigned NOT NULL DEFAULT 1,
UNIQUE KEY `user_id` (`user_role__user_id`,`user_role__role_id`),
KEY `user_role_ibfk_02` (`user_role__role_id`),
CONSTRAINT `user_role_ibfk_01` FOREIGN KEY (`user_role__user_id`) REFERENCES `user` (`user__id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_role_ibfk_02` FOREIGN KEY (`user_role__role_id`) REFERENCES `user_setting_role` (`user_setting_role__id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
a tady jeden ze zkusebnich kodu >> ,
public function getSelection(): Selection
{
$rows = $this->database->table('user_employee')
->select('user.*, user_employee.*, IF(user_role.user_role__role_id = 4 AND user_role.user_role__user_active = 1, 1, NULL) AS role_check')
->where('user_employee.user_employee__active', 1)
->joinWhere('user', 'user.user__id = user_employee.user_employee__user_id')
->joinWhere('user_role', 'user_role.user_role__user_id = user.user__id AND user_role.user_role__role_id = 4 AND user_role.user_role__user_active = 1', 'LEFT');
bdump($rows->fetchall());
return $rows;
}
v SQL me dotaz funguje, a vráti mi řádky, které očekávam
- QwetakCZ
- Člen | 4
Ve stručnosti, potřebuji vytahnout všechny „zaměstnance“, kteři jsou aktivní, zobrazit zakladni hodnoty z tabulky ‚user‘ a z tabulky ‚user_role‘, pokud jejích id má ve sloupci ‚user_role__role_id‘ hodnotu 4 a zároveň ve sloupci ‚user_role__user_active‘ 1 tak v mém případě vypsat například „1“ a pokud tam tento záznam není, tak například „null“ …
- m.brecher
- Generous Backer | 871
@QwetakCZ
Ahoj,
jdeš na to zbytečně komplikovaným SQL způsobem, Explorer nabízí jiné elegantní techniky. Vyzkoušel jsem si to a našel způsob, jak požadovaná data Explorer stylem vytáhnout.
Protože názvy sloupců jak je máš v tabulkách jsou zbytečně komplikované, tak jsem si Tvoje tabulky přepsal srozumitelně, abych se v tom lépe vyznal.
Přepsané tabulky:
-- Adminer 4.8.1 MySQL 8.0.17 dump
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
SET NAMES utf8mb4;
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(25) COLLATE utf8mb4_czech_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) COLLATE utf8mb4_czech_ci NOT NULL,
`surname` varchar(30) COLLATE utf8mb4_czech_ci NOT NULL,
`email` varchar(40) COLLATE utf8mb4_czech_ci NOT NULL,
`is_employee` tinyint(4) NOT NULL,
`active` tinyint(1) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user_employee`;
CREATE TABLE `user_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`phone` varchar(20) COLLATE utf8mb4_czech_ci NOT NULL,
`email` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_czech_ci NOT NULL,
`active` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`),
CONSTRAINT `user_employee_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
`active` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `role_id` (`role_id`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
-- 2024-10-20 23:59:57
A teď dotazy v Exploreru.
Modelová třída s metodou pro výběr z tabulky user:
class UserRepository
{
public function __construct(private Explorer $explorer)
{}
public function getUsers(): Selection // varianta se sloupcem is_employee
{
return $this->explorer->table('user')
->where('is_employee', true)
->where(':user_role.role_id', 1)
->where(':user_role.active', true);
}
public function getUsers(): Selection // varianta bez sloupce is_employee
{
return $this->explorer->table('user')
->where(':user_employee.id NOT', null)
->where(':user_role.role_id', 1)
->where(':user_role.active', true);
}
}
Jestli je uživatel zaměstnanec nebo ne je lepší explicitně vyjádřit boolean polem is_employee, je to čitelnější než pouhá existence dat v připojené tabulce a lépe se s tím pracuje, ale filtraci zaměstanců lze provést i bez pole is_employee dotazem ->where(‚:user_employee.id NOT‘, null)
Left join ani nevím zda v Exploreru nějak schůdně funguje, metodu joinWhere() kterou Jsi použil jsem ani v dokumentaci nikde nenašel. Explorer ale umí skvěle dotahovat data z related tabulek a toho využijeme v šabloně:
{block 'content'}
<div class="box-container product-container">
<table>
<tr>
<td>Uživatel</td>
<td>Aktivní</td>
<td>Zaměstnanec</td>
<td>Email</td>
<td>Telefon</td>
<td>Role</td>
</tr>
{foreach $users as $usr}
{varType Nette\Database\Table\ActiveRow $usr}
{var
$employee = $usr->related('user_employee')->fetch(),
$userRoles = $usr->related('user_role')->where('user_role.active', true)
}
<tr class="body">
<td>{$usr->name} {$usr->surname}</td>
<td>{$usr->active}</td>
<td>{$usr->is_employee}</td>
<td>{$employee?->email}</td>
<td>{$employee?->phone}</td>
<td>
{foreach $userRoles as $userRole}
{$userRole->role->title}{sep}, {/sep}
{/foreach}
</td>
</tr>
{/foreach}
</table>
</div>
{/block}
Otestoval jsem si to a vybírá to správná data podle Tvých požadavků. Jak vidíš v tříde UserRepository jsou vlastní where podmínky velmi jednoduché a na první pohled srozumitelné !! Důležité je pochopit úlohu dvojtečky, ta označuje, že tabulka je ve vztahu k hlavní tabulce related čili podřízená (child) nebo také s vazbou hasMany – na rozdíl od tečky, která označuje referenced tabulky, neboli nadřízené (parent)
Kde Jsi zjistil, jaké parametry předat do metody joinWhere(), protože v dokumentaci tato metoda není popsána a to co je v API dokumentaci je nedostatečné pro použití ??
Metodu joinWhere() jsem zkoušel, ale Explorer po mě chtěl foreign keys obráceným způsobem než jsou v databázi, což ale nejde udělat, to by nefungovalo.
Editoval m.brecher (21. 10. 2:37)
- m.brecher
- Generous Backer | 871
@QwetakCZ
Varianta s použitím metody joinWhere()
Řešení co jsem poslal sice funguje jak má, ale nevýhodou je skládání dat tabulek user a user_employee až v šabloně – což není estetické. Lepší by bylo již v modelové třídě provést klasický LEFT JOIN.
Zkusil jsem použít metodu Selection::joinWhere(), která je pro mne v Exploreru novinkou:
public function getUsers(): Selection
{
return $this->explorer->table('user')
->joinWhere('user_employee', 'user_employee.user_id = user.id');
}
skončil jsem na výjimce:
Nette\InvalidArgumentException
No reference found for $user->user_employee.
Vazba kterou máme user_employee > user není hasMany, ale 1:1, protože user může mít volitelně max. jeden související záznam v user_employee. Můžeme tedy vazbu (cizí klíč) mezi tabulkami user a user_employee otočit, abychom vyhověli požadavku výjimky. Zde je změněná struktura databáze – cizí klíč je otočen: user.user_employee_id > user_employee.id
-- Adminer 4.8.1 MySQL 8.0.17 dump
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
SET NAMES utf8mb4;
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(25) COLLATE utf8mb4_czech_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_employee_id` int(11) DEFAULT NULL,
`name` varchar(30) COLLATE utf8mb4_czech_ci NOT NULL,
`surname` varchar(30) COLLATE utf8mb4_czech_ci NOT NULL,
`email` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_czech_ci NOT NULL,
`active` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_employee_id` (`user_employee_id`),
KEY `name` (`name`),
KEY `surname` (`surname`),
KEY `email` (`email`),
CONSTRAINT `user_ibfk_1` FOREIGN KEY (`user_employee_id`) REFERENCES `user_employee` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user_employee`;
CREATE TABLE `user_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`employee_email` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_czech_ci NOT NULL,
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_czech_ci NOT NULL,
`employee_active` tinyint(1) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
`active` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `role_id` (`role_id`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci;
-- 2024-10-21 14:26:23
model s upravenou podmínkou joinWhere():
public function getUsers(): Selection
{
return $this->explorer->table('user')
->joinWhere('user_employee', 'user.user_employee_id = user_employee.id');
}
šablona latte:
{foreach $users as $id => $usr}
<tr>
<td>{$usr->name} {$usr->surname}</td>
<td>{$usr->email}</td>
<td>{$usr->active}</td>
<td>{$usr->user_employee_id}</td>
<td>{$usr?->employee_email}</td>
<td>{$usr?->phone}</td>
<td>{$usr?->employee_active|boolean}</td>
</tr>
{/foreach}
skončíme na výjimce:
Nette\MemberAccessException
Cannot read an undeclared column 'employee_email'
Explorer neumí automaticky přidat výběr sloupců joinované tabulky – vygeneruje tento SQL:
SELECT `user`.*
FROM `user`
LEFT JOIN `user_employee` ON `user`.`user_employee_id` = `user_employee`.`id` AND
(`user`.`user_employee_id` = `user_employee`.`id`)
Upravíme modelovou třídu – přidáme select(‚*‘):
public function getUsers(): Selection
{
return $this->explorer->table('user')
->select('*')
->joinWhere('user_employee', 'user.user_employee_id = user_employee.id');
}
Výborně, funguje to, vygenerovaný SQL (po odstranění duplicitní podmínky):
SELECT *
FROM `user`
LEFT JOIN `user_employee` ON `user`.`user_employee_id` = `user_employee`.`id`
A šablona mě vypisuje joinované tabulky.
Spustím vygenerovaný SQL dotaz v Admineru – vypíše jiný počet řádků, v jiném pořadí než latte :(
zkusím vykreslit pro kontrolu $id a $usr->id – měly by to být identické hodnoty user.id:
{foreach $users as $id => $usr}
<tr>
<td>{$id}</td>
<td>{$usr->id}</td>
<td>{$usr->name} {$usr->surname}</td>
{* ..... *}
</tr>
{/foreach}
…nejsou, $usr->id je user_employee.id, v $id není ani user.id, ani user_employee.id, ale nějaká číselná hodnota, která ani nesouvisí s konkrétním řádkem, a podle ->order() se může pro stejný řádek měnit :(
Zkouším joinované tabulky setřídit:
public function getUsers(): Selection
{
return $this->explorer->table('user')
->select('*')
->joinWhere('user_employee', 'user.user_employee_id = user_employee.id')
->order('user.name')
// ->order('user.surname')
// ->order('user.email')
;
}
Setřídění podle sloupce name, email je OK, podle surname třídění nefunguje. Horší ale je, že jednotlivé podmínky pro třídění mění filtrování výsledných řádků :(
Zkusím ještě, zda funguje výpis rolí uživatele, který ve včerejším řešení fungoval:
{foreach $users as $id => $usr}
{var $userRoles = $usr->related('user_role')}
<tr>
<td>{$usr->name} {$usr->surname}</td>
{* ..... *}
<td>
{foreach $userRoles as $userRole}
{$userRole->role->title}{sep}, {/sep}
{/foreach}
</td>
</tr>
{/foreach}
skončím na TypeError v Exploreru:
TypeError
Nette\Database\Table\GroupedSelection::setActive(): Argument #1 ($active) must be of type string|int, null given, called in ...vendor\nette\database\src\Database\Table\Selection.php on line 957
Závěr
Použít metodu joinWhere() pro uvedené 4 tabulky se podařilo otočením směru cizího klíče mezi tabulkami user a user_employee a tento směr je asi i správné řešení.
Bohužel, přestože Explorer generuje správné SQL s LEFT JOIN-em, tak v šabloně nevypisuje správná data a navazující funkce třídění nefungují. Zřejmě toto bude důvod, proč není joinWhere() v oficiální dokumentaci a bezpochyby je dobré se jeho použití vyhnout. Což je škoda, protože by se výborně hodil pro spojování tabulek 1:1 :(
Editoval m.brecher (21. 10. 16:51)
- QwetakCZ
- Člen | 4
Mockrát děkuji za snahu, bohužel projekt je už ve fázi několika měsíčního vývoje a nedokáži tam změny takto zakomponovat. Tudíž nejjednodušší řešení vyhrálo a to, že do user_employee přidame jak jste navrhoval sloupec, na který se ptáme v tabulce user_role a hodl tato informace bude duplicitni …
Děkuji mnohokrát za pomoc :)