As the title states,
I would like to run 1 query to get results from a table with the count of their respective relationships.
Lets say I have a Person entity with a OneToMany relationship with a Friend entity
The Person entity might look something like the following:
class Person
{
/**
* #ORM\OneToMany(...)
*/
protected $friends;
public function __construct()
{
$this->friends = new ArrayCollection();
}
...
}
A classic SQL solution to what I want to achieve might look like the following:
SELECT p.*, COUNT(f.id) as friendsCount
FROM Persons p
LEFT JOIN Friends f
ON f.person_id = p.id
GROUP BY p.id
Now I am wondering if this could be done in DQL as well and storing the count value into the Person Entity
Lets say I expand the Person entity like: (Keep in mind this is just an idea)
class Person
{
/**
* #ORM\OneToMany(...)
*/
protected $friends;
protected $friendsCount;
public method __construct()
{
$this->friends = new ArrayCollection();
}
...
public function getFriendsCount()
{
return $this->friendsCount;
}
}
Now I am having trouble finding how I could populate the count value in the entity from DQL:
SELECT p, /* What comes here ? */
FROM AppBundle\Entity\Person p
LEFT JOIN p.friends f
GROUP BY p.id
PS: I do know I could just do:
$person->getFriends()->count();
And even mark it as extra lazy to get the count result.
I just though this count relationships example demonstrated well what I am trying to do.
(Which is populating the entity's non #ORM\Column properties from dql)
Is this possible with Doctrine ?
Is this breaking some solid principles ? (SRP ?)
Cookie for your thoughs ;)
You probably just want to select the count as you need it, as described above with $person->getFriends()->count();. However, you can select both an object and a count at the same time (see these Doctrine query examples), there is one very similar to what you are doing:
SELECT p, COUNT(p.friends)
FROM AppBundle\Entity\Person p
LEFT JOIN p.friends f
GROUP BY p.id
What should be returned back is an array of arrays with the object and count, like so:
[
[Person, int],
[Person, int],
...,
]
Your best bet would be to make that a Repository call on your PersonRepository, something like findPersonsWithFriendCount(), which would look like:
public function findPersonsWithFriendCount()
{
$persons = array();
$query = $this->_em->createQuery('
SELECT p, COUNT(p.friends)
FROM AppBundle\Entity\Person p
LEFT JOIN p.friends f
GROUP BY p.id
');
$results = $query->getResult();
foreach ($results as $result) {
$person = $result[0];
$person->setFriendsCount($result[1]);
$persons[] = $person;
}
return $persons;
}
Keep in mind you'd need to add a setFriendsCount() function on your Person object. You could also write a native SQL query and use result set mapping to automatically map your raw result's columns to your entity fields.
Related
In my Symfony project, I have an "ExerciceComptable" and a "DocumentAttendu" entities.
There is a relation in ExerciceComptable that reference DocumentAttendu (OneToMany).
In DocumentAttendu, I have a property named "recu" which is a boolean.
I need to retrieve all "ExerciceComptable" that are completed, meaning that all "DocumentAttendu" for an "ExerciceComptable" have the property "recu" set to true.
How can I achieve that ?
ExerciceComptable
#[ORM\OneToMany(mappedBy: 'exercice', targetEntity: DocumentAttendu::class)]
private Collection $documentAttendus;
/**
* #return Collection<int, DocumentAttendu>
*/
public function getDocumentAttendus(): Collection
{
return $this->documentAttendus;
}
public function addDocumentAttendu(DocumentAttendu $documentAttendu): self
{
if (!$this->documentAttendus->contains($documentAttendu)) {
$this->documentAttendus->add($documentAttendu);
$documentAttendu->setExercice($this);
}
return $this;
}
public function removeDocumentAttendu(DocumentAttendu $documentAttendu): self
{
if ($this->documentAttendus->removeElement($documentAttendu)) {
if ($documentAttendu->getExercice() === $this) {
$documentAttendu->setExercice(null);
}
}
return $this;
}
DocumentAttendu
#[ORM\ManyToOne(inversedBy: 'documentAttendus')]
#[ORM\JoinColumn(nullable: false)]
private ?ExerciceComptable $exercice = null;
#[ORM\Column(nullable: true)]
private ?bool $recu = null;
public function getExercice(): ?ExerciceComptable
{
return $this->exercice;
}
public function setExercice(?ExerciceComptable $exercice): self
{
$this->exercice = $exercice;
return $this;
}
public function isRecu(): ?bool
{
return $this->recu;
}
public function setRecu(?bool $recu): self
{
$this->recu = $recu;
return $this;
}
What I tried
$qb = $this->createQueryBuilder( 'ec' );
$qb->join( 'ec.documentAttendus', 'da');
$qb->andWhere('da.recu = true');
This is not working properly. If just one "DocumentAttendu" have "recu" = true, then the query will find it. I need all "DocumentAttendu" to have "recu" = true, not just one out of five for example.
I also tried to use Criteria, but I don't really understand how that works. I tried some line with "having('COUNT')", etc...But I'm not sure I used it correctly.
Important point, I need to be in "ExerciceComptableRepository".
The easiest solution might be a subquery. More specifically, use the Expr class from doctrine. Using a "where not exists (subquery)", should give you the correct results.
You'd get something like:
// This subquery fetches all DocumentAttendu entities
// for the ExerciceComptable where recu is false
$sub = $this->getEntityManager()->getRepository(DocumentAttendu::class)->createQueryBuilder('da');
$sub->select('da.id');
$sub->where('da.exercice = ec.id');
$sub->andWhere('da.recu IS FALSE');
// We fetch the ExerciceComptable entities, that don't
// have a result from the above sub-query
$qb = $this->createQueryBuilder('ec');
$qb->andWhere($qb->expr()-not(
$qb->expr()->exists($sub->getDQL()) // This resolves to WHERE NOT EXISTS (...)
))
In short: you're fetching all the ExerciceComptable entities that do not have DocumentAttendu entities with recu = false
Note: if a ExerciceComptable entity doesn't have any documentAttendus, this query will also return that ExerciceComptable entity
My solution is not a full doctrine solution and could make performance issue for larger data, but i believe it could be a great way to deal with very specific case like this.
Lets talk about the correct Sql query before doctrine, it should be something like that :
SELECT ec.id FROM ExerciceComptable ec
INNER JOIN (SELECT COUNT(*) total, exercice_comptable_id FROM DocumentAttendu)
all_documents ON all_documents.exercice_comptable_id = ec.id // COUNT ALL document for each execice
INNER JOIN (SELECT COUNT(*) total, exercice_comptable_id FROM DocumentAttendu da WHERE da.recu = 1)
received_documents ON received_documents.exercice_comptable_id = ec.id // COUNT ALL received document for each execice
WHERE all_documents.total = received_document.total;
Then only the ExerciceComptable with a total documents = received document will be retrieved.
It's important to know that subquery inside select are bad for performance since it doest 1 query for each result (so if you have 100 ExerciceComptable it will do 100 subqueries) where subquery using join only do 1 query for the the whole query. This is why i builded my query like that.
The problem is you wont get entity object with a raw mysql function inside a repositories.
So you have two choice.
Using subqueries inside Doctrine DQL (which is painfull for very complexe case). I advise you to do it only if you have performance issue
Execute the first query with raw sql -> retrieve only the ids -> call doctrine function findBy(['id' => $arrayOfIds]) -> you have the object you're looking for.
It's a trick, it's true.
But i believe specific usecase with doctrine are often very hard to maintain. Where sql query can be easily tested and changed.
The fact is that only the first will be the one to maintain and the second query will always be very fast since query on id are very fast.
If you want to see a case of DQL with subquery look at : Join subquery with doctrine 2 DBAL
I gave you generic guideline and i hope it helped.
Just never forget : Never Ever do subequeries inside select or where. It has very bad performance since it does one subqueries on server side for each line of result. Use Inner / Left Join to do that
I have simple Entity:
id
username
guard
"guard" is id of another user from the same Entity. I have to render view with simple table:
username | name of guard
-------------------------
John | Bob
I tried to do that by query:
$ur = $this->getDoctrine()->getRepository(User::class)->createQueryBuilder('u')
->leftJoin(User::class, 'u2', \Doctrine\ORM\Query\Expr\Join::WITH, 'u.guard = u2.id')
->getQuery()
->getResult();
but it gives me just id and username, no joined data.
I know that entire query should be like:
SELECT
*
FROM
user u0_
LEFT JOIN user u1_ ON (u0_.guard = u1_.id)
but I can't find the way to implement that by QueryBuilder and then to access that in twig template.
Regards
OK, I found out the mistakes in my code:
I tried to set that OneToOne realtion and that was small mistake, but I needed here ManyToOne.
/**
* Many Users have One Guard (User)
* #ORM\ManyToOne(targetEntity="User")
*/
private $guard = 0;
When I did that Symfony automatically force me to change my code and in column "guard" I have to insert User object.
After that I don't need join anymore - just select data from table and guard column includes User object which I can use in Twig, etc.
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function findAllDB()
{
$qb = $this->createQueryBuilder('u');
$query = $qb->getQuery();
return $query->execute();
}
}
I'm making a tool in which a user can view data from an entity, where they can choose what data and how they see the records.
I created a form with two date fields (start and end) and a list of fields that correspond to data counts and sums of the entity.
My question is:
How I can create a dynamically QueryBuilder that allows me to add fields based on what the user wants to see?
EDIT for Symfony2 dynamic queryBuilder
public function reportData($fields, $dateStart, $dateFinish)
{
$em = $this->getEntityManager()
->getRepository('AcmeBundle:Entity');
$query = $em->createQueryBuilder('e');
foreach($fields as $field)
{
switch($field)
{
case 'totalResults':
$query->setect('SUM(e.id) AS '.$field);
break;
}
}
$query->addWhere('e.dateStart >= :dateStart');
$query->addWhere('e.dateFinish <= :dateFinish');
...
Something like this ? You store all your select queries in an array, then pass the array to the query builder after testing each of your fields.
public function reportData($fields, $dateStart, $dateFinish)
{
$em = $this->getEntityManager()
->getRepository('AcmeBundle:Entity');
$query = $em->createQueryBuilder('e');
$select_array = array();
foreach($fields as $field)
{
switch($field)
{
case 'totalResults':
$select_array[] = 'SUM(e.id) AS '.$field;
break;
}
}
$query->select($select_array);
$query->addWhere('e.dateStart >= :dateStart');
$query->addWhere('e.dateFinish <= :dateFinish');
....
Basically, you want to keep on adding the
Select Fields
based upon the conditions.
So, the solution is simple.
You can use,
$queryBuilder->addSelect();
See Doctrine Query Builder Documentation
I would do a regular full query then filter it into a not doctrine object (dao/dto) then display it.
This way you can do the complex and optimized query first, then filter the result on whatever you want, even if it's not related to the query itself
I have a User entity which has an ArrayCollection of Positions. Each Position has for sure a user_id property.
Now i want to get all positions from a user (to get all i would do $user->getPositions()) that are matching a specific query, for example have a date property that matches the current date. Therefor i want to do something like $user->getCurrentPositions() and it should return a subset of the positions related to that user.
How is that possible?
EDIT:
What i really wanna do is something like this in my controller:
$em = $this->getDoctrine()->getManager();
$users = $em->getRepository('fabianbartschWhereMyNomadsAtBundle:User')->findAll();
foreach ($users as $user) {
$positions = $user->getCurrentPositions();
foreach ($positions as $position) {
echo $position->getLatitude().'<br>';
}
}
I wanna iterate over all users and from each user i want to have the relevant positions. But that isnt possible from the repository i guess, as i get the following message: Attempted to call method "getCurrentPositions" on class ...
If you are using Doctrine you can use the built-in Criteria API which is meant for this purpose exactly.
Collections have a filtering API that allows you to slice parts of data from a collection. If the collection has not been loaded from the database yet, the filtering API can work on the SQL level to make optimized access to large collections.
Ok i found out, its for sure possible with Repositories:
Entity\User.php
/**
* #ORM\Entity(repositoryClass="fabianbartsch\WhereMyNomadsAtBundle\Entity\UserRepository")
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
Entity\UserRepository.php
/**
* UserRepository
*/
class UserRepository extends EntityRepository
{
public function getCurrentPositions()
{
$query = $this->getEntityManager()
->createQuery(
"SELECT p
FROM xxx:Position p
WHERE p.start <= '2014-08-17' AND p.end >= '2014-08-17'"
);
try {
return $query->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
}
In the user object only related position entries are affected by the query, so is no need to join user entity with the position entity. Pretty simple, should just try out instead posting on stackoverflow, sry guys :P
I have a entity with the next join:
class blogComment
{
....
/**
* #ORM\OneToMany(targetEntity="BlogComment", mappedBy="replyTo")
*/
protected $replies;
....
}
Now I get successfully all the replies. But I only want to get: where active = true
How to do that?
Oke if you guys recommend to get the comments by query in the controller how to build a nested array to get result like this:
For solving the part where you only want active replies there are a couple of options:
1) Use some custom DQL in a repository:
$dql = 'SELECT bc FROM BlogComment bc WHERE bc.replyTo = :id AND bc.active = :active';
$q = $em->createQuery($dql)
->setParameters(array('id' => $id, 'active' => true));
2) Using ArrayCollection::filter() in the getter:
public function getReplies()
{
return $this->replies
->filter(function ($reply) {
return $reply->isActive();
})
->toArray();
}
3) Using ArrayCollection::matching() (Collection Criteria API) in the getter:
use Doctrine\Common\Collections\Criteria;
// ...
public function getReplies()
{
$criteria = new Criteria::create()
->where(Criteria::expr()->eq('active', true));
return $this->replies
->matching($criteria)
->toArray();
}
4) Use Filters. These can add where clauses to queries regardless of where that query is generated. Please see the docs.
If you want to be able to fetch an entire set of replies, nested and all, in a single query, you need to implement some kind of "tree" of "nested set" functionality. I'd advise you to look at the Tree behavior of l3pp4rd/DoctrineExtensions.
http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html
Wherever you are obtaining your blog comments to display them (probably on a controller), you need to customise your query so that only the active replies are extracted. Something like:
$query = $em->createQuery('SELECT b FROM blogComment b JOIN b.replies r WHERE r.active = :active');
$query->setParameter('active', true);
$blogComments = $query->getResult();
EDIT:
For your new requirement of nested replies, you would need to specify a relationship between a comment entity and its parent comment.