Doctrine 2 – OneToOne relace nebo postupné načítání entit

- Magnus
 - Člen | 65
 
Ahoj,
řeším teď jeden problém v teoretické rovině – uvedu názorný
příklad.
Mám entitu User, která obsahuje atributy, jako je jméno, heslo, e-mail atd.
Jelikož v aplikaci přibývá funkčnost, vytváří se v databází čím
dál tím více tabulek, které souvisí s uživatelem (například nastavení,
statistiky, …). Všechny tyto vazby budou v entitě řešené pomocí
OneToOne relací.
Pokud ale vyhledám entitu User, Doctrine k tomu musí načíst všechny tyto
relace, protože je User vždy inverzní stranou.
To znamená, že při jakémkoliv vyhledání entity, se bude muset načíst
hromada dalších dat (a v mnoha případech i zbytečně, protože se
nevyužijí).
Teď tedy otázka – je lepší naházet entitě tyto atributy, nebo vytvořit
v nějaké fasádě metody, které daná data vyhledají jednotlivě?
Příklad:
- atributy v entitě
 
<?php
use Doctrine\ORM\Mapping as ORM;
class User
{
 	/**
	 * @ORM\Column(type="string")
	 */
	private $name;
	/**
	 * @ORM\Column(type="string")
	 */
	private $email;
	/**
	 * @ORM\OneToOne(targetEntity="Settings")
	 */
	private $settings;
	/**
	 * @ORM\OneToOne(targetEntity="Statistics")
	 */
	private $statistics;
	// další atributy s OneToOne relací
}
$user = $em->find(User::class, $id);  // musí načíst i $settings a $statistics, i když nejsou potřeba
?>
- data ve fasádě
 
<?php
class User
{
 	/**
	 * @ORM\Column(type="string")
	 */
	private $name;
	/**
	 * @ORM\Column(type="string")
	 */
	private $email;
}
class Settings
{
	/**
	 * @ORM\Column(type="integer")
	 */
	private $userId;
	// ostatní atributy
}
class UserFacade
{
	/** @var EntityManager */
	private $em;
	public function __construct(EntityManager $em)
	{
		$this->em = $em;
	}
	public function getSettings(User $user)
	{
		return $this->em->getRepository(Settings::class)->findOneBy(array("userId" => $user->getId()));
	}
}
$user = $em->find(User::class, $id);
if ($user !== NULL) {
	$settings = $userFacade->getSettings($user);
	// předání proměnných šabloně apod.
}
?>
Pozn.: Snažil jsem se o co nejstručnější kód, vynechal jsem settery a gettery u entit. Předpokládejme, že $em a $userFacade jsou definované (získané např. z DI Containeru).
Chtěl bych se vás zeptat, jestli je v některých případech lepší použít druhou možnost? Případně jaké mají obě řešení výhody/nevýhody – podle čeho se v určité situaci rozhodnout, které řešení zvolit.
Nesouvisí to sice úplně s Nette, ale je tu sekce ORM a spousta chytrých
lidí, tak snad to nebude vadit.
Děkuji!

- Jan Endel
 - Člen | 1016
 
Ahoj, v DameJidlo jsme řešili velice podobný problém – „vyřešili“ jsme to tak, že jsme všechny vazby oneToOne přepsali na oneToMany, je to více psaní, ale nemusí se donačítat věci co člověk vždy nepotřebuje.
B bych vůbec nepoužíval, zavlečuješ si do kódu zbytečné závislosti, když chceš z usera settings.

- Lukeluha
 - Člen | 130
 
Azathoth napsal(a):
a nepomohlo by nastavit v anotaci té one to one asociace fetch=EXTRA_LAZY?
Řešil jsem úplně stejný problém, dle dokumentace doctrine jsem použil právě fetch=EXTRA_LAZY, což mi ale nepomoho, relace se stále načítaly. Tak nevím, jestli jsem dělal něco špatně, popř. jestli toto není bug @FilipProcházka ?
Editoval Lukeluha (26. 7. 2015 10:45)

- Jan Endel
 - Člen | 1016
 
Podle Doctrine to není bug, ale feature, řešením je jenom jak jsem psal výše, to přepsat na oneToMany – tady si s tím pokecal Honza Tichý s vývojářema Doctrine

- Magnus
 - Člen | 65
 
@JanEndel chápu tedy správně, že máte v podstatě kolekci s jediným prvkem a píšete něco v tomto smyslu?
<?php
public function getSettings()
{
	return $this->settingsCollection[0];
}
?>
Protože záznam v tabulce settings bude k jednomu uživateli
pochopitelně jen jeden. Doctrine ale umí pro kolekce lazy loading, a tak se
další databázový dotaz provede až v případě, kdy tu metodu
getSettings() použiji.
Editoval Magnus (26. 7. 2015 13:13)

- Filip Procházka
 - Moderator | 4668
 
@Magnus jen bych dodal, že přepsání oneToOne na oneToMany je sice funkční, ale nenazývejme to řešením – je to hack.
Jako opravdu správné řešení bych viděl otočit tu vazbu tak, aby ten owner (tedy user) měl skutečně v sobě idčko té entity, kterou „obsahuje“, nikoliv naopak.

- Magnus
 - Člen | 65
 
@FilipProcházka mně to přišlo jako zvláštní řešení, i když
chytré.
Chceš tím říct, že bude např. settings_id v tabulce
user a ne naopak user_id v tabulce
settings?
S Doctrine začínám, tak si nejsem jistý, zda to nejde „obejít“ ještě
jiným způsobem. Omluv tedy, prosím, hloupý dotaz.
Děkuji

- Filip Procházka
 - Moderator | 4668
 
@Magnus hloupý dotaz to rozhodně není.
Dává mi logicky smysl, aby uživatel když „vlastní“ settings, aby věděl které settings vlastně vlastní. Takhle settings ví, že je vlastněn uživatelem a to že se k němu dostaneš i z uživatele řeší Doctrine, což mi dává menší smysl, protože mi to nedává smysl z hlediska objektového návrhu.
Dokonce bych řekl, že je daleko lepší vůbec nedělat bi-directional vazbu, ale nechat jenom tu settings OneToOne z uživatele a v settings vubec nedělat property s uživatelem.
class User
{
    /**
     * @ORM\Column(type="string")
     */
    private $name;
    /**
     * @ORM\OneToOne(targetEntity="Settings")
     * @ORM\JoinColumn(nullable=FALSE)
     */
    private $settings;
    public function __construct()
    {
        $this->settings = new Settings();
    }
}
class Settings
{
    /**
     * @ORM\Column(type="boolean", options={"default":FALSE})
     */
    private $vip = FALSE;
    // ostatní atributy
}
To je právě to přemýšlení v objektech, o kterém každý mluví když se snaží vysvětlit, že s ORM se pracuje maličko jinak než se samotnými tabulkami.

- hrach
 - Člen | 1844
 
Takove malicke offtopic: toto v Nextras Orm funguje automaticky spravne, tj. lazy, ale vcetne zachovani efektivniho donacteni pro celou kolekci.