Symfony 2 OneToMany relations - wrong results in association - php

I have two tables in DB with OneToMany relations. Using 'createQueryBuilder()' method in 'EntityRepository' i try to select some objects with conditions. There is my method:
$query = $this->getEntityManager()->createQueryBuilder();
$query->select("parent")
->from('TestAppBundle:Parent', 'parent')
->leftJoin("parent.children", 'child', 'WITH', 'child.IdP = parent.id')
->where('child.date < :date')
->andWhere('child.status = :null')
->setParameter('date', new DateTime())
->setParameter('null', 0);
And it works almost good. I get Parent Objects with Children Objects in ArrayCollections. Methods select the Parent Objects with conditions but the problem is I get also Children Objects which don't keep the conditions.
I want to get only Parent Objects which keep conditions and Children Objects which also keep conditions. At this time a have to filter results after query and remove Children Object manualy.
I hope you will understand the problem :)

Basically, if you don't select the entities at query time you'll lazy load all children for the parent when you call getChildren(). Select both children and parents like this to avoid lazy loading:
$query->select("parent, child")
For more information please see my answer to a similar question.

Could you try something like this:
$query = $this->getEntityManager()->createQueryBuilder();
$query->select("parent")
->from('TestAppBundle:Parent', 'parent')
->leftJoin("parent.children", 'child', 'WITH', 'child.status = 0');

As far as I understand your needs you should not use leftJoin - with this field you will get parents that don't have children. Use innerJoin instead.
Assuming that child.IdP refers to the parent.id in your model definition you don't have to use WITH clause in this way. So the queryBuilder would be:
$query = $this->getEntityManager()->createQueryBuilder();
$query->select("parent")
->from('TestAppBundle:Parent', 'parent')
->innerJoin("parent.children", 'child')
->where('child.date < :date and child.status = :null')
->setParameter('date', new DateTime())
->setParameter('null', 0);
Regards.

Related

Doctrine 2, association mapping with conditions

I have a similar problem and DB structure:
Doctrine2 association mapping with conditions
But, I need collections of Article with approved comments:
How to do assotiation mapping, that do without N+1 ?
$articles = $repository->findAllArticlesWithApprovedComments();
Foreach($articles as $article){
$article->getTitle();
foreach($article->getAprovedComments() as $comment){
$comment->getText();
}
}
Criteria worked, if i use lazy load, but its N+1 problem. If I use Eager load (join and addSelect ) - Criteria not work.
If I use this code:
$articles = $em->createQueryBuilder()
->from(Article::class,'article')
->leftJoin('article.comments','comments')
->addSelect('article')
->addSelect('comments')
->andWhere('comments.approved= :ap ')
->setParameter('ap',true)
->getQuery()->getResult();
I will receive articles with approved comments, but if the article has 0 comments, it will not fall into the collection of articles.
How to get articles with approved comments, but if there are no comments in the article, the article remains in the collection?
Example:
I have in DB:
Article1: [approvedComment, nonAprovedComment]
Article2: [nonAprovedComment]
Article3: [approvedComment]
I need result (with doctrine, non filter in code):
Article1: [approvedComment]
Article2: []
Article3: [approvedComment]
You can use a join instead of a where condition to filter your collection on the database level.
Your query would then look like this:
$articles = $em->createQueryBuilder()
->from(Article::class, 'article')
->leftJoin('article.comments', 'comments', 'WITH', 'comments.approved = :ap')
->addSelect('article')
->addSelect('comments')
->setParameter('ap', true)
->getQuery()->getResult();
Since this is a left join, it will return all articles, even if they do not have any approved comment.

Assign a query to variable to then perform two separate queries?

Is it possible to start an eloquent query, assign it to a variable then continue using the variable for two separate queries without them conflicting with one another. A simple example:
$students = $this->student
// more query stuff
->where('is_active', 1);
$bachelorStudents = $students
->where('course_id', 3)
->get();
$masterStudents = $students
->where('course_id', 4)
->get();
or would I need to do:
$bachelorStudents = $this->student
->where('course_id', 3)
->get();
$masterStudents = $this->student
->where('course_id', 4)
->get();
I always thought I could do the former, but some of my results appear to show I can't but I am open to believe that if you can do it then perhaps I'm doing something wrong.
When you're calling
$students = $this->student->where('is_active', 1);
you're creating a query builder object. Calling where*() on this object updates the object by adding given criteria. Therefore it's not possible to achieve what you want in your first code snippet, because when you call
$masterStudents = $students
->where('course_id', 4)
->get();
the query builder already contains where('course_id', 3) constraint added when you bachelorStudents.
Once you do that:
$students = $this->student->where('is_active', 1);
$stundents will contain a query builder with your where clause
If you do:
$bachelorStudents = $students->where('course_id', 3)->get();
You'll add another where clasuse to the $students builder, and this should work as you expect
But, when you do:
$masterStudents = $students->where('course_id', 4)->get();
You are adding another where clasuse to the same $students builder, thus resulting the query builder to be something like this:
$students->where('is_active', 1)
->where('course_id', 3)
->where('course_id', 4)
->get();
That probably isn't what you expect, because you have 2 where clauses with different course_id values
Think of $student as an object you modify everytime you add a clause, so you can use it for progressive query building, but remember that once you've added a clause to the query builder, the object is modified and the clause will be keept in the builder, so when you re-use the builder it will contain all the clasuses you previously added
Also, Rembember that when you need to apply some pre-defined filters to your query, in Laravel you should use query scopes
While everyone is explaining query builder and how it works, here's your answer.
1) Start off your query builder
$studentsQuery = $this->student
//Start a new query builder (optional)
->newQuery()
->where('is_active', 1);
2) Clone the initial query builder to our separate queries
$bachelorStudentsQuery = clone $studentsQuery;
$masterStudentsQuery = clone $studentsQuery;
3) Assign your where conditions and get the results
$bachelorStudentsResult = $bachelorStudentsQuery->where('course_id', 3)
->get();
$masterStudentsResult = $masterStudentsQuery->where('course_id',4)
->get();
Your use case is too simple for cloning.
It might help you DRY your code when lots of method chaining has been performed, especially when applying filters to queries.

Assign multiple distinct Entities to one Array row in Doctrine

When I execute something like this in Doctrine:
$qb = $doctrine
->getRepository('EntityA')
->createQueryBuilder('a')
->addSelect('b')
->join('EntityB', 'b', 'WITH', 'a.b = b')
->getQuery()
->getResult()
I get an array that looks like this:
array(0 => EntityA,
1 => EntityB,
2 => EntityA,
4 => EntityB)
In fact, I get 2 result rows, but an array which has a size of 4. This makes iterating over it for displaying in templates nearly impossible.
I would like a result like this:
array(0 => array(EntityA, EntityB),
1 => array(EntityA, EntityB))
Of course I could create a mapping on EntityA that references EntityB. But even with the possibility to change the loading behavior, LAZY, EAGER etc., it could be desirable to have the possibility to create such relations on the fly.
For example:
In overviews with large amounts of Entities, EAGER loading is needed to prevent excessive amounts of queries. But when I want to display only one Entity and do not need the extra data, LAZY loading is more desirable.
Since you have EntityB mapped as EntityA.b, as indicated by this relation:
...join('EntityB', 'b', 'WITH', 'a.b = b')...
Thus you do not need in fact to add b to the select as you are loading it either eagerly or lazily.
Remove ->addSelect('b') from your query builder and use EntityB trough your EntityA.b mapping.
Example:
$as = $doctrine
->getRepository('EntityA')
->createQueryBuilder('a')
->join('EntityB', 'b', 'WITH', 'a.b = b')
->getQuery()
->getResult();
foreach($as as $a){
echo "EntityA property".$a->id;
echo "EntityB property".$a->b->id;
}
This query will return an ArrayCollection of EntityA.
Note: The example above assumes that EntityA.b is a public property.
Update:
After some digging I found this older post: Doctrine 2 QueryBuilder add multiple select elements /parameters?. According to it you can get the result you wan by directly separarating the entities with commas instead of using the ->addSelect() method.
Try the following DQL:
$query = $em->createQuery("SELECT a, b FROM EntityA a JOIN a.b b")
The query above will dynamically eagerly fetch b.

How to use a findBy method with comparative criteria

I'd need to use a "magic finder" findBy method using comparative criteria (not only exact criteria). In other words, I need to do something like this:
$result = $purchases_repository->findBy(array("prize" => ">200"));
so that I'd get all purchases where the prize is above 200.
The class Doctrine\ORM\EntityRepository implements Doctrine\Common\Collections\Selectable API.
The Selectable interface is very flexible and quite new, but it will allow you to handle comparisons and more complex criteria easily on both repositories and single collections of items, regardless if in ORM or ODM or completely separate problems.
This would be a comparison criteria as you just requested as in Doctrine ORM 2.3.2:
$criteria = new \Doctrine\Common\Collections\Criteria();
$criteria->where(\Doctrine\Common\Collections\Criteria::expr()->gt('prize', 200));
$result = $entityRepository->matching($criteria);
The major advantage in this API is that you are implementing some sort of strategy pattern here, and it works with repositories, collections, lazy collections and everywhere the Selectable API is implemented.
This allows you to get rid of dozens of special methods you wrote for your repositories (like findOneBySomethingWithParticularRule), and instead focus on writing your own criteria classes, each representing one of these particular filters.
This is an example using the Expr() Class - I needed this too some days ago and it took me some time to find out what is the exact syntax and way of usage:
/**
* fetches Products that are more expansive than the given price
*
* #param int $price
* #return array
*/
public function findProductsExpensiveThan($price)
{
$em = $this->getEntityManager();
$qb = $em->createQueryBuilder();
$q = $qb->select(array('p'))
->from('YourProductBundle:Product', 'p')
->where(
$qb->expr()->gt('p.price', $price)
)
->orderBy('p.price', 'DESC')
->getQuery();
return $q->getResult();
}
You have to use either DQL or the QueryBuilder. E.g. in your Purchase-EntityRepository you could do something like this:
$q = $this->createQueryBuilder('p')
->where('p.prize > :purchasePrize')
->setParameter('purchasePrize', 200)
->getQuery();
$q->getResult();
For even more complex scenarios take a look at the Expr() class.
$criteria = new \Doctrine\Common\Collections\Criteria();
$criteria->where($criteria->expr()->gt('id', 'id'))
->setMaxResults(1)
->orderBy(array("id" => $criteria::DESC));
$results = $articlesRepo->matching($criteria);
The Symfony documentation now explicitly shows how to do this:
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT p
FROM AppBundle:Product p
WHERE p.price > :price
ORDER BY p.price ASC'
)->setParameter('price', '19.99');
$products = $query->getResult();
From http://symfony.com/doc/2.8/book/doctrine.html#querying-for-objects-with-dql
I like to use such static methods:
$result = $purchases_repository->matching(
Criteria::create()->where(
Criteria::expr()->gt('prize', 200)
)
);
Of course, you can push logic when it is 1 condition, but when you have more conditions it is better to divide it into fragments, configure and pass it to the method:
$expr = Criteria::expr();
$criteria = Criteria::create();
$criteria->where($expr->gt('prize', 200));
$criteria->orderBy(['prize' => Criteria::DESC]);
$result = $purchases_repository->matching($criteria);
Copying the findBy query and modifying it to return your expected result is a good approach.

PHP Doctrine toArray problem

I have a problem with the toArray() method in Doctrine. Its doesn't get my relations:
First query :
$q = Doctrine::getTable('posts')->find(1);
debug($q->toArray(true));
Print the postid=1 with out the relations
$q = Doctrine::getTable('posts')->find(1);
$q->Tags->toArray();
debug($q->toArray(true));
...prints the results with tag relation.
But i want to do:
Doctrine::getTable('posts')->findAll()->toArray(true);
...and get all of relations of posts , instead I got an array of post row.
Any idea about how to make it work with the relations?
(notice i added toArray(true) for deep property.
thanks for any help
You could create named query for this table with all relations attached:
Doctrine::getTable('posts')->addNamedQuery('get.by.id.with.relations', 'DQL here...');
And then just use something like this:
Doctrine::getTable('posts')->find('get.by.id.with.relations', array(123));
I beleive you need to do a Join with the query. Otherwise it doesnt hydrate the realated data.
$q = Doctrine_Query::create()
->from('Post p')
->leftJoin('p.RelatedModel1 rm1')
->leftJoin('p.RelatedModel2 rm2');
$q->findAll()->toArray(true);
$q = Doctrine_Query::create()
->from('Post p')
->leftJoin('p.RelatedModel1 rm1')
->leftJoin('p.RelatedModel2 rm2');
$q->findAll()->toArray(true);
Can i Add ->limit()->offset()
to the query ?
I guss that if i first create the query then findAll will act the same as execute right ?

Categories