Doctrine QueryBuilder groupBy relation not working - php

I have the following query:
$query = $qb->select('p')
->from(get_class($page), 'p')
->innerJoin('p1.translations', 't')
->groupBy('p.id')
->addGroupBy('t.id')
->getQuery();
Doctrine returns the above like:
Page entity -> [translation 1, translation 2, translation 3]
But I want the result like:
Page entity 1 -> translation 1
Page entity 1 -> translation 2
Page entity 1 -> translation 3
Does anyone know how I can do this? I want to return a list of entities.

First of all, both groupBy are completely superfluous, assuming both id fields you are grouping by are the primary keys of their respective tables. For any given (p.id, t.id) combination there will only be at most one result, whether you are grouping or not.
Second, even though you are joining the the translations table, you are not fetching any data from it (t is absent from your ->select()). It just appears that way since doctrine "magically" loads the data in the background when you call $page->getTranslations() (assuming your getter is called that way).
Third, your issue isn't with the groupBy. You are completely misunderstanding what an ORM actually does when hydrating a query result. The SQL query that doctrine generates from your code will actually return results in a fashion just like you expect, with "Page entity 1" repeated multiple times.
However, now comes the hydration step. Doctrine reads the first result row, builds a "Page entity 1" and a "Translation 1" entity, links them and adds them to it's internal entity registry. Then, for the second result, Doctrine notices that it has already hydrated the "Page entity 1" object and (this is the crucial part!) reuses the same object from the first row, adding the second translation to the ->translation collection of the existing object. Even if you read the "Page entity 1" again in a completely different query later in your code, you still get the same PHP object again.
This behaviour is at the core of what Doctrine does. Basically, any table row in the database will always be mirrored by a single object on the PHP side, no matter how often or in what way you actually read it from the database.
To summarize, your query should look like this:
$query = $qb->select('p, t')
->from(get_class($page), 'p')
->innerJoin('p.translations', 't')
->getQuery();
If you really need to iterate over all (p, t) combinations, do so with nested loops:
foreach ($query->getResult() as $p) {
foreach ($p->getTranslations() as $t) {
// do something
}
}
EDIT: If your relationship is bidirectional, you could also turn your query around:
$query = $qb->select('t, p')
->from('My\Bundle\Entity\Translation', 't')
->innerJoin('t.page', 'p')
->getQuery();
Now in your example you actually get 3 results that you can iterate over in a single foreach(). You would still only get a single "Page entity 1" object, with all the translations in the query result pointing to it.

Related

Symfony Doctrine QueryBuilder OneToMany filtering

I have following code in my Repository
// ProductBundle/Repository/ProductRepository.php
$qb->where($qb->expr()->eq('afp.id', 15));
$qb->andWhere($qb->expr()->eq('afp.id', 14));
return $qb
->select('a', 'afp')
->leftJoin('a.productFields', 'afp')
->getQuery()
->getResult();
But I always get null return, but I want to get products which have both productFields (so orWhere is not good).
You want to use MEMBER OF instead of comparing id. Otherwise, you're looking for a record that has two different id values, which of course isn't possible.
This will do what you want:
$qb->where($qb->expr()->isMemberOf(15, 'a.productFields'));
$qb->andWhere($qb->expr()->isMemberOf(14, 'a.productFields'));
Try something like this (Symfony 5):
$qb->andWhere(':c MEMBER OF a.productFields');
$qb->setParameter('c', 15);

How can I order NULL values first on a Doctrine 2 collection using annotations?

I have a project using Symfony 2 and containing Doctrine 2 entities. Some of these entities are related to each other. This association is defined by an annotation:
/**
* #ORM\OneToMany(targetEntity="Event", mappedBy="firstEntityId" cascade={"persist", "remove"})
* #ORM\OrderBy({"dateEnd" = "DESC", "dateBegin" = "DESC"})
*/
private $events;
As you can see, this association contains several events that have a start and an end date. When retrieving this collection, I want to have the most recents events (i.e. those which have not ended yet or have ended recently) sorted first.
The problem with the current approach is that it will sort events with an end date of NULL after all other events.
How can I tell Doctrine to sort the events with an end date of NULL first and then sort the remaining events by descending end date?
I have so far seen several questions on SO about how to tell Doctrine how to order entities. However, none of them mention annotations. Tricks with reversing the sign as suggested e.g. in Doctrine 2 Order By ASC and Null values in last do not work because Doctrine does not accept anything other than a property name and ASC or DESC in the annotation.
It's an old post but i found a pretty simple solution if you are using doctrine query builder :
$sortDirection = 'ASC';
$qb = $this->createQueryBuilder('e');
$qb->addSelect('CASE WHEN e.valueToOrder IS NULL THEN 1 ELSE 0 END AS HIDDEN myValueIsNull');
//other stuffs
//$qb->where ...
$qb->orderBy('myValueIsNull','ASC');
$qb->addOrderBy('e.valueToOrder',':sortDirection');
$qb->setParameter(':sortDirection',$sortDirection);
return $qb->getQuery()->getResult();
PHP way, besides being slower, avoid to use offsets (for an infinite scroll for example)
Thanks to https://stackoverflow.com/a/23420682/6376420
Probably not. There is an SQL syntax that allows to ORDER BY column DESC NULLS FIRST. However, it is not supported by all DB vendors and thus if I scanned the merge request correctly, has not been merged into DQL. Depending on which database platform you use, you may be lucky. The comments in the merge request provide insight into how to extend Doctrine at different points to implement the behavior, maybe that helps you to do it by yourself.
My workaround is to create add an additional select to the query and extract the entity from the resulting array collection, it would be better to have it only examined in query time and not select it (to keep the result array intact) but I have not found a proper solution to this yet (using QueryBuilder).
$queryBuilder = $this->getEntityManager()->createQueryBuilder();
$queryBuilder->select('e')
->from(Entity::class, 'e')
// We use ZZZZ here as placeholder to push the null values last, use 'AAAA' to sort them first.
->addSelect('CASE WHEN(e.name IS NULL) THEN \'ZZZZ\' ELSE e.name END AS name')
->addOrderBy('name', 'ASC');
// As we have a array result due to the "addSelect()" above, we must extract the entities now, in this example by looping over the result array.
$entities = array_map(function ($contributor) {
return $contributor[0];
}, $queryBuilder->getQuery()->getResult());
I had the same problem and this was my approach:
If we are not talking about a huge amount of processing you can use a custom sort, I needed to have the results sorted by a column in asc or desc depending on user choice. BUT, I also needed the null values of the same column to appear first. So after a lot of googling for the NULLS FIRST approach I decided to make an usort right after you get the result from the query builder:
// Custom sort to put the nulls first
usort($invoices, function(Entity $a, Entity $b) use ($order) {
if(null === $a->getNumber())
return -1;
if(null === $b->getNumber())
return 1;
if(strtoupper($order) == "DESC") {
if($a->getNumber() > $b->getNumber())
return -1;
if($b->getNumber() > $a->getNumber())
return 1;
} else {
if($a->getNumber() < $b->getNumber())
return -1;
if($b->getNumber() < $a->getNumber())
return 1;
}
});
This way when you get the results from the QueryBuilder you will get the NULLS first and then you will have your original sorting. if it was ASC it will stay ASC and vice-versa.
In case NULL values are required at the end you just need to change the first 'if' to the contrary sign.
I know this question is already Answered but thought I might leave this here in case it helps someone else.

Doctrine 2 delete with query builder

I have two Entities with relation OneToMany, Project and Services. Now i want to remove all the services by project_id.
First attempt:
$qb = $em->createQueryBuilder();
$qb->delete('Services','s');
$qb->andWhere($qb->expr()->eq('s.project_id', ':id'));
$qb->setParameter(':id',$project->getId());
This attempt fails with the Exception Entity Service does not have property project_id. And it's true, that property does not exists, it's only in database table as foreign key.
Second attempt:
$qb = $em->createQueryBuilder();
$qb->delete('Services','s')->innerJoin('s.project','p');
$qb->andWhere($qb->expr()->eq('p.id', ':id'));
$qb->setParameter(':id',$project->getId());
This one generetate a non valid DQL query too.
Any ideas and examples will be welcome.
You're working with DQL, not SQL, so don't reference the IDs in your condition, reference the object instead.
So your first example would be altered to:
$qb = $em->createQueryBuilder();
$qb->delete('Services', 's');
$qb->where('s.project = :project');
$qb->setParameter('project', $project);
If you really can't get project object and you have to handle only with id you can use this.
Citation from doctrine documentation:
There are two possibilities for bulk deletes with Doctrine. You can either issue a single DQL DELETE query or you can iterate over results removing them one at a time. (Below I paste only first solution)
DQL Query
The by far most efficient way for bulk deletes is to use a DQL DELETE query.
Example that worked in my project
$q = $em->createQuery('delete from AcmeMyTestBundle:TemplateBlock tb where tb.template = '.intval($templateId));
$numDeleted = $q->execute();
In entity TemplateBlock I have property called template which is mapped to template_id in db.
But I agree that highly preferred way of doing it is using objects.

Doctrine $record->get() with a JOIN

I'm using the following piece of code to retrieve the tags of a shop:
$tags = $this->getObject()->get('Tag');
$this->getObject() returns a Shop object, and ->get('Tag') returns an array of Tag objects related to this shop.
Here's how my database is arranged: 1 Shop = 1 or more Tag, and 1 Tag = 1 Tag_Translation.
What i'd like to do is to retrieve, instead of an array of Tag objects, and array of Tag objects with their translations (in other words, a kind of JOIN).
How is that possible, keeping that same syntax? Thank you very much, i'm new to Doctrine and ORMs in general, i would have had no problem doing it with MySQL but here ...
You may solve this issue like this
a) You can call Tag Models function, when you need the translation
$tag->getTagTranslation()
b) Or you can overwrite your Shop's getTag() function and build your own Query with DQL as #greg0ire suggested, to fetch translation and tag at once
public function getTag(){
return Doctrine_Query::create()
->from("Tag t")
->leftJoin("t.TagTranslation tt")
->addWhere("t.shop_id = ?", $this->getId())
}
(Of course you can name a new function e.g. getTagsWithTranslation())
This assumes, you have built a schema.yml with proper relations !

Doctrine 2 entities with one to many relations

I'm trying to fetch an annotation with an one to many relation but as soon as I use a join I will end up with the following data:
entities\Topic
id = 1 // integer
title = "example" // string
comments // entities\Comment = oneToMany
id = 1 // integer
comment = "first comment" // string
topic // entities\Topic = manyToOne
id = 1 // again..
title = "example"
Why does doctrine fetch the the manyToOne relation inside comments when I join on the topics comments? This is my query:
$this->em->createQueryBuilder()
->from('Entities\Topic', 't')
->select("t, c")
->leftjoin("t.comments", 'c')
->where('t.id = :id')
->setParameter('id', 1)
->getQuery()->getSingleResult();
Shouldn't the topic property be null or at least an empty arrayCollection?
Another thing:
Why do I get a PersistentCollection back as comments when I specify that comments is an arrayCollection? Do I always need to use unwrap on the PersistentCollection before I can loop through it?
On the first bit - it's likely populating the topic because it already has the data. Were the data not already on-hand, you'd likely have a proxy entity there. It would never be null, because null would be wrong (the comment does have a topic).
As for ArrayCollection/PersistentCollection, you can safely ignore the distinction. I don't know the implementation details, but basically, the EM gives back stuff in PersistentCollections, which I assume play a role in managing the entities in the collection. If you're creating the collection, you use ArrayCollection. Sorry I can't be more specific here, but the bottom line is, you should probably just think about any PersistentCollections you get from the EM as just "a collection"

Categories