I have one entity, say Person, which contains a list of $pets:
protected $pets;
public function getPets()
{
return $this->pets;
}
Standard Doctrine. Unfortunately, these pets may be of different types, such as cats or dogs, or a mix. So I used Class Table Inheritance:
/**
* #ORM\Entity
* #ORM\Table(name="pets")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="pettype", type="string")
* #ORM\DiscriminatorMap({"cat_animal" = "CatAnimal", "dog_animal" = "DogAnimal"})
*/
class Pet
{
/**
* #ORM\Column(name="eventid", type="integer")
* #ORM\Id
*/
private $id; // protected did not work either
/**
* Get id
*/
public function getId()
{
return $this->id;
}
}
/**
* #ORM\Entity
* #ORM\Table(name="cat_animal")
*/
class CatAnimal extends Pet
{
/**
* #ORM\Column(type="float")
*/
protected $height;
// etc.
}
// DogAnimal class omitted.
This was relatively straightforward using Doctrine's docs.
If I want to get all cats for an individual person, I have discovered I can do this:
public function getCats($person)
{
return $this->getEntityManager()->getRepository('MyBundle:CatAnimal')
->findByPerson($person);
}
However, how do I access the subclasses using a query builder? If I have the Person repository ($repos here), I want to do something like the following:
$repos->createQueryBuilder('person')
->select('pet.height')
->join('person.pets', 'pet')
->where('person = :person')
->setParameter('person', $person);
Except Pet doesn't have height, so this throws an exception. The DQL generated automagically joins to DogAnimal and CatAnimal, so I should be able to access these properties, but I don't know how. I have tried:
$repos->createQueryBuilder('person')
->select('cat.height')
->from('MyBundle:CatAnimal', 'cat)
->join('person.pets', 'pet')
->where('person = :person')
->setParameter('person', $person);
But this seems to do the cartesian product. I can solve that by adding:
->andWhere('person.id = cat.person')
This seems overly complicated for what I want. I have tried looking for the correct way to do this, but resources are limited.
This builds on a previous question, with a similar structure. The names of the tables were changed for clarity and generalisability.
You need to join correctly to Person, adding a field to the Pet class. In my example I named it owner:
$catRepo->createQueryBuilder('cat')
->select('cat.height')
->from('MyBundle:CatAnimal', 'cat')
->join('cat.owner', 'person')
->where('person = :person')
->setParameter('person', $person);
Related
EDIT 2018-05-22: no answer fully fixed issue - can no longer replicate issue as no longer have access. Not removing based on this meta discussion
Please do not spend time/effort creating an answer
Discussion in #Wilt's answer led me to what I know about using discriminators now, it might help future questioners. In my case it helped, but did not provide an answer.
I've got a bit of complex problem where the hydration of data received from client-side gets hydrated incorrectly. I've been trying to fix the problem for close to a week now, so I thought to ask you guys.
We've got this application that allows for the creation of assignments for students. Assignments may contain Questions, Text items, Media items, and more. The problem is with the Questions and associated Answers.
Scenario
Assignment
QuestionSheet 1 (L1)
Question 1 (L1 - V1)
Answer A (L1 - V1 - A1)
Answer B (L1 - V1 - A2)
Question 2 (L1 - V2)
Answer A (L1 - V2 - A1) (just 1 answer)
QuestionSheet 2 (L2)
Question 1 (... and so on)
Answer A
Answer B
Question 2
Answer A
Answer B
Question 3
Answer A
Answer B
The above gets send properly from the client-side. Screenshot of snipped of that data:
Important to note is that, as you can see above, a Question is in fact a GridElements entity. It might've also been Text or Image, this is based on the property type = question which is a Discriminator.
After hydrating the data we get the following Entity structure:
As you can see, the data is no longer correct after hydration. This is done during the $form->isValid(). QuestionSheet 1 contains the first Question for QuestionSheet 2 and that Question has the first Answer from the third Question of the second QuestionSheet.
When reading through the full hydrated dataset, I see that the Answers created for the first QuestionSheet have been dropped. The Answers from the second QuestionSheet have been duplicated and have overwritten the Answers in the first QuestionSheet. In essence, what you see in the picture above.
Worse still
The below is all the data that is saved to the database after the above, with the mentioned scenario of 2 lists, 5 questions and 9 answers.
So of the first Question, no Q&A left. The second QuestionSheet's Questions have been used to overwrite them. Also , only the last 2 Answers are there, filling the space of what should've been 9!.
Btw, the query returning this is completely LEFT JOIN so as to show all empty data as well, this is all there's left.
It seems that it grabs the last set of whatever child entities there are to fill up previous entities, or something. I've gotten lost.
How is this possible?
As I mentioned, I've been at it a good long while, but cannot find a solution. Hope you guys can help.
If you need any info on code I'll do my best to either show it here or explain it as best as possible in case of some proprietary code.
Update - Entities
Assignment entity
/**
* #ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\AssignmentRepository" )
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="ass_assignment")
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class Assignment extends SeoUrl
{
//Traits and properties
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\Assignment\Entity\QuestionSheet", mappedBy="assignment", cascade={"persist", "remove"}, orphanRemoval=true)
**/
protected $questionSheets;
public function __construct()
{
$this->abstractEntity_entityCategories = new ArrayCollection();
$this->questionSheets = new ArrayCollection();
$this->documents = new ArrayCollection();
}
public function __toString()
{
return (string)$this->id;
}
//More getters/setters
}
QuestionSheet entity
/**
* #ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\QuestionSheetRepository")
* #ORM\Table(name="ass_questionsheet")
**/
class QuestionSheet extends AbstractEntity
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\Assignment\Entity\Assignment", inversedBy="questionSheets")
* #ORM\JoinColumn(name="assignment_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $assignment;
/**
* #ORM\OneToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="grid_id", referencedColumnName="id")
**/
protected $grid;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
//More getters/setters
}
Grid entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid")
**/
class Grid extends AbstractEntity
{
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\AbstractElement", mappedBy="grid", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\OrderBy({"y" = "ASC", "x" = "ASC"})
*/
protected $gridElements;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
}
Grid Element entity
/**
* #ORM\Table(name="lg_grid_element")
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\HasLifecycleCallbacks
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", inversedBy="gridElements")
* #ORM\JoinColumn(name="grid_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $grid;
public $type = ''; //This is a discriminator
}
Question entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid_question")
**/
class Question extends AbstractElement
{
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Answer", mappedBy="question", cascade={"persist"})
*/
protected $answers;
public $type = 'question'; //Inherited property, now filled in with discriminator value
public function __construct()
{
$this->answers = new ArrayCollection();
}
}
Answer entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid_answer")
**/
class Answer extends AbstractEntity
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Question", inversedBy="answers", cascade={"persist"})
* #ORM\JoinColumn(name="question_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $question;
public function __toSting() {
return (string) $this->getId();
}
}
Update 2
Updated AbstractElement entity based on #Wilt's answer.
/**
* #ORM\Table(name="lg_grid_element")
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\HasLifecycleCallbacks
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "abstractElement"="AbstractElement",
* "question"="Question",
* //Others
* })
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
//Same as above
}
This update created some problems with the NonUniformCollection which handles getting the correct Entity. This used to be based on the $type property.
However, having a $type property in an Entity which has * #ORM\DiscriminatorColumn(name="type", type="string") as a notation, is not allowed. Therefore all the classes making use of a discriminator have also been updated with the following.
const ELEMENT_TYPE = 'question'; //Overwritten from AbstractElement
/**
* #return string
*/
public function getType() //Overwritten from AbstractElement
{
return self::ELEMENT_TYPE;
}
Alas, the original problem remains.
I am not sure if this is causing your issue, but it seems to me that your inheritance mapping is not setup correctly:
You need to declare your discriminator column inside your entity definitions as written in the docs, they should not be set as properties, doctrine takes care of setting them inside your database:
/**
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({"element"="AbstractElement", "question"="Question", "text"="TextItem", "media"="MediaItem"})
*/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
//...
}
And is your abstract entity properly mapped as a #MappedSuperClass?
/**
* #MappedSuperclass
*/
class AbstractEntity
{
//...
}
This might be part of your solution, please come back with feedback after you updated accordingly...
I would like to find out a way to properly select a subset of records using ORM such as Doctrine in my case.
$rep = $this->entityManager->getRepository('Entity\ServiceLineItem');
$serviceItems = $rep->findBy(...);
In my case I want to select all instances of ServiceLineItem that have a $serviceType of "Testing".
In my code I have created the following two entities:
/** #Entity */
class ServiceLineItem
{
/** #Id #Column(type="integer") #GeneratedValue */
private $id;
/**#ManyToOne(targetEntity="ServiceType") */
private $serviceType;
/** #Column(length=255) */
private $description;
}
/** #Entity */
class ServiceType
{
/** #Id #Column(type="integer") #GeneratedValue */
private $id;
/** #Column(length=255)*/
private $serviceType;
}
My database looks like this:
serviceLineItem(id INT, serviceType_id INT, description VARCHAR);
serviceType(id INT, serviceType VARCHAR);
I am looking for a PHP ORM Doctrine equivalent of
select *
from ServiceLineItem
join ServiceType on ServiceLineItem.ServiceType_id = ServiceType.id
where ServiceType.name = "Testing";
Currently I am using
$serviceItems = $rep->findBy(array(
//"Testing" has an index of 1 in the database table
'serviceType' => '1'
));
I am looking for a way to avoid using underlying database indices and use higher-level constructs, such as actual values (i.e. "Testing"). How can I do that in Doctrine?
Single Table Inheritance
Maybe you're looking for Single Table Inheritance. In this case you have different model classes for a single table 'discriminated' by a specified column.
See:
Doctrine Documentation
Single Table Inheritance Pattern (Martin Fowler)
Current approach
In this case you must use a DQL query. Use $em->createQuery(<query>); or the built-in QueryBuilder:
Use a custom repository by extending your model's annotations:
/**
* #Entity(repositoryClass="Repository\ServiceLineItemRepository")
*/
Create the repository with your find method:
class ServiceLineItemRepository extends \Doctrine\ORM\EntityRepository
{
public function findByServiceType($serviceType)
{
$queryBuilder = $this->_em->createQueryBuilder('item');
if ($serviceType instanceof ServiceType) {
$queryBuilder->where('item.serviceType = :type')
->setParameter('type', $serviceType);
} else {
$queryBuilder->innerJoin('item.serviceType', 'type')
->where('type.serviceType = :type')
->setParameter('type', $serviceType);
}
return $queryBuilder->getQuery()->getResult();
}
}
!! Everything untested !!
So let's say we use a User and a Ticket class. They are normal entities, nothing fancy.
The User class contains this lines:
/**
* #ORM\ManyToMany(targetEntity="Ticket", mappedBy="collaborateurs")
**/
private $tickets;
The Ticket class contains this:
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="tickets")
* #ORM\JoinTable(name="users_to_tickets")
**/
private $collaborateurs;
To get all ticket's a user has I can just call the getTickets() function created by Symfony. As far as good. The Ticket class has a few additional fields like updated which is a DateTime field or status which is an integer. I would like to sort those tickets by status DESC and updated DESC
I know I could just make a function in the repository like findTicketsByUserOrderedByFooBar($user), but I'm wondering if there isn't a better way.
If you always want your tickets to be in that order you can set and orderBy on the association.
/**
* #ORM\ManyToMany(targetEntity="Ticket", mappedBy="collaborateurs")
* #ORM\OrderBy({"status" = "DESC", "updated" = "DESC"})
**/
private $tickets;
You can add an Helper method to your User entity and sort/filter DIRECTLY on the ArrayCollection with doctrine2 criteria. Something like this:
/**
* this return a ArrayCollection
*/
public function getTicketsByUserOrderedByFooBar()
{
$criteria = Criteria::create()
->orderBy(array('foo'=>'DESC','bar' =>'ASC'))
return $this->tickets->matching($criteria);
}
/**
* this return a ArrayCollection
*/
public function getTicketsByUserOrderedBytitle()
{
$criteria = Criteria::create()
->orderBy(array('title'=>'DESC'))
return $this->tickets->matching($criteria);
}
See also this
Hope this help.
Creating a function the way you suggested would be the suggested approach.
I have an Inheritance class as shown here:
As you can easily see users, buildings and hotels have addresses (more than one) and address table keeps the id of the owner in whose column.
Is my logic correct?
Let's say I want to get the address of user (or buildings or hotels) whose id is 2; must I run a DQL statement (and how?) or can I get it with find() function without DQL?
And I'll be happy if you give example since Doctrine documentation doesn't help much.
Thanks.
Edit: users, buildings and hotels are just symbolic names that is why they can have multiple addresses otherwise buildings and hotels would have only one address.
Edit 2:I think I couldn't make myself clear, when I talk about the Class Table Inheritance I mean entity class has the Discriminator column as
/**
* ...
*
* #DiscriminatorColumn(name="classname", type="string")
* #DiscriminatorMap({"Entities\users" = "Entities\users",
* "Entities\buildings" = "Entities\buildings"}) ... etc
*/
Each and every subclass is related to parent (Entity) with the foreign key relation as "id". But of course doctrine creates this relation already for me.
Usually an Address is a typical value object. Value objects are usually stored with the entity compositing the value object so it is neither about relations nor about class table inheritance. If your domain indicates otherwise (e.g. you can do something with your address, meaning), they might be an entity, than entity Hotel holds an entity Address (persisted in a n:m relation table) and entity Building holds and Address too (in a different n:m relation table).
If you go the value object route, things are different. You would store the address with the Building entity as well as with the Hotel entity (as you would do it with other value objects may it be Moneyor Email or Password). So you don’t need relations at all, just a few more fields. The issue with Doctrine 2 is, that it does not support Component mapping. Component mapping would be used to nicely store value objects. T accomplish the same thing with Doctrine 2, you would implement a #prePersist and a #postLoad handler like that:
class Hotel
{
private ;
/** These fields are persisted */
/** #Column(type=string) */
private $addressStreet;
/** #Column(type=string) */
private $addressCity;
/** #Column(type=string) */
private $addressZip;
/** #Column(type=string) */
private $addressCountry;
/** #prePersist */
public function serializeValueObjects()
{
$this->addressStreet = ->address->getStreet();
$this->addressCity = ->address->getCity();
$this->addressZip = ->address->getZip();
$this->addressCountry = ->address->getCountry();
}
public function unserializeValueObjects()
{
$this->address = new Address(->addressStreet, ->addressCity, ->addressZip, ->addressCountry);
}
}
As you need to serialize/unserialize Address value objects in various places, you might want to extract the serializing code into a separated class.
/**
*
* #Entity
* #Table(name="proposaltemplate")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="entitytype", type="string")
* #DiscriminatorMap({"proposal" = "ProposalTemplate","page" = "PageTemplate"})
*
*/
abstract class AbstractProposalTemplate
{
/**
*
* #var integer
* #Id
* #Column(type="integer")
* #generatedValue(strategy="AUTO")
*
*/
private $id;
}
next
#Entity
class ProposalTemplate extends AbstractProposalTemplate
{
#Id
#Column(type="integer")
#generatedValue(strategy="AUTO")
private $id;
}
next another class
#Entity
class PageTemplate extends AbstractProposalTemplate
{
/**
*
* #var integer
* #Id
* #Column(type="integer")
* #generatedValue(strategy="AUTO")
*
*/
private $id;
}
So you've got a superclass called "Entity", which has subclasses "User", "Building", and "Hotel".
Your "Entity" entity should have a OneToMany relation to Address. Let's imagine it looks like this, in your Entity definition:
/**
* #OneToMany(targetEntity="Address", mappedBy="whose"
*/
protected $addresses;
This is a more-or-less fine approach, though the use of inheritance is a little smelly.
Then if you want to iterate over the addresses, from inside User, Building, or Hotel:
foreach($this->addresses as $address){
//do something with adderess
}
Does that answer your question?
NOTE : if what I want is not possible, a "not possible" answer will be accepted
In the Doctrine 2 documentation about inheritance mapping, it says there are 2 ways :
Single table inheritance (STI)
Class table inheritance (CTI)
For both, there is the warning :
If you use a STI/CTI entity as a many-to-one or one-to-one entity you should never use one of the classes at the upper levels of the inheritance hierachy as “targetEntity”, only those that have no subclasses. Otherwise Doctrine CANNOT create proxy instances of this entity and will ALWAYS load the entity eagerly.
So, how can I proceed to use inheritance with an association to the base (abstract) class ? (and keep the performance of course)
Example
A user has many Pet (abstract class extended by Dog or Cat).
What I want to do :
class User {
/**
* #var array(Pet) (array of Dog or Cat)
*/
private $pets;
}
Because of the warning in Doctrine documentation, I should do that :
class User {
/**
* #var array(Dog)
*/
private $dogs;
/**
* #var array(Cat)
*/
private $cats;
}
This is annoying, because I loose the benefits of inheritance !
Note : I didn't add the Doctrine annotations for the mapping to DB, but you can understand what I mean
I'm tired, but this seems like much ado about nothing.
You missed the important bit of that warning:
If you use a STI/CTI entity as a many-to-one or one-to-one entity
That's not the case in your example! If you had not omitted the doctrine annotations, you might have noticed.
The association User::pets is OneToMany, not [One|Many]ToOne. One user has many pets.
The inverse association is OneToOne, but it's targeting User, which has no inheritance.
Robin's answer should have been a good hint -- you can log the sql queries and see what doctrine actually does to your database!
The bad-for-performance scenario is something like:
abstract class Pet { ... }
class Cat extends Pet { ... }
class Dog extends Pet { ... }
class Collar {
/**
* #Column(length="16")
*/
protected $color;
/**
* ManyToOne(targetEntity="Pet")
*/
protected $owner;
}
Now, if you wanted to iterate over all the blue collars, Doctrine runs into some trouble. It doesn't know what class $owner is going to be, so it can't use a Proxy. Instead, it's forced to eagerly load $owner to find out whether it's a Cat or a Dog.
This isn't a problem for OneToMany or ManyToMany relationships, because in that case, lazy loading works fine. Instead of a proxy, you get a PersistentCollection. And a PersistentCollection is always just a PersistentCollection. It doesn't care about it's own contents until you actually ask for them. So lazy loading works fine.
I think you've misunderstood, the section of the manual you've quoted is entitled "Performance impact", they're not telling you you can't do this, only that there are performance implications if you do. This makes sense for lazy loading - for heterogeneous collections of STI entities you have to go to the database and load the entity before you know what class it will be, so lazy loading isn't possible / doesn't make sense. I'm learning Doctrine 2 myself at the moment, so I mocked up your example, the following works OK for more:
namespace Entities;
/**
* #Entity
* #Table(name="pets")
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="pet_type", type="string")
* #DiscriminatorMap({"cat" = "Cat", "dog" = "Dog"})
*/
class Pet
{
/** #Id #Column(type="integer") #generatedValue */
private $id;
/** #Column(type="string", length=300) */
private $name;
/** #ManyToOne(targetEntity="User", inversedBy="id") */
private $owner;
}
/** #Entity */
class Dog extends Pet
{
/** #Column(type="string", length=50) */
private $kennels;
}
/** #Entity */
class Cat extends Pet
{
/** #Column(type="string", length=50) */
private $cattery;
}
/**
* #Entity
* #Table(name="users")
*/
class User
{
/** #Id #Column(type="integer") #generatedValue */
private $id;
/** #Column(length=255, nullable=false) */
private $name;
/** #OneToMany(targetEntity="Pet", mappedBy="owner") */
private $pets;
}
... and the test script ....
if (false) {
$u = new Entities\User;
$u->setName("Robin");
$p = new Entities\Cat($u, 'Socks');
$p2 = new Entities\Dog($u, 'Rover');
$em->persist($u);
$em->persist($p);
$em->persist($p2);
$em->flush();
} else if (true) {
$u = $em->find('Entities\User', 1);
foreach ($u->getPets() as $p) {
printf("User %s has a pet type %s called %s\n", $u->getName(), get_class($p), $p->getName());
}
} else {
echo " [1]\n";
$p = $em->find('Entities\Cat', 2);
echo " [2]\n";
printf("Pet %s has an owner called %s\n", $p->getName(), $p->getOwner()->getName());
}
All my cats and dogs load as the correct type:
If you look at the generated SQL, you'll notice that when the OneToMany targetEntity is "pet", you get SQL like this:
SELECT t0.id AS id1, t0.name AS name2, t0.owner_id AS owner_id3, pet_type,
t0.cattery AS cattery4, t0.kennels AS kennels5 FROM pets t0
WHERE t0.owner_id = ? AND t0.pet_type IN ('cat', 'dog')
But when it's set to Cat, you get this:
SELECT t0.id AS id1, t0.name AS name2, t0.cattery AS cattery3, t0.owner_id
AS owner_id4, pet_type FROM pets t0 WHERE t0.owner_id = ? AND t0.pet_type IN ('cat')
HTH.