W Doctrine relacja one-to-one jest jedną z opcji powiązań pomiędzy encjami. W sposobie mapowania nie wyróżnia się niczym szczególnym od pozostałych relacji. Różnice znajdziemy jednak w sposobie działania. Dotyczy to relacji dwukierunkowej (bidirectional), w której zarówno pierwsza encja ma odniesienie do drugiej, jak i druga do pierwszej.
Żeby zobrazować na czym rzecz polega, przygotowałem dwie proste encje, które połączone są wyżej wspomnianą relacją. Są to Stolica (Capital City) oraz Kraj (Country). Zazwyczaj, poza pewnymi wyjątkami, kraj posiada jedną stolicę, a stolica należy do jednego kraju.
Tworzenie encji
use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class CapitalCity { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private ?int $id; /** * @ORM\Column(type="string", length=45, nullable=false) */ private string $name; /** * @ORM\OneToOne(targetEntity="Country", inversedBy="capitalCity") * @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false) */ private Country $country; public function __construct(string $name, Country $country) { $this->name = $name; $this->country = $country; } // getters, setters } use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Country { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private ?int $id; /** * @ORM\Column(type="string", length=45, nullable=false) */ private string $name; /** * @ORM\OneToOne(targetEntity="CapitalCity", mappedBy="country") */ private CapitalCity $capitalCity; public function __construct(string $name, CapitalCity $passport) { $this->name = $name; $this->capitalCity = $passport; } //... }W powyższym mapowaniu owning side relacji one-to-one znajduje się w encji CapitalCity. Tabela utworzona dla tej encji zawiera klucz obcy do drugiej tabeli. W mapowaniu takiej relacji znajdziemy atrybut inversedBy. Zawiera on nazwę pola asocjacyjnego w drugiej encji. Analogicznie, inverse side znajduje się w encji Country i zawiera atrybut mappedBy.
Dodałem do bazy danych kilka rekordów, żeby lepiej zobrazować, co będzie się działo podczas pobierania danych. Zarówno dzięki repozytoriów, jak i Query Builder'a. Następnie wykonałem proste zapytanie SQL:
SELECT c.id as country_id, c.name as country_name, cc.id as city_id, cc.name as city_name, cc.country_id as city_country_id FROM country c LEFT JOIN capital_city cc on c.id = cc.country_idDane w bazie prezentują się w następujący sposób:
1 | Poland | 1 | Warsaw | 1 |
2 | Germany | 2 | Berlin | 2 |
3 | France | 3 | Paris | 3 |
Pobieranie miast
Jako pierwsze wyciągnę stolice. Użyję do tego repozytorium:
//... $capitalCityRepository = $entityManager->getRepository(CapitalCity::class); $capitalCities = $capitalCityRepository->findAll(); //...1 | 0.37 ms | SELECT t0.id AS id_1, t0.name AS name_2, t0.country_id AS country_id_3 FROM capital_city t0Parameters:[] |
Jak widać na rezultatach z profilera, nic szczególnego się tu nie dzieje. Doctrine wykonał prostego selecta do bazy danych. Następnie zwrócił listę encji CapitalCity. Tabela capital_city posiada klucz obcy do tabeli country, jednak encja Country nie zostanie w tym przypadku pobrana z bazy danych. Stworzony zostanie obiekt klasy proxy, który dziedziczy po klasie CapitalCity. W momencie wywołania metody $capitalCity->getCountry(), Doctrine, w sposób leniwy pobierze dodatkowe dane.
Pobieranie krajów
Czy sytuacja dla państw będzie analogiczna jak dla stolic?
//... $countryRepository = $entityManager->getRepository(Country::class); $countries = $countryRepository->findAll(); //...1 | 0.43 ms | SELECT t0.id AS id_1, t0.name AS name_2, t3.id AS id_4, t3.name AS name_5, t3.country_id AS country_id_6 FROM country t0 LEFT JOIN capital_city t3 ON t3.country_id = t0.idParameters:[] |
Zapytanie to już nie prosty select jak w pierwszym przypadku. Encja Country nie ma kolumny (a raczej tabela country nie ma kolumny), która mówiłaby, czy stolica jest do niej przypisana. W związku z tym Doctrine próbuje pobrać ją, używając left joina. Ma to jednak miejsce tylko w sytuacji, w której używamy metod dostępnych w repozytorium.
Budowanie zapytań dzięki Query Buildera
W przypadku pobierania stolic, które zawierają klucz obcy do krajów, poniższe zapytanie nie będzie różniło się od tych wykonywanych przez funkcję findAll() z repozytorium:
//... $capitalCityRepository = $entityManager->getRepository(CapitalCity::class); $capitalCities = $capitalCityRepository ->createQueryBuilder('c') ->getQuery() ->getResult(); //...1 | 0.44 ms | SELECT c0_.id AS id0, c0.name AS name1, c0.country_id AS country_id_2 FROM capital_city c0_Parameters:[] |
W przypadku państw celowo nie zrobiłem joinów do stolic. Doctrine po pobraniu rezultatów, dociągnął do każdego kraju stolicę poprzez osobne zapytania SQL do każdego rekordu. Co więcej, zrobił to pomimo braku jakiegokolwiek odwoływania się do któregokolwiek obiektu, ani iterowania po liście krajów:
//... $countryRepository = $entityManager->getRepository(Country::class); $countries = $countryRepository ->createQueryBuilder('c') ->getQuery() ->getResult(); //...1 | 0.32 ms | SELECT c0_.id AS id0, c0.name AS name_1 FROM country c0_Parameters:[] |
2 | 0.78 ms | SELECT t0.id AS id_1, t0.name AS name_2, t0.country_id AS country_id_3 FROM capital_city t0 WHERE t0.country_id = ? Parameters:[1] |
3 | 0.24 ms | SELECT t0.id AS id_1, t0.name AS name_2, t0.country_id AS country_id_3 FROM capital_city t0 WHERE t0.country_id = ? Parameters:[2] |
4 | 0.23 ms | SELECT t0.id AS id_1, t0.name AS name_2, t0.country_id AS country_id_3 FROM capital_city t0 WHERE t0.country_id = ? Parameters:[3] |
Dodatkowych zapytań można uniknąć, dodając w Query Builderze left joina do encji CapitalCity i selecta. Wtedy Doctrine pobierze wszystkie dane jednym zapytaniem:
//... $countryRepository = $entityManager->getRepository(Country::class); ->createQueryBuilder('c') ->addSelect('cc') ->leftJoin('c.capitalCity', 'cc') ->getQuery() ->getResult(); //...1 | 0.32 ms | SELECT c0_.id AS id0, c0.name AS name1, c1.id AS id2, c1.name AS name3, c1.country_id AS country_id4 FROM country c0 LEFT JOIN capitalcity c1 ON c0.id = c1.country_idParameters:[] |
Podsumowanie
Tworząc dwukierunkową relację jeden-do-jeden, należy zastanowić się, która z encji będzie pobierana częściej i to z niej zrobić Owning Side poprzez umieszczenie klucza obcego do powiązanej tabeli. Zaoszczędzimy wówczas wielu zapytań do bazy danych. Możemy też tworzyć własne zapytania dołączające powiązaną encje. Należy jednak mieć na uwadze, iż wraz z rozwojem aplikacji, takich zapytań może być całkiem sporo. Pamiętanie, żeby w każdym z nich dodać joina może po jakimś czasie okazać się problematyczne.