Original question
I am doing a fetch join in Doctrine, using a join table that has a composite key and no other defined fields, and getting incorrect data loaded into my entity. Is this a bug in Doctrine, or am I doing something wrong?
What follows is a simple example that illustrates the problem.
Create three entities:
/**
* #ORM\Entity
* #ORM\Table(name="driver")
*/
class Driver
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255);
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="DriverRide", mappedBy="driver")
*/
private $driverRides;
function getId() { return $this->id; }
function getName() { return $this->name; }
function getDriverRides() { return $this->driverRides; }
}
/**
* #ORM\Entity
* #ORM\Table(name="driver_ride")
*/
class DriverRide
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Driver", inversedBy="driverRides")
* #ORM\JoinColumn(name="driver_id", referencedColumnName="id")
*/
private $driver;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Car", inversedBy="carRides")
* #ORM\JoinColumn(name="car", referencedColumnName="brand")
*/
private $car;
function getDriver() { return $this->driver; }
function getCar() { return $this->car; }
}
/**
* #ORM\Entity
* #ORM\Table(name="car")
*/
class Car
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=25)
* #ORM\GeneratedValue(strategy="NONE")
*/
private $brand;
/**
* #ORM\Column(type="string", length=255);
*/
private $model;
/**
* #ORM\OneToMany(targetEntity="DriverRide", mappedBy="car")
*/
private $carRides;
function getBrand() { return $this->brand; }
function getModel() { return $this->model; }
function getCarRides() { return $this->carRides; }
}
Populate the corresponding database tables with this data:
INSERT INTO driver (id, name) VALUES (1, 'John Doe');
INSERT INTO car (brand, model) VALUES ('BMW', '7 Series');
INSERT INTO car (brand, model) VALUES ('Crysler', '300');
INSERT INTO car (brand, model) VALUES ('Mercedes', 'C-Class');
INSERT INTO car (brand, model) VALUES ('Volvo', 'XC90');
INSERT INTO car (brand, model) VALUES ('Dodge', 'Dart');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Crysler');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Mercedes');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Volvo');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Dodge');
Use this code to hydrate a Driver entity and display its contents:
$qb = $em->createQueryBuilder();
$driver = $qb->select('d, dr, c')
->from('Driver', 'd')
->leftJoin('d.driverRides', 'dr')
->leftJoin('dr.car', 'c')
->where('d.id = 1')
->getQuery()->getSingleResult();
print '<p>' . $driver->getName() . ':';
foreach ($driver->getDriverRides() as $ride) {
print '<br>' . $ride->getCar()->getBrand() . ' ' . $ride->getCar()->getModel();
}
Expected output:
John Doe:
BMW 7 Series
Crysler 300
Dodge Dart
Mercedes C-Class
Volvo XC90
Actual output:
John Doe:
BMW 7 Series
Dodge Dart
Dodge Dart
Volvo XC90
Volvo XC90
There is a strange duplication going on here with the associated entities. Specifically, child entity #3 is duplicated as #2, #5 is duplicated as #4, etc., and child entity #2, #4, etc. are not loaded at all. This pattern is consistent and reproducible.
Is there something wrong with my code? Why is Doctrine failing to correctly map the data from the database tables to the entities?
Additional remarks
The same problem occurs if one queries for all rides (aka associations between drivers and cars) and performs a fetch-join. This is an important case, because the answer by #qaqar-haider does not work for this scenario.
Assume the following table data
INSERT INTO driver (id, name) VALUES (1, 'John Doe');
INSERT INTO driver (id, name) VALUES (2, 'Erika Mustermann');
INSERT INTO car (brand, model) VALUES ('BMW', '7 Series');
INSERT INTO car (brand, model) VALUES ('Crysler', '300');
INSERT INTO car (brand, model) VALUES ('Mercedes', 'C-Class');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Crysler');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Mercedes');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (2, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (2, 'Crysler');
Imagine the following query with fetch-joins
$qb = $em->createQueryBuilder();
$rides = $qb->select('dr, d, c')
->from('DriverRide', 'dr')
->leftJoin('dr.driver', 'd')
->leftJoin('dr.car', 'c')
->getQuery()->getResult();
foreach ($rides as $ride) {
print $ride->driver->getName() . ': ' . $ride->getCar()->getModel();
}
Expected output:
John Doe: Chrysler
John Doe: Mercedes
John Doe: BMW
Erika Mustermann: BMW
Erika Mustermann: Chrysler
Actual output:
John Doe: Chrysler
John Doe: Mercedes
John Doe: Mercedes
Erika Mustermann: BMW
Erika Mustermann: BMW
Once again, the number of results is correct, but the associations are mixed up.
This is a simplified test case of the original question and is not subject to any WHERE or GROUP BY clause.
This is a bug in Doctrine. Unfortunately, the relevant Doctrine source code is quite ugly, so I am not certain that my bug fix is the best one, but it does fix the problem. I am copying the explanation from my bug report here for completeness:
The main cause of this bug is located in the ObjectHydrator class. In the hydrateRowData() method, $resultPointers is used to store a reference to the most recently hydrated object of each entity type. When a child entity needs to be linked to its parent, this method looks in $resultPointers to find a reference to the parent and, if it finds one, links the child to that parent.
The problem is that $resultPointers is an instance/object (rather than local/method) variable that does not get reinitialized every time hydrateRowData() is called, and so it may retain a reference to an entity that was hydrated the previous time the method was called rather than the current time.
In this particular example, the Car entity is hydrated before the DriverRide entity each time hydrateRowData() is called. When the method looks for the parent of the Car entity, it finds nothing the first time (because DriverRide has not yet been processed at all) and, on every subsequent call, finds a reference to the DriverRide object that was hydrated the previous time the method was called (because DriverRide has not yet been processed for the current row, and $resultPointers still retains a reference to the result of processing for the previous row).
The bug disappears when additional fields are added to DriverRide only because doing so happens to cause the DriverRide entity to be processed before the Car entity in hydrateRowData(). The duplication of records happens because some other weird part of this method causes the child entity to be identified as not fetch joined (and so lazy loaded) every other time the method is called (not counting the first time), and so those times (the third, fifth, etc.) the link between child and parent happens to turn out correctly.
I believe that the fundamental problem is that $resultPointers is neither a local variable nor reinitialized each time hydrateRowData() is called. I cannot think of any scenario in which you would need a reference to an object that was hydrated with data from the previous row of data, so I would recommend simply reinitializing this variable at the beginning of this method. That should fix the bug.
This is a nice question. I've spent some time trying to find out the answer, but I managed to achieve the expected result only by modifying the entity mappings - why the initial problem arises I still cannot understand, but I will continue to investigate.
So, this is what I've done. Instead of manually defining a DriverRide entity, let Doctrine define it for you with many-to-many mapping, as described here.
The entities will look like:
/**
* #Entity
* #Table(name="cars")
*/
class Car
{
/**
* #Id
* #Column(type="string", length=25)
* #GeneratedValue(strategy="NONE")
*/
protected $brand;
/**
* #Column(type="string", length=255);
*/
protected $model;
public function getBrand() { return $this->brand; }
public function getModel() { return $this->model; }
public function setBrand($brand) { $this->brand = $brand; }
public function setModel($model) { $this->model = $model; }
}
/**
* #Entity
* #Table(name="drivers")
*/
class Driver
{
/**
* #Id
* #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Column(type="string", length=255);
*/
protected $name;
// This will create a third table `driver_rides`
/**
* #ManyToMany(targetEntity="Car")
* #JoinTable(
* name="driver_rides",
* joinColumns={#JoinColumn(name="driver_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="car", referencedColumnName="brand")}
* )
*/
protected $cars;
public function __construct()
{
$this->cars = new ArrayCollection();
}
function getId() { return $this->id; }
function getName() { return $this->name; }
function getCars() { return $this->cars; }
public function setName($name) { $this->name = $name; }
public function setCars($cars) { $this->cars = $cars; }
public function addCar(\Car $car) { $this->cars[] = $car; return $this; }
public function removeCar(\Car $car) { $this->cars->removeElement($car); }
}
And the query to get what you want:
$qb = $entityManager->createQueryBuilder();
/** #var $driver Driver */
$driver = $qb->select('d, dr')
->from('Driver', 'd')
->leftJoin('d.cars', 'dr') // join with `driver_rides` table
->where('d.id = 1')
->getQuery()
->getSingleResult();
printf("%s:\n", $driver->getName());
foreach ($driver->getCars() as $car) {
printf("%s %s\n", $car->getBrand(), $car->getModel());
}
You have to group it by a brand, othervise it would give you desired results
Group By brand
qb = $em->createQueryBuilder();
$driver = $qb->select('d, dr, c')
->from('Driver', 'd')
->leftJoin('d.driverRides', 'dr')
->leftJoin('dr.car', 'c')
->where('d.id = 1')
->groupBy('c.brand')
->getQuery()->getSingleResult();
Related
I have two fields : account_type_id and account_id.
How can i manually map doctrine TargetEntity to join Company Entity if accountTypeId = 1 OR join User Entity if account_type_id = 2 ?
<?php
/** #Entity */
class Accounts
{
// 1= Company, 2 = User
private $accountType;
/**
* #ManyToOne(targetEntity="Companies")
*/
private $company;
/**
* #ManyToOne(targetEntity="Users")
*/
private $user;
//...
}
Unfortunately, joining different columns on the fly cannot be done automatically, but you can have both fields set as nullable and only set the correct one when persisting the Account entity.
This would be the annotation:
/**
* #ORM\ManyToOne(targetEntity="Users", inversedBy="users")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
*/
private $user;
Keep in mind that nullable=true is the default anyway, I'm just being specific here.
If you want to go defensively about this, you can have an additional check in getter
/**
* #return User
* #throws \Exception
*/
public function getUser()
{
if ($this->accountType !== 2) {
throw new \Exception("Entity is not of type 'user'");
}
return $this->user;
}
I have a table of states/regions which has 2 fields: state and country, where the country is a reference to the country object in table of countries:
/**
* #ORM\Entity
* #ORM\Table(name="LocationStates")
*/
class LocationState
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $state_id;
public function getStateID()
{
return $this->state_id;
}
public function setStateID($state_id)
{
$this->state_id = $state_id;
}
/**
* Many States have one Country
* #ORM\ManyToOne(targetEntity="LocationCountry")
* #ORM\JoinColumn(name="country",referencedColumnName="country_id",onDelete="SET NULL")
*/
protected $country;
public function getCountry()
{
return $this->country;
}
public function setCountry($country)
{
$this->country = $country;
}
I have a single page and controller where a user adds a new state or edits the existing state.
There are the following conditions for the States:
Countries are selected from a given set of countries (part of the site engine), a state/region cannot be saved without a selected country
There are 2 fields for the state/region: select box populated on selecting a country with known states (e.g. USA, Australia, UK) - not all countries have states, so there is also a text box to enter a region for a country without states
When a state is saved, the controller should check if a state with the same name AND country already exists - if yes, then it's an error, you can't have same state names with the same country
If a new state is saved, I check if this state new or existing and if new, then I check if another state exists with the same name and country ID:
!is_object(LocationState::getByID($state_id)) && !empty(LocationState::getByState($state, $country_id))
So far so good, that works. But problems start when I edit and save an existing state and can't figure out what the logic should be.
If I do this:
is_object(LocationState::getByID($state_id)) && LocationState::getByID($state_id)->getState() != $state && !empty(LocationState::getByState($state, $country_id))
it checks if it's an existing state, if yes then it checks I changed its name, then it checks if another one with the same name and country ID exists - and this is not working, I can't get the logic right. It only works if the country doesn't change. But if I edit the state, I may want to change its country too. But the above code saves the state with the same name and same country ID as the already existing one.
Here's the getByState() which is part of class LocationState:
public static function getByState($state, $country = null)
{
$em = \ORM::entityManager();
if (is_null($country)) {
return $em->getRepository(get_class())->findOneBy(array('state' => $state));
}
else {
$qb = $em->createQueryBuilder();
return $qb->select('c')
->from('LocationState', 'c')
->leftJoin('c.country', 'j', 'WITH', 'j.country_id = c.country')
->andWhere('j.country_id = ?1')->setParameter(1, $country)
->andWhere('c.state = ?2')->setParameter(2, $state)
->getQuery()
->getResult();
}
}
Will really appreciate your help with the last bit of logic to save edited state to make sure it doesn't exist yet.
Given the following parent-child entities, how can I preload all child C entities with only single database query when I have many P (already loaded) enties?
/**
* #ORM\Entity
**/
class P {
/** #var Collection #ORM\ManyToMany(targetEntity="C") */
public $childs;
}
/**
* #ORM\Entity
**/
class C {
/** #var int #ORM\Column(type="integer") **/
public $v;
}
Test case, this code should not issue any additional database query once preloaded.
foreach ($ps as $p) {
foreach ($p->childs as $child) { $dummy = $child->v; }
}
The following query preloads all N:N child entities in one query.
note: performance is only about 0.5 times better than foreach over non preloaded data. Probably because the P entity (in my applied case) contained a lot of fields.
$em->createQueryBuilder()->select('p', 'c')
->from(P, 'c')
->leftJoin('l.childs', 'c') // preload
->where('p.id IN (:ps)')->setParameter('ps', $ps)
->getQuery()->getResult();
I have two entities with a Unidirectional Many-to-One mapping.
Here's Product:
use Doctrine\Common\Collections\ArrayCollection;
/**
* #Entity
* #Table(name="Product")
* #gedmo:TranslationEntity(class="GPos_Model_Translation_ProductTranslation")
*/
class GPos_Model_Product extends GPos_Doctrine_ActiveEntity {
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #ManyToMany(targetEntity="GPos_Model_Category")
* #JoinTable(name="products_categories",
* joinColumns={#JoinColumn(name="product_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
protected $categories;
public function __construct() {
$this->categories = new ArrayCollection();
}
public function addCategory(GPos_Model_Category $category) {
if (!$this->categories->contains($category))
$this->categories->add($category);
}
}
As you can see, $categories is an ArrayCollection of GPos_Model_Category entities.
Now what?
Well now I'd like to retrive all products that are in a given category and also all products that are NOT in a given category.
I've tried $products = GPos_Model_Product::findByCategories($category->getId());
but that only gave me
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '= '1'' at line 1 and $category's ID is 1 so I guess it's not the way to go. Anyone knows how to deal with that ?
Thank you!
I finally found out how to select all products that are in a category thanks to https://stackoverflow.com/a/9808277/1300454.
I tweaked his solution a bit so I could pass an array of Category entities and it would find all products that are within these categories. If you give more than one entity it will return any product that is in at least one of the given categories.
Here's my tweak (I located this function in my Product entity):
/**
*
* Takes an array of GPos_Model_Category entities as parameter and returns all products in these categories
* #param array $categories
*/
public static function findByCategories($categories) {
$categoryArray = array();
foreach ($categories as $category) {
array_push($categoryArray, $category->getId());
}
$qb = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb ->select('p')
->from('GPos_Model_Product', 'p')
->leftJoin('p.categories', 'c')
->andWhere($qb->expr()->in('c.id', $categoryArray));
return $qb->getQuery()->execute();;
}
Here's how you call it:
$products_cat = GPos_Model_Product::findByCategories(array($category));
In this case $category is an entity alone that's why I put it in an array before giving it to the function.
And here is the way you find products that are not in a given category or list of category:
/**
*
* Takes an array of GPos_Model_Category entities as parameter and returns all products not in these categories
* #param array $categories
*/
public static function findByNotCategories($categories) {
$categoryArray = array();
foreach ($categories as $category) {
array_push($categoryArray, $category->getId());
}
$qb = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb2 = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb->select('p')
->from('GPos_Model_Product', 'p')
->where($qb->expr()->notIn('p.id',
$qb2->select('p2.id')
->from('GPos_Model_Product', 'p2')
->leftJoin('p2.categories', 'c')
->andWhere($qb->expr()->in('c.id', $categoryArray))
->getDQL()
));
return $qb->getQuery()->execute();
}
This is actually using a subselect. I'm selecting all products id that are in the given category (that's the subselect) then I'm selecting all products that are not in the result of the subselect. My job here is done!
I have tow entities Slaplans and Slaholidays and a join table slaplans_slaholidays.
After creating two Slaholidays objects, I persist them both, add them to the Slaplans and flush. The problem is that only the slaplans and slaholidays tables are updated, but the join table isn't.
Slaplans Entity :
<?php
namespace ZC\Entity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Slaplans
*
* #Table(name="slaplans")
* #Entity(repositoryClass="Repositories\Slaplans")
*/
class Slaplans
{
/*
* #ManyToMany(targetEntity="Slaholidays",inversedBy="plans", cascade={"ALL"})
* #JoinTable(name="slaplans_slaholidays",
* joinColumns={#JoinColumn(name="slaplanid" ,referencedColumnName="slaplanid")},
* inverseJoinColumns={#JoinColumn(name="slaholidayid" ,referencedColumnName="slaholidayid")})
* }
*/
private $holidays;
public function __construct()
{
$this->holidays = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getHolidays() {
return $this->holidays;
}
public function setHolidays($holidays)
{
$this->holidays=$holidays;
}
/*public function addHoliday($holiday) {
$this->holidays[]=$holiday;
}*/
}
Slaholidays Entity:
<?php
namespace ZC\Entity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Slaholidays
*
* #Table(name="slaholidays")
* #Entity(repositoryClass="Repositories\Slaholidays")
*/
class Slaholidays
{
/**
* #var integer $slaholidayid
*
* #Column(name="slaholidayid", type="integer", nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
private $slaholidayid;
/*
* #ManyToMany(targetEntity="Slaplans",mappedBy="holidays", cascade={"ALL"})
*/
private $plans;
/*public function getPlans(){
return $this->plans;
}*/
}
Code to persist the entities:
$allholidays=array();
$holiday=$this->_em->getRepository('ZC\Entity\Slaholidays')->find($value);
$holiday=new ZC\Entity\Slaholidays();
//..sets holiday fields here
$this->_em->persist($holiday);
$allholidays[]=$holiday;
$slaplan->setHolidays($allholidays);
foreach ($slaplan->getHolidays() as $value) {
$this->_em->persist($value);
}
$this->_em->persist($slaplan);
$this->_em->flush();
The are two issues in your code:
The first one: you are persisting each Slaholiday twice: first with
$this->_em->persist($holiday);
and second with
foreach ($slaplan->getHolidays() as $value) {
$this->_em->persist($value);
}
There is no problem actually, as they are not actually persisted in the db until flush are called, but anyway, you don't need that foreach.
The reason why your join table is not updated is in $slaplan->setHolidays method. You are initializing $slaplan->holidays with ArrayCollection (which is right) and in setHolidays you set it to the input parameter (which is $allholidays Array, and this is not right).
So, the correct way to do that is to use add method of the ArrayCollection
public function setHolidays($holidays)
{
//$this->holidays->clear(); //clears the collection, uncomment if you need it
foreach ($holidays as $holiday){
$this->holidays->add($holiday);
}
}
OR
public function addHolidays(ZC\Entity\Slaholiday $holiday)
{
$this->holidays->add($holiday);
}
public function clearHolidays(){
$this->holidays->clear();
}
//..and in the working script...//
//..the rest of the script
$this->_em->persist($holiday);
//$slaplan->clearHolidays(); //uncomment if you need your collection cleaned
$slaplan->addHOliday($holiday);
Although Doctrine checks the owning side of an association for things that need to be persisted, it's always important to keep both sides of the association in sync.
My advise is to have get, add and remove (no set) methods at both sides, that look like this:
class Slaplans
{
public function getHolidays()
{
return $this->holidays->toArray();
}
public function addHoliday(Slaholiday $holiday)
{
if (!$this->holidays->contains($holiday)) {
$this->holidays->add($holiday);
$holiday->addPlan($this);
}
return $this;
}
public function removeHoliday(Slaholiday $holiday)
{
if ($this->holidays->contains($holiday)) {
$this->holidays->removeElement($holiday);
$holiday->removePlan($this);
}
return $this;
}
}
Do the same in Slaplan.
Now when you add a Slaholiday to a Slaplan, that Slaplan will also be added to the Slaholiday automatically. The same goes for removing.
So now you can do something like this:
$plan = $em->find('Slaplan', 1);
$holiday = new Slaholiday();
// set data on $holiday
// no need to persist $holiday, because you have a cascade={"ALL"} all on the association
$plan->addHoliday($holiday);
// no need to persist $plan, because it's managed by the entitymanager (unless you don't use change tracking policy "DEFERRED_IMPLICIT" (which is used by default))
$em->flush();
PS: Don't use cascade on both sides of the association. This will make things slower than necessary, and in some cases can lead to errors. If you create a Slaplan first, then add Slaholidays to it, keep the cascade in Slaplan and remove it from Slaholiday.