symfony querybuilder for search by relation in collection - symfony

I have Entity Application with relation to Applicant
/**
* #ORM\ManyToOne(targetEntity=Applicant::class, inversedBy="applications")
* #ORM\JoinColumn(nullable=false)
*/
private $applicant;
now I try create QueryBuilder for search application by Applicant name in ApplicantRepository i have
public function searchByName($searchString)
{
return $this->createQueryBuilder('a')
->andWhere('a.name LIKE :phrase')->setParameter('phrase', '%'.$searchString.'%')
->getQuery()
->getResult();
}
in controller I have
$applicants = $applicantRepository->searchByName($searchString);
Now I want search Application with applicant name in this applicants collection. May I use QueryBuilder fot that?
I am trying something like this
public function getApprovedSearchByApplicants($applicants)
{
return $this->createQueryBuilder('a')
->andWhere('a.applicant IN (:applicants)')
->setParameter('applicants', $applicants)
->getQuery()
->getResult();
}

so, looking to your configuration, your Application::$applicant === Applicant::$name, just because Application::$applicant property has Applicant::$id value, by default. You can check the documentation.
So, this way, you need to make smth like this:
/**
* #ORM\ManyToOne(targetEntity=Applicant::class, inversedBy="applications")
* #ORM\JoinColumn(name="applicant_name", referencedColumnName="name", nullable=false)
*/
private $applicant;
It should work.
UPDATE after question update and discussions:
So, the problem was in the testing data in the database. Bad question.

I did not test it, but something like the following code should do the trick. It is almost the same solution as goulashsoup proposed, but without typing raw DQL.
/**
* #param array|Applicant[] $applicants
*
* #return array|Application[]
*/
public function findByApplicants(array $applicants): array
{
$qb = $this->createQueryBuilder('a')
return $qb->innerJoin('a.applicant', 'at')
->where(
$qb->expr()->in('at.id', ':applicants')
)
->setParameter('applicants', $applicants)
->getQuery()
->getResult();
}
I don't think you need to name the function wtih "ApprovedSearch" since the method is only aware of a list of Applicant for whom you want the list of Application.

Search by search string:
$entityManager
->createQuery('
SELECT ct
FROM App\Entity\Application ct
JOIN ct.applicant nt
WHERE nt.name LIKE :phrase
')
->setParameters(['phrase' => "%$searchString%"])
->getResult();
Search by applicants:
$entityManager
->createQuery('
SELECT ct
FROM App\Entity\Application ct
JOIN ct.applicant nt
WHERE nt IN (:nts)
')
->setParameters(['nts' => $applicants])
->getResult();

Related

Doctrine OneToOne always loaded on querybuilder

I have Partner entity with two relation:
/**
* #var PartnerSettings
* #ORM\OneToOne(targetEntity="PartnerSettings", mappedBy="partner", cascade={"persist", "remove"}, fetch="LAZY")
*/
private $settings;
/**
* #var PartnerRating
* #ORM\OneToOne(targetEntity="PartnerRating", mappedBy="partner", cascade={"persist", "remove"}, fetch="LAZY")
*/
private $rating;
...getRepository(Partner::class)->findAll() work correctly, one query was made,but when I create queryBuilder:
return $this->createQueryBuilder('p')
->getQuery()
->getResult();
doctrine make 31 queries(i have 10 partners)... in debug toolbar i saw select queries to settings and rating for every partner. I don't want it in this case.
Additionally, in every querybuilder where I used join to partners, setting and rating are selected too.
answer
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
did the job
This behaviour is known as the N + 1 selects problem. To reduce database queries, you might want to consider the following approach.
First, retrieve all partners:
$partners = $em->createQueryBuilder()
->select("p")
->from("Parent", "p")
->where(/*...*/)
->setParameter(/*...*/)
->indexBy("p.id")
->getQuery()->getResult();
Now load all children at once, in two queries:
$settings = $em->createQueryBuilder()
->select("s")
->from("PartnerSetting", "s")
->where("IDENTITY(s.partner) IN (?1)")
->setParameter(1, array_keys($partners))
->getQuery()->getResult();
$ratings = $em->createQueryBuilder()
->select("r")
->from("PartnerRating", "r")
->where("IDENTITY(r.partner) IN (?1)")
->setParameter(1, array_keys($partners))
->getQuery()->getResult();
Doctrine will now have all of the retrieved entities are stored in memory. So when, for example, you do a $parnter->getRatings(), you don’t trigger a new DB query, instead the entity is filled from memory.
just define the table AND the relation in the select
$qb->select('g', 'gi');
and now it works
Just mapped relation: #ORM\OneToOne(targetEntity="ENTITY", mappedBy="MAPPEDBY", fetch="EAGER")

Doctrine 2: Cache in One-to-Many associations

I'm trying to use doctrine cache from Common package, but I can't get it working with one-to-many, many-to-one accosiations. I'll explain later what I want to do.
My configuration:
'configuration' => array(
'orm_default' => array(
'metadata_cache' => 'filesystem',
'query_cache' => 'filesystem',
'result_cache' => 'filesystem',
'hydration_cache' => 'filesystem',
)
),
My entity
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*
* #var string
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100, nullable=false)
*/
protected $name;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Category", inversedBy="childrenId", fetch="EAGER")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
protected $parentId;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="parentId", fetch="EAGER")
*/
protected $childrenId;
}
My DQL
$result = $this->em->createQueryBuilder()->select('c')
->from('App\Entity\Category', 'c')
->where('c.parentId IS NULL')
->orderBy('c.priority', 'ASC')
->getQuery()
->setFetchMode("App\Entity\Category", "parentId", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
->useResultCache(true, 900, 'categories')
->getResult();
I have 28 categories, 15 of them have parentId.
Above query executes 29 SQL queries, but Doctrine store in cache only 1, So when I run again this query, it executes 28 queries.
Any idea what am I doing wrong? missing some cache configuration? missing some methods in DQL? I would like to cache all queries not only one- main query.
Edit
I would like to use query result in loop, like this:
foreach($result as $row)
{
$categories[]['attr'] = $row->getAttribute()->getName();
$categories[]['value'] = $row->getAttribute()->getValue();
}
but this way cache won't work, so currently I'm using:
foreach($result as $row)
{
$attributes = $this->em->createQueryBuilder()->select('c, a.name, a.value')
->from('App\Entity\Category', 'c')
->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
->where('c.id = :catId')
->setParameter('catId', $row['id'])
->getQuery()
->useResultCache(true, 900, $categoryName.'-attributes')
->getArrayResult();
}
But I would rather work on objects then on arrays, but I can't cuz if I use object and it has association then this association will not be cached. So ideally would be some way to cache object + ALL his associations.
Association fetch-modes
The query you present only fetches "parent" Category entities, which get hydrated with an uninitialized collection for the children. When accessing that collection (by iterating over those children for example), Doctrine will load the collection, thus perform another query. It will do that for all parent categories hydrated by the first query.
Setting fetch-mode to EAGER only changes the moment these queries are done. Doctrine will do them right after hydrating the parent categories, it won't wait until you access the collection (like with fetch-mode LAZY). But it will still do those queries.
Fetch-join query
The simplest way to tell Doctrine to query and hydrate the categories with their children is to do a "fetch join" query:
$queryBuilder = $this->em->createQueryBuilder();
$queryBuilder
->select('p', 'c') // [p]arent, [c]hild
->from('App\Entity\Category', 'p')
->leftJoin('p.children', 'c')
->where('p.parent IS NULL')
->orderBy('p.priority', 'ASC');
$query = $queryBuilder->getQuery();
$query
->useResultCache(true, 900, 'categories')
$result = $query->getResult();
Note the select() and leftJoin() calls here.
You also don't need to alter the fetch-mode of the association (by calling setFetchMode()), because the query itself will tell Doctrine to do what you want.
The result of this is that Doctrine will perform 1 query if it isn't cached yet (or the cache is stale), or 0 queries if it is cached (and still fresh).
Assumptions
The property $parentId (in Category) is renamed to $parent. This property will contain the parent Category entity, or null, but never an id.
The property $childrenId is renamed to $children. This property will contain a collection of Category entities (which might be empty), but never a collection (or array) of ids, and certainly never a single id.
The query I suggest above takes these renames into account.
I'm completely ignoring the fact that right after your "edit" a new Attribute entity has sprung into existence. It isn't relevant to your question or this answer IMHO.
More levels
It looks/sounds like your Categories only use 2 levels (parents and children). When you introduce more levels (grandchildren, etc), reading this model can become very inefficient very quickly.
When going for 3 or more levels, you might want to look into the Nested Set model. It's heavier on the writes, but highly optimized for reads.
The DoctrineExtensions library has support for this, and there's also a Symfony Bundle.
According to the docs: http://doctrine-orm.readthedocs.io/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql I think that setFetchMode - EAGER will not work for you as this will produce extra queries that will not be cached.
Why don't you load your categories with attributes?
$result = $this->em->createQueryBuilder()
->select('c, a.name, a.value')
->from('App\Entity\Category', 'c')
->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
->where('c.parentId IS NULL')
->orderBy('c.priority', 'ASC')
->getQuery()
->useResultCache(true, 900, 'categories')
->getResult();
And it should work as expected.

Doctrine ORM : Calculated related entity count in one shot

I've a User entity and a Product entity.
class User{
/*
* #ORM\OneToMany(targetEntity="Product", mappedBy="User")
*/
private $Products;
}
class Product{
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="Products")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $User;
}
Now I'm trying to display a html table of users, but I want to show each user's product count too.
By using following code I'm able to obtain the users objects.
$qb = $this->_em->createQueryBuilder();
$qb->select('usr')
->from('User', 'usr');
$query = $qb->getQuery();
But I don't know how to get the products count in one shot. Any help?
First, you should really create a repository class for your entities if you want to create custom queries. Then you can simply run that query by injecting the entity repository as a service wherever you need it and then running the query method.
Second, you need to return a result from a doctrine query to retrieve anything from the database. If you want to determine the count of the objects returned, simply do this:
$qb = $this->_em->createQueryBuilder();
$qb->select('usr')
->from('User', 'usr');
$query = $qb->getQuery();
$count = count($query->getResult());
A doctrine query will return an array of objects matching your query. If you just want to return a count of the matching records, try something like this:
$qb = $this->_em->createQueryBuilder();
$qb->select('count(id)')
->from('User', 'usr');
$query = $qb->getQuery();
$count = $query->getSingleScalarResult();
Or to just get a count of Product objects for that user, from within the User repository class:
$qb = $this->_em->createQueryBuilder('usr');
$qb->select('count(p.id)')
->from('usr.Products', 'p');
return $qb->getQuery()->getSingleScalarResult();

How to execute two repository functions in one Action

I have a problem. I want to execute this
$events1 = $em->getRepository('AfishaBundle:Event')->getEvents($this->getCurrentRegion(),'2012-11-12');
//$event_repo->clear();`
$events2 = $em->getRepository('AfishaBundle:Event')->getEvents($this->getCurrentRegion(),'2012-11-13');
foreach($events2 as $event){
die(var_dump($event->getSeanses()->toArray()));
}
But result return with first date = "2012-11-12"
when I not use $event_repo->clear();
That's ok but I have a problem with Twig rendering.
Can I execute this without clear() method ? If I can't, please comment.
For sake of completeness, this is getEvents() code:
public function getEvents($region, $date){
$qb = $this->_em->createQueryBuilder();
$qb
->select('e,s')
->from('AfishaBundle:Event', 'e')
->leftJoin('e.seanses', 's')
->andWhere('s.date = :date')
->andWhere('s.region = :region')
->setParameter('region', $region)
->setParameter('date',$date)
;
return $qb->getQuery()->getResult();
}
This is the relation between Event and EventSeans:
/**
* #ORM\OneToMany(
* targetEntity="EventSeans",
* cascade={"persist", "remove", "merge"},
* mappedBy="event"
* )
* #ORM\OrderBy({"time"= "ASC"})
*/
protected $seanses;
Finally, this is date field in EventSeans entity
/**
* #ORM\Column(name="date", type="date")
*/
protected $date;
Please, consider that it's really hard to understand what you are asking for! I understood that when you pass two different dates to the getEvents() function you always get the same result.
It seems that the code you currently posted is ok, but you are going to do is not to select the Event entities with the getEvents() function.
If you ready the query from you QueryBuilder, you are selecting the EventSeans entities such that the $region and $date are the one provided by you! But you are nor selecting the Event entities nor involving the relationship between Event and EventSeans entities.
Maybe with more details and a better explanation of your problem one could helps.
EDIT
I think you should not select both Event and EventSeanses, since you can get all the EventSeanses objects from the Event entity once you retrieve the Event from repo. But I suggest to select all the EventSeanses such that the criteria are satisfied, and then to get the associated Event object directly from the EventSeanses object. Clean and easy.
public function getEventSeanses($region, $date){
$qb = $this->_em->createQueryBuilder();
$qb
->select('s')
->from('AfishaBundle:EventSeanses', 's')
->andWhere("s.date = ':date'")
->andWhere('s.region = :region')
->setParameter('region', $region)
->setParameter('date',$date)
;
return $qb->getQuery()->getResult();
}
Notice that in SQL you should put strings (like a date) like follows 'date' and not simply date.
Then, in your controller you should do as follows:
$events = array();
$region=...
$date=....//year-month-day
$eventseansesList = $em->getRepository('AfishaBundle:EventSeanses')->getEventSeanses($region,$date);
foreach($eventseansesList as $es){
$events[] $es->getEvent(); // this function retrieve the Event associated to the EventSeanses
}
PS: You order $seanses by "time" in Event and not by "date", That's strange!

Count Rows in Doctrine QueryBuilder

I'm using Doctrine's QueryBuilder to build a query, and I want to get the total count of results from the query.
$repository = $em->getRepository('FooBundle:Foo');
$qb = $repository->createQueryBuilder('n')
->where('n.bar = :bar')
->setParameter('bar', $bar);
$query = $qb->getQuery();
//this doesn't work
$totalrows = $query->getResult()->count();
I just want to run a count on this query to get the total rows, but not return the actual results. (After this count query, I'm going to further modify the query with maxResults for pagination.)
Something like:
$qb = $entityManager->createQueryBuilder();
$qb->select('count(account.id)');
$qb->from('ZaysoCoreBundle:Account','account');
$count = $qb->getQuery()->getSingleScalarResult();
Some folks feel that expressions are somehow better than just using straight DQL. One even went so far as to edit a four year old answer. I rolled his edit back. Go figure.
Here is another way to format the query:
return $repository->createQueryBuilder('u')
->select('count(u.id)')
->getQuery()
->getSingleScalarResult();
It's better to move all logic of working with database to repositores.
So in controller you write
/* you can also inject "FooRepository $repository" using autowire */
$repository = $this->getDoctrine()->getRepository(Foo::class);
$count = $repository->count();
And in Repository/FooRepository.php
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->getQuery()
->getSingleScalarResult();
}
It's better to move $qb = ... to separate row in case you want to make complex expressions like
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->where($qb->expr()->isNotNull('t.fieldName'))
->andWhere($qb->expr()->orX(
$qb->expr()->in('t.fieldName2', 0),
$qb->expr()->isNull('t.fieldName2')
))
->getQuery()
->getSingleScalarResult();
}
Also think about caching your query result - http://symfony.com/doc/current/reference/configuration/doctrine.html#caching-drivers
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->getQuery()
->useQueryCache(true)
->useResultCache(true, 3600)
->getSingleScalarResult();
}
In some simple cases using EXTRA_LAZY entity relations is good
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html
If you need to count a more complex query, with groupBy, having etc... You can borrow from Doctrine\ORM\Tools\Pagination\Paginator:
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
$totalRows = count($paginator);
Since Doctrine 2.6 it is possible to use count() method directly from EntityRepository. For details see the link.
https://github.com/doctrine/doctrine2/blob/77e3e5c96c1beec7b28443c5b59145eeadbc0baf/lib/Doctrine/ORM/EntityRepository.php#L161
Example working with grouping, union and stuff.
Problem:
$qb = $em->createQueryBuilder()
->select('m.id', 'rm.id')
->from('Model', 'm')
->join('m.relatedModels', 'rm')
->groupBy('m.id');
For this to work possible solution is to use custom hydrator and this weird thing
called 'CUSTOM OUTPUT WALKER HINT':
class CountHydrator extends AbstractHydrator
{
const NAME = 'count_hydrator';
const FIELD = 'count';
/**
* {#inheritDoc}
*/
protected function hydrateAllData()
{
return (int)$this->_stmt->fetchColumn(0);
}
}
class CountSqlWalker extends SqlWalker
{
/**
* {#inheritDoc}
*/
public function walkSelectStatement(AST\SelectStatement $AST)
{
return sprintf("SELECT COUNT(*) AS %s FROM (%s) AS t", CountHydrator::FIELD, parent::walkSelectStatement($AST));
}
}
$doctrineConfig->addCustomHydrationMode(CountHydrator::NAME, CountHydrator::class);
// $qb from example above
$countQuery = clone $qb->getQuery();
// Doctrine bug ? Doesn't make a deep copy... (as of "doctrine/orm": "2.4.6")
$countQuery->setParameters($this->getQuery()->getParameters());
// set custom 'hint' stuff
$countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountSqlWalker::class);
$count = $countQuery->getResult(CountHydrator::NAME);
For people who are using only Doctrine DBAL and not the Doctrine ORM, they will not be able to access the getQuery() method because it doesn't exists. They need to do something like the following.
$qb = new QueryBuilder($conn);
$count = $qb->select("count(id)")->from($tableName)->execute()->fetchColumn(0);
To count items after some number of items (offset), $qb->setFirstResults() cannot be applied in this case, as it works not as a condition of query, but as an offset of query result for a range of items selected (i. e. setFirstResult cannot be used togather with COUNT at all). So to count items, which are left I simply did the following:
//in repository class:
$count = $qb->select('count(p.id)')
->from('Products', 'p')
->getQuery()
->getSingleScalarResult();
return $count;
//in controller class:
$count = $this->em->getRepository('RepositoryBundle')->...
return $count-$offset;
Anybody knows more clean way to do it?
Adding the following method to your repository should allow you to call $repo->getCourseCount() from your Controller.
/**
* #return array
*/
public function getCourseCount()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('count(course.id)')
->from('CRMPicco\Component\Course\Model\Course', 'course')
;
$query = $qb->getQuery();
return $query->getSingleScalarResult();
}
You can also get the number of data by using the count function.
$query = $this->dm->createQueryBuilder('AppBundle:Items')
->field('isDeleted')->equals(false)
->getQuery()->count();

Resources