I am currently trying to get data from a repository to show it in a Bootgrid, but I always exceed the maximum execution time (120 seconds).
I use the following code:
In my Javascript:
$params.table.bootgrid({
ajax: true,
url: $params.authoritiesDataPath //the url is correct here, I verified it
})
In my controller:
/**
* Authorities Data
*
* #param Request $request
* #return JsonResponse
*/
public function dataAction(Request $request)
{
$this->denyAccessUnlessGranted(RoleVoterHelper::SECTION_COMPANY_VIEW);
try {
$data = $this->getDoctrine()->getRepository('BugTrackerModelBundle:Authority')->findByParameters(
$request->request->all()
);
} catch (\Exception $e) {
$data = [
'status' => 'error',
'error' => $e->getMessage(),
'rows' => [],
'current' => 1,
'rowCount' => 0,
'total' => 0,
];
}
return new JsonResponse($data);
}
in my repository:
public function findByParameters(array $parameters)
{
$queryBuilder = $this->createQueryBuilder('a')
->select('COUNT(a.id)');
if (!empty($parameters['searchPhrase'])) {
$queryBuilder->where('a.name LIKE :search')
->setParameter('search', '%'.$parameters['searchPhrase'].'%');
}
$parameters['rows'] = array();
$parameters['current'] = isset($parameters['current']) ? (int)$parameters['current'] : 0;
if ($parameters['total'] = (int)$queryBuilder->getQuery()->getSingleScalarResult()) {
$queryBuilder->select('a.id', 'a.name', 'COUNT(DISTINCT c.id) as companies',
'COUNT(DISTINCT u.id) as users', 'COUNT(DISTINCT dl.id) as deviceLists',
'COUNT(DISTINCT d.id) as devices', 'a.name as authority', 'a.enabled')
->leftJoin('BugTrackerModelBundle:Company', 'c', Join::WITH, 'c.authority = a.id')
->leftJoin('BugTrackerModelBundle:Device', 'd', Join::WITH, 'd.authority = a.id')
->leftJoin('BugTrackerModelBundle:Device\DeviceList', 'dl', Join::WITH, 'dl.authority = a.id')
->leftJoin('BugTrackerModelBundle:User', 'u', Join::WITH, 'u.company = c.id')
->groupBy('a.id');
if (!empty($parameters['sort'])) {
$order = reset($parameters['sort']) ?: 'ASC';
switch (key($parameters['sort'])) {
case 'name':
$queryBuilder->orderBy('c.name', $order);
break;
case 'users':
$queryBuilder->orderBy('users', $order);
break;
case 'company':
$queryBuilder->orderBy('companies', $order);
break;
case 'enabled':
$queryBuilder->orderBy('a.enabled', $order);
break;
default:
$queryBuilder->orderBy('c.id', $order);
}
}
if (isset($parameters['rowCount']) && $parameters['rowCount'] > 0) {
$queryBuilder->setFirstResult(($parameters['current'] - 1) * $parameters['rowCount'])
->setMaxResults($parameters['rowCount']);
}
$parameters['rows'] = $queryBuilder->getQuery()->getArrayResult();
}
return $parameters;
}
I tried returning arrays pretty much everywhere to find where the loop was (I'm guessing that it's a loop since my code is usually very fast), and it seems to come from the following line
$parameters['rows'] = $queryBuilder->getQuery()->getArrayResult();
I tried returning $queryBuilder->getQuery() and it took a few seconds, so the issue is with getArrayResult
The query it returned when I printed the sql:
SELECT a0_.id AS id_0, a0_.name AS name_1, COUNT(DISTINCT c1_.id) AS sclr_2, COUNT(DISTINCT u2_.id) AS sclr_3, COUNT(DISTINCT d3_.id) AS sclr_4, COUNT(DISTINCT d4_.id) AS sclr_5, a0_.name AS name_6, a0_.enabled AS enabled_7 FROM authority a0_ LEFT JOIN client_company c1_ ON (c1_.authority_id = a0_.id) LEFT JOIN device d4_ ON (d4_.authority_id = a0_.id) LEFT JOIN device_list d3_ ON (d3_.authority_id = a0_.id) LEFT JOIN user u2_ ON (u2_.company_id = c1_.id) GROUP BY a0_.id LIMIT 5 OFFSET 0
Here is the "explain" when I ran the query in PhpMyAdmin:
Explain
I really don't understand why it takes so much time, I never had issues with getting data from my database and there is no loop in all that code that could cause it. Is there any way for me to test other things to understand why it takes so much time and change it?
have you indexed the correct columns? this should reduce the querying time, additionally, it could be that the results set is so complex it's taking a while to hydrate the array which is causing the time duration you're seeing.
Looking at the query produced, you have 4 joins in it and it hydrates 5 entities BugTrackerModelBundle:Authority, BugTrackerModelBundle:Company, BugTrackerModelBundle:Device, BugTrackerModelBundle:Device\DeviceList and BugTrackerModelBundle:User.
That `s a lot of joins,as "the process of hydration becomes extremely expensive when more than 2 LEFT JOIN operations clauses". See Reference. In short, the result set is large and doctrine takes too long to map it to Entities.
My assumption is that the ORM is taking too long to normalize the result set returned by the query.
My advice is to split the query to 2 with maximum 2 joins each:
$queryBuilder = $this->createQueryBuilder('a')
->select('a.id', 'a.name',
'COUNT(DISTINCT c.id) as companies',
'COUNT(DISTINCT u.id) as users',
'a.name as authority', 'a.enabled')
->leftJoin('BugTrackerModelBundle:Company', 'c', Join::WITH, 'c.authority = a.id')
->leftJoin('BugTrackerModelBundle:User', 'u', Join::WITH, 'u.company = c.id')
->groupBy('a.id');
$queryBuilder = $this->createQueryBuilder('a')
->select('a.id', 'a.name',
'COUNT(DISTINCT dl.id) as deviceLists',
'COUNT(DISTINCT d.id) as devices', 'a.name as authority', 'a.enabled')
->leftJoin('BugTrackerModelBundle:Device', 'd', Join::WITH, 'd.authority = a.id')
->leftJoin('BugTrackerModelBundle:Device\DeviceList', 'dl', Join::WITH, 'dl.authority = a.id')
->groupBy('a.id');
Hope this helps.
Related
I am trying to return my visits but I need a distinct and not a count. I tried these repository methods but only count works.
Working:
public function numberOfVisitsBetweenDates($dateMin, $dateMax)
{
$builder = $this->createQueryBuilder('visit');
$builder->select('count(DISTINCT visit.user)');
$builder->where('visit.date BETWEEN :dateMin AND :dateMax');
$builder->setParameter('dateMin', $dateMin);
$builder->setParameter('dateMax', $dateMax);
$query = $builder->getQuery();
// dd($query);
return $query->getResult();
}
My dump dql -_dql: "SELECT count(DISTINCT visit.user) FROM App\Entity\Visit visit WHERE visit.date BETWEEN :dateMin AND :dateMax"
Not working:
public function numberOfVisitsBetweenDates($dateMin, $dateMax)
{
$builder = $this->createQueryBuilder('visit');
$builder->select('visit.user');
$builder->where('visit.date BETWEEN :dateMin AND :dateMax');
$builder->setParameter('dateMin', $dateMin);
$builder->setParameter('dateMax', $dateMax);
$builder->distinct();
$query = $builder->getQuery();
// dd($query);
return $query->getResult();
}
My dump dql: -_dql: "SELECT DISTINCT visit.user FROM App\Entity\Visit visit WHERE visit.date BETWEEN :dateMin AND :dateMax"
My error:
QueryException
QueryException
HTTP 500 Internal Server Error
[Semantical Error] line 0, col 22 near 'user FROM App\Entity\Visit': Error: Invalid PathExpression. Must be a StateFieldPathExpression.
Both codes are almost the same, i can't find the solution
You should try something like this:
public function numberOfVisitsBetweenDates($dateMin, $dateMax)
{
$builder = $this->createQueryBuilder('visit');
$builder->where('visit.date BETWEEN :dateMin AND :dateMax');
$builder->setParameter('dateMin', $dateMin);
$builder->setParameter('dateMax', $dateMax);
$builder->groupBy('visit.user');
$query = $builder->getQuery();
return $query->getResult();
}
I have a query that pulls in some jobs if they have an end date and are not completed which produces the desired result:
real code
$this->createQueryBuilder('j')
->where('j.completed = :false OR j.completed is NULL')
->andWhere('j.has_due_date = :true')
->andWhere('j.has_due_date = :true')
->setParameters( array(
'false' => FALSE,
'true' => TRUE
) )
->orderBy('j.due_date', 'asc')
->getQuery();
result
SELECT j FROM App\Entity\Jobs j
WHERE (
j.completed = :false
OR j.completed is NULL
)
AND j.has_due_date = :true
ORDER BY j.due_date ASC
I'm wanting to follow the DQL best practice for this and feel like there is another way or writing this and only having a single WHERE clause per call on a ->where variant (andWhere(), orWhere())
In my mind it's something like this but I can't find anything out there to confirm it:
pseudo code
$query = $this->createQueryBuilder('j')
->where( $requiredClass->
->where('j.completed = :false')
->orWhere('j.completed is NULL')
->setParameter('false', FALSE)
)
->andWhere('j.has_due_date = :true')
->setParameter('true', TRUE)
->orderBy('j.due_date', 'asc')
->getQuery();
Main questions are:
Is there a way to do this the second way?
am I overcomplicating things unnecessarily?
if there is a second way to do this as mentioned, the first statement works so why should it matter?
Using the query builder should be easier and more understandable
public function someFunction()
{
$qb = $this->createQueryBuilder('j');
$orX = $qb->expr()->orX();
$andX = $qb->expr()->andX();
$orX->add($qb->expr()->eq('j.completed', $qb->expr()->literal(false)));
$orX->add($qb->expr()->isNull('j.completed'));
$andX->add($qb->expr()->eq('j.has_due_date', $qb->expr()->literal(true)));
$qb->andWhere($orX, $andX)
->orderBy('j.due_date', 'asc')
->getQuery()
->getResult();
}
As stated in the official documentation for QueryBuilder best practice - the right way is to use the helper method QueryBuilder::expr() and also the Doctrine\ORM\Query\Expr helper class. High level API methods
i.e.:
// create QueryBuilder
$qb = $this->createQueryBuilder('j');
// build conditions
$qb->where(
$qb->expr()->orX(
$qb->expr()->eq('j.completed', ':false'),
$qb->expr()->isNull('j.completed')
)
)
->andWhere(
$qb->expr()->eq('j.has_due_date', ':true')
)
->andWhere(
$qb->expr()->eq('j.has_due_date', ':true')
)
->setParameters(array(
'false' => FALSE,
'true' => TRUE
))
->orderBy('j.due_date', 'asc');
// get query
$query = $qb->getQuery();
// execute and get result
$result = $query->getResult();
I am very new to Drupal and attempting to build a module that will allow admins to tag nodes with keywords to boost nodes to the top of the search results.
I have a separate DB table for the keywords and respective node IDs. This table is UNIONed with the search_index table via hook_query_alter...
function mos_search_result_forcer_query_alter(QueryAlterableInterface &$query) {
if (get_class($query) !== 'PagerDefault') { //<< check this because this function will mod all queries elsewise
return;
}
// create unioned search index result set...
$index = db_select('search_index', 's');
$index->addField('s', 'sid');
$index->addField('s', 'word');
$index->addField('s', 'score');
$index->addField('s', 'type');
$msrfi = db_select('mos_search_result_forcer', 'm');
$msrfi->addField('m', 'nid', 'sid');
$msrfi->addField('m', 'keyword', 'word');
$msrfi->addExpression('(SELECT MAX(score) + m.id / (SELECT MAX(id) FROM {mos_search_result_forcer}) FROM {search_index})', 'score');
$msrfi->addExpression(':type', 'type', array(':type' => 'node'));
$index->union($msrfi);
$tables =& $query->getTables();
$tables['i']['table'] = $index;
return $query;
}
Drupal then generates the almost correct query...
SELECT
i.type AS type, i.sid AS sid, SUM(CAST('10' AS DECIMAL) * COALESCE(( (12.048628015788 * i.score * t.count)), 0) / CAST('10' AS DECIMAL)) AS calculated_score
FROM (
SELECT
s.sid AS sid, s.word AS word, s.score AS score, s.type AS type
FROM
search_index s
UNION SELECT
m.nid AS sid, m.keyword AS word, (
SELECT
MAX(score) + m.id / (SELECT MAX(id) FROM mos_search_result_forcer)
FROM
search_index
) AS score, 'node' AS type
FROM
mos_search_result_forcer m
) i
INNER JOIN node n ON n.nid = i.sid
INNER JOIN search_total t ON i.word = t.word
INNER JOIN search_dataset d ON i.sid = d.sid AND i.type = d.type
WHERE (n.status = '1')
AND( (i.word = 'turtles') )
AND (i.type = 'node')
/* this is the problem line... */
AND( (d.data LIKE '% turtles %' ESCAPE '\\') )
/* ...end problem line */
GROUP BY i.type, i.sid
HAVING (COUNT(*) >= '1')
ORDER BY calculated_score DESC
LIMIT 10 OFFSET 0
...I need that "problem line" to read...
AND( (d.data LIKE '% turtles %' ESCAPE '\\') OR (d.sid IN (SELECT nid FROM mos_search_result_forcer)) )
...what hook can I use to add that OR condition?
I don't want to hack Drupal's core.
I don't want to change the union/subqueries (not my decision).
I will optimize queries later - functionality is more important.
Thanks, smart people!
The basic principle is to grab the conditions array, loop through and find the index for the problem condition, remove it, then re-add an identical condition along with the new one as part of a db_or()
This is un-tested and most likely won't work verbatim, but it should give you a starting point:
$conditions =& $query->conditions();
$index = FALSE;
$removed_condition = NULL;
for ($i = 0, $l < count($conditions); $i < $l; $i++) {
if ($conditions[$i]['field'] == 'd.data' && $conditions[$i]['operator'] == 'LIKE') {
$index = $i;
$removed_condition = $condition;
break;
}
}
if ($index !== FALSE) {
unset($conditions[$index]);
$sub_query = db_select('mos_search_result_forcer')->fields('mos_search_result_forcer', array('nid'));
$new_condition = db_or()
->condition('d.data', $removed_condition['value'], 'LIKE')
->condition('d.sid', $sub_query, 'IN');
$query->condition($new_condition);
}
Thanks to some helpful suggestions from #Clive with hook_module_implements_alter, and a lot of trial and error, I have finally solved this issue.
Here is the final code...
function mos_search_result_forcer_module_implements_alter(&$imps, $hook) {
if ($hook !== 'query_alter' || !array_key_exists('mos_search_result_forcer', $imps)) {
return;
}
$imp = $imps['mos_search_result_forcer'];
unset($imps['mos_search_result_forcer']);
$imps['mos_search_result_forcer'] = $imp;
}
function mos_search_result_forcer_query_alter(QueryAlterableInterface &$query) {
if (get_class($query) !== 'PagerDefault') { //<< check this because this function will mod all queries elsewise
return;
}
// create unioned search index result set...
$index = db_select('search_index', 's');
$index->addField('s', 'sid');
$index->addField('s', 'word');
$index->addField('s', 'score');
$index->addField('s', 'type');
$msrfi = db_select('mos_search_result_forcer', 'm');
$msrfi->addField('m', 'nid', 'sid');
$msrfi->addField('m', 'keyword', 'word');
$msrfi->addExpression('(SELECT MAX(score) + m.id / (SELECT MAX(id) FROM {mos_search_result_forcer}) FROM {search_index})', 'score');
$msrfi->addExpression(':type', 'type', array(':type' => 'node'));
$index->union($msrfi);
$tables =& $query->getTables();
$tables['i']['table'] = $index;
// needs special "or" condition to keep from filtering out forced resutls...
class MSRFPagerDefaultHelper extends PagerDefault { //<< override to gain access to protected props
static function msrfHelp(PagerDefault &$pagerDefault) {
$searchQuery =& $pagerDefault->query;
MSRFSearchQueryHelper::msrfHelp($searchQuery);
}
}
class MSRFSearchQueryHelper extends SearchQuery { //<< override to gain access to protected props
static function msrfHelp(SearchQuery &$searchQuery) {
$conditions =& $searchQuery->conditions;
$condition = db_or()->condition($conditions)->condition('d.sid', db_select('mos_search_result_forcer')->fields('mos_search_result_forcer', array('nid')), 'IN');
$searchQuery->conditions = $condition;
}
}
MSRFPagerDefaultHelper::msrfHelp($query);
return $query; //<< i don't think this is needed as var is reffed - just for good measure, i guess
}
Context: Given the fact that the following query :
$queryBuilder = $this->createQueryBuilder("cv")
->leftJoin('cv.user', 'u')
->where('cv.game = :game')
->setParameter('game', $game);
Will trigger 1+X distinct queries (one to get all the CV, then if u.user is used in the template, will trigger X other queries to fetch users).
If I want to optimize and to reduce those multiple unoptimized queries to 1 single query, i'll do so :
$queryBuilder = $this->createQueryBuilder("cv")
->select('cv, u')
->leftJoin('cv.user', 'u')
->where('cv.game = :game')
->setParameter('game', $game);
This Way, i'll be able to save X queries.
Now, my problem is in my repository, I have conditional joins and I want to chain the select aliases at different places in my code.
Like (simplified example) :
$queryBuilder = $this->createQueryBuilder("cv")
->select('cv, u')
->leftJoin('cv.user', 'u')
->where('cv.game = :game')
->setParameter('game', $game);
if ($myCondition === true) {
$queryBuilder->add('select', 'l');
$queryBuilder->join('cv.level', 'l');
}
But it seems that the add->('select') does not stack like an addWhere().
Are there any other solutions than using a custom solution like this :
$queryBuilder = $this->createQueryBuilder("cv")
->leftJoin('cv.user', 'u')
->where('cv.game = :game')
->setParameter('game', $game);
$aliases = array('cv', 'u');
if ($myCondition === true) {
$aliases[] = 'l';
$queryBuilder->add('select', 'l');
$queryBuilder->join('cv.level', 'l');
}
$queryBuilder->select(implode(',', $aliases);
Thanks.
// Replace
$queryBuilder->add('select', 'l');
// With
$queryBuilder->addSelect('l');
And a bit of unsolicited advice. I know how much "un optimized queries" bother most people, including myself. However, consider doing some bench marks on large data sets. It's surprising how fast lazy loading is. Very little difference even with thousands of queries.
I have this function,
public function getWall(){
$q = $this->createQueryBuilder('f');
$q->leftJoin("f.profilo", 'p');
$q->leftJoin("p.utente", 'u');
$q->where('(f.foto_eliminata IS NULL OR f.foto_eliminata != 1)');
$q->andWhere('p.fase_registrazione = :fase');
$q->andWhere('u.locked = :false');
$q->slice(0, 20);
$q->setParameter(':fase', 100);
$q->setParameter('false', false);
$q->orderBy('f.created_at', 'desc');
$dql = $q->getQuery();
$results = $dql->execute();
return $results;
}
but I get this error,
Call to undefined method Doctrine\ORM\QueryBuilder::slice()
Ok, so, u get this error, cause QueryBuilder has no such method. But Collection has. If u want to use slice, a possible variant is:
use Doctrine\Common\Collections;
public function getWall(){
$result = $this->createQueryBuilder('f')
->leftJoin("f.profilo", 'p')
->leftJoin("p.utente", 'u')
->where('(f.foto_eliminata IS NULL OR f.foto_eliminata != 1)')
->andWhere('p.fase_registrazione = :fase')
->andWhere('u.locked = :false')
->setParameter('fase', 100)
->setParameter('false', false)
->orderBy('f.created_at', 'desc')
->getQuery()
->getResult();
// $result typed as array
return new Collections\ArrayCollection($result))->slice(0,20); // convert array to collection, then slice
}
By the way, it is not a good idea to 'limit' result of the query in a such way.
U can use setMaxResults(20), and not to select all objects at all.
About lazy collections (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html): after selecting result objects, u can take some object from result collection: $r = $result[0] after that:
$portfilos = $r->getPortfolio(); // returns for example Collection with some objects;
// its Lazy, without SQL query!
$portfolios->slice(0, 20); // queries first 20 potfolios
To use slice is a rather good idea, if u have lots of objects in some relation.
p.s. sry, mb I didn't recognized your problem, but tried :)
EDITED
fixed errors in code.