Doctrine rekurze nefunkcni matching

kleinpetr
Člen | 480
+
0
-

Zdravim,

narazil na takovy zvlastni problem, ktery bych si rad ujasnil.

Mam tabulky

country
locality (id, id_parent_locality, name)
city (id, id_locality)

a vysledek je treba v cr

cz(country) -> Ustecky kraj(lokalita) -> Okres teplice(lokaita) -> teplice(mesto)

A ted v localityEntity mam

/**
 * @var CountryEntity
 * @ORM\ManyToOne(targetEntity="CountryEntity", inversedBy="localities")
 * @ORM\JoinColumn(name="id_country", referencedColumnName="id", onDelete="CASCADE", nullable=false)
 */
private $country;

/**
 * @var LocalityEntity
 * @ORM\ManyToOne(targetEntity="LocalityEntity", inversedBy="sub_localities")
 * @ORM\JoinColumn(name="id_locality", referencedColumnName="id", onDelete="CASCADE", nullable=true)
 */
public $parent_locality;

/**
 * @var Collection
 * @ORM\OneToMany(targetEntity="LocalityEntity", mappedBy="parent_locality")
 */
private $sub_localities;

/**
 * @var Collection
 * @ORM\OneToMany(targetEntity="CityEntity", mappedBy="locality", cascade={"persist"})
 */
private $cities;

a v CountryEntity mam

/**
 * @var Collection
 * @ORM\OneToMany(targetEntity="LocalityEntity", mappedBy="country")
 */
private $localities;

Ted mi slo o to udelat si v CountryEntity metodu, ktera mi bude vracet nejake pocty. Takze napr. metodu, ktera vrati pocet lokalit v dane zemi, ktere uz nemaji zadny parent a pote pocet vsech mest v dane zemi pro vsechny lokality. Tak jsem napsal nasledujici v CountryEntity

/**
     * @return array
     */
    public function getLocalities($all = false)
    {
        if ($all) {
            return $this->localities->toArray();
        }

        $criteria = Criteria::create()->where(Criteria::expr()->isNull('parent_locality'));
        return $this->localities->matching($criteria)->toArray();
    }

    public function getCitiesCount()
    {
        $count = 0;

        /** @var LocalityEntity $localityEntity */
        foreach ($this->getLocalities(true) as $localityEntity) {
            $count += count($localityEntity->getCities());
        }

        return $count;
    }

Pri tomhle pouziti, vznikne problem kdyz v template iteruji zeme a nad kazdou si zavolam

{foreach $countries as $item}
 {foreach $item->getLocalities() as $locality}
.....
.....

vyhodi Cannot access private property App\Entity\LocalityEntity::$parent_locality
Co jsem pochopil, tak on si v matching hleda getter, a muj getter je getParent()

Takze matching upravim na

$criteria = Criteria::create()->where(Criteria::expr()->isNull('parent'));
        return $this->localities->matching($criteria)->toArray();

V tu chvily funguje spravne, ale jakmile si nactu detail nejake zeme a zavolam si v sablone

{foreach $country->getLocalities() as $item}

Vyhodi Unrecognized field: parent

Reseni jsou 2

  • budto nastavit $parent_locality jako public
  • prejmenovat getter na getParentLocality() a v matching nechat ‚parent_locality‘

Prejmenovat getter je to nejmensi, jen mi nejde do hlavy, co je to za chovani, to musi byt getter striktne pojmenovany jako nazev promenne ? Kdybych tam getter vubec nemel, tak to padne taky.

Diky za vysvetleni.

Editoval kleinpetr (7. 2. 2018 21:59)

Martk
Člen | 661
+
0
-

Podle mě to funguje takhle:

První případ Kolekce již byla načtena z databáze, nyní se bude filtrovat na „úrovni PHP“, takže se budou procházet všechny entity a porovnávat hodnoty, k tomu potřebuješ getter nebo public proměnnou.

Druhý případ Kolekce není načtena z databáze, takže pro optimalizaci se vytvoří SQL pro vytažení z databáze. Doctrine potřebuje vytáhnout z metadat informace o položce „parent“ (aby mohl filtrovat podle reálného názvu sloupce), ale ta neexistuje, proto hláška Unrecognized field: parent , kterou generuje výjimka ze třídy Metadata.

EDIT: V dokumentaci jsem našel tohle

The Criteria has a limited matching language that works both on the SQL and on the PHP collection level. This means you can use collection matching interchangeably, independent of in-memory or sql-backed collections

Editoval Martk (8. 2. 2018 9:38)

kleinpetr
Člen | 480
+
0
-

@Martk aha, diky za odpoved. Slo mi o to, ze kdyby bylo jasne dano, ze se to bere na urovni php, tak bych naopak mohl vytvorit nejake custom gettery, a mel bych mnohem rozmanitejsi filtraci. Nicmene takto jsem omezen na to vzdy dat stejny nazev getteru jako promenne, prijde mi to lepsi nez davat public promennou.

Kazdopadne s doctrinou teprve zacinam a tak me napadlo, nedal by se nejak optimalizovat ten getter na citiesCount ? Abych nemusel jednotlive prochazet lokality a pripocitavat jejich mesta.

Diky

Martk
Člen | 661
+
0
-

imho bez entity managera to optimalizovat nepůjde.

kleinpetr
Člen | 480
+
0
-

@Martk Myslel jsem si to.. Premyslel jsem nad tim uz vicekrat, ze pokud bych nemel brat entitu pouze jako obalku na data, ale mel bych v ni resit i nejake operace, tak bych tam musel nacpat entity managera nebo spis nejake repository.. Jak to resis ty ?

Editoval kleinpetr (8. 2. 2018 11:15)

Martk
Člen | 661
+
0
-

Používám jen entitu jako obálku na data, složitější dotazy / optimalizaci řeším přes repozitář. Napadají mě 2 způsoby jak by se to dalo ještě udělat.

První je použít navrhový vzor strategie, předáš entitu a entity managera do nové třídy a změníš jen algoritmus pro výpočet metody getCitiesCount (nezkoušel jsem to v praxi, je to jen teoretická úvaha a není to nejčistší řešení, protože to porušuje dry)

Druhá je použít DTO, jednoduché třídy na přepravu dat s public proměnnýma. Někdo to používá a diskuze o tom tady určitě budou.

Raději bych počkal na radu od zkušenějších…

kleinpetr
Člen | 480
+
0
-

@Martk Jeste kdyz jsme u te Doctrine, ted jsem zjistil, ze kdyz si udelam DQL dotaz s joinem a mam napriklad knihu a autora, dejme tomu, ze hledam nazev knihy a k ni nazev autora. Pokud to udelam takhle:

->createQueryBuilder()
            ->select('b')
            ->from(BookEntity::class, 'b')
            ->leftJoin('b.author', 'a')
            ->where('b.name LIKE :name')
            ->setParameter('name', $name)
            ->getQuery()
            ->getResult();

Tak kdyz si pak dumpnu $book->getAuthor(), tak mi vrati instanci Kdyby/GeneratedProxy/__CG__...... , ale pokud udelam pridam do ->select('b, a') tak uz to vraci klasicky AuthorEntity. Jedna se lazyloading ?

Mohl bys me navest vcem je presne rozdil, popripade kde bych si na to mel dat pozor a mohlo by to delat neplechu. Diky

Editoval kleinpetr (10. 2. 2018 20:51)

Mysteria
Člen | 797
+
0
-

Přesně tak. Pokud uvedeš do SELECTu, že chceš knihu i autora, tak se udělá jeden JOIN a vytáhne se všechno dohromady. Pokud dáš že chceš jenom knihu, tak autor se ti vytáhne, až když u nějaké knihy zavoláš $book->getAuthor().

Pozor je potřeba dát na to, že když použiješ druhou variantu, tak v případě, že ji nijak nezoptimalizuješ, tak když budeš procházet foreachem třeba 500 knih, a v něm budeš volat $book->getAuthor(), tak budeš mít 1 + 500 dotazů do databáze.

Případně se podívej na: https://tideways.io/…should-avoid

kleinpetr
Člen | 480
+
0
-

@Mysteria Diky ! to je presne to co jsem potreboval, je tedy lepsi misto findAll() napsat rovnou querybuilder a vyselectovat vsechny asociace, potom je to otazka jednoho query a myslim, ze ikdyz bych nejakou asociaci ani zrovna v nejakou chvily nepotreboval, tak je to globalne lepsi nez lazyloading.

Diky :)