Laravel: How to remove a specific 'where' clause from the Query Builder - php

I am fairly new to laravel and need to remove a 'where clause' from the Query Builder based on search options being selected or not.
My model contains the following already in a local scope:
$builder->where('users.active', '=', 'Yes');
and I need to override this in a specific search circumstance to either allow values of 'Yes' and 'No', or just remove the clause entirely. This must be done in a Trait and be suitable for multi uses/models.
I have actually got this to work by adding a new method in my SearchTrait that strips the query builder apart, removes the offending column and associated binding and returns the modified query builder.
$builder = static::removeWhere($builder, 'users.active');
This seems to work - but my question(s) are:
Is this a suitable solution, or is there something more elegant?
Are there any glaring issues with my solution?
Is there a better place for this / can I easily extend the 'builder'?
/**
* #param Builder $builder
* #param $whereColumn
* #return Builder
*/
public static function removeWhere(Builder $builder, $whereColumn)
{
$bindings = $builder->getQuery()->bindings['where'];
$wheres = $builder->getQuery()->wheres;
$whereKey = false;
foreach ($wheres as $key => $where) {
if ($where['column'] == $whereColumn) {
$whereKey = $key;
break;
}
}
if ($whereKey !== false) {
unset($bindings[$whereKey]);
unset($wheres[$whereKey]);
}
$builder->getQuery()->wheres = $wheres;
$builder->getQuery()->bindings['where'] = $bindings;
return $builder;
}

Related

Am I doing eager loading correctly? (Eloquent)

I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).

Is it possible to use result of an SQL function as a field in Doctrine?

Assume I have Product entities and Review entities attached to products. Is it possible to attach a fields to a Product entity based on some result returned by an SQL query? Like attaching a ReviewsCount field equal to COUNT(Reviews.ID) as ReviewsCount.
I know it is possible to do that in a function like
public function getReviewsCount() {
return count($this->Reviews);
}
But I want doing this with SQL to minimize number of database queries and increase performance, as normally I may not need to load hundreds of reviews, but still need to know there number. I think running SQL's COUNT would be much faster than going through 100 Products and calculating 100 Reviews for each. Moreover, that is just example, on practice I need more complex functions, that I think MySQL would process faster. Correct me if I'm wrong.
You can map a single column result to an entity field - look at native queries and ResultSetMapping to achieve this. As a simple example:
use Doctrine\ORM\Query\ResultSetMapping;
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$rsm = new ResultSetMapping;
$rsm->addEntityResult('AppBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
$results = $query->getResult();
Then in your Product entity you would have a $reviewsCount field and the count would be mapped to that. Note that this will only work if you have a column defined in the Doctrine metadata, like so:
/**
* #ORM\Column(type="integer")
*/
private $reviewsCount;
public function getReviewsCount()
{
return $this->reviewsCount;
}
This is what is suggested by the Aggregate Fields Doctrine documentation. The problem is here is that you are essentially making Doctrine think you have another column in your database called reviews_count, which is what you don't want. So, this will still work without physically adding that column, but if you ever run a doctrine:schema:update it's going to add that column in for you. Unfortunately Doctrine does not really allow virtual properties, so another solution would be to write your own custom hydrator, or perhaps subscribe to the loadClassMetadata event and manually add the mapping yourself after your particular entity (or entities) load.
Note that if you do something like COUNT(r.id) AS reviewsCount then you can no longer use COUNT(id) in your addFieldResult() function, and must instead use the alias reviewsCount for that second parameter.
You can also use the ResultSetMappingBuilder as a start into using the result set mapping.
My actual suggestion is to do this manually instead of going through all of that extra stuff. Essentially create a normal query that returns both your entity and scalar results into an array, then set the scalar result to a corresponding, unmapped field on your entity, and return the entity.
After detailed investigation I've found there are several ways to do something close to what I wanted including listed in other answers, but all of them have some minuses. Finally I've decided to use CustomHydrators. It seems that properties not managed with ORM cannot be mapped with ResultSetMapping as fields, but can be got as scalars and attached to an entity manually (as PHP allows to attach object properties on the fly). However, result that you get from doctrine remains in the cache. That means properties set in that way may be reset if you make some other query that would contain these entities too.
Another way to do that was adding these field directly to doctrine's metadata cache. I tried doing that in a CustomHydrator:
protected function getClassMetadata($className)
{
if ( ! isset($this->_metadataCache[$className])) {
$this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
if ($className === "SomeBundle\Entity\Product") {
$this->insertField($className, "ReviewsCount");
}
}
return $this->_metadataCache[$className];
}
protected function insertField($className, $fieldName) {
$this->_metadataCache[$className]->fieldMappings[$fieldName] = ["fieldName" => $fieldName, "type" => "text", "scale" => 0, "length" => null, "unique" => false, "nullable" => true, "precision" => 0];
$this->_metadataCache[$className]->reflFields[$fieldName] = new \ReflectionProperty($className, $fieldName);
return $this->_metadataCache[$className];
}
However, that method also had problems with entities' properties reset. So, my final solution was just to use stdClass to get the same structure, but not managed by doctrine:
namespace SomeBundle;
use PDO;
use Doctrine\ORM\Query\ResultSetMapping;
class CustomHydrator extends \Doctrine\ORM\Internal\Hydration\ObjectHydrator {
public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) {
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach($resultSetMapping->entityMappings as $root => $something) {
$rootIDField = $this->getIDFieldName($root, $resultSetMapping);
foreach($data as $row) {
$key = $this->findEntityByID($result, $row[$rootIDField]);
if ($key === null) {
$result[] = new \stdClass();
end($result);
$key = key($result);
}
foreach ($row as $column => $field)
if (isset($resultSetMapping->columnOwnerMap[$column]))
$this->attach($result[$key], $field, $this->getPath($root, $resultSetMapping, $column));
}
}
return $result;
}
private function getIDFieldName($entityAlias, ResultSetMapping $rsm) {
foreach ($rsm->fieldMappings as $key => $field)
if ($field === 'ID' && $rsm->columnOwnerMap[$key] === $entityAlias) return $key;
return null;
}
private function findEntityByID($array, $ID) {
foreach($array as $index => $entity)
if (isset($entity->ID) && $entity->ID === $ID) return $index;
return null;
}
private function getPath($root, ResultSetMapping $rsm, $column) {
$path = [$rsm->fieldMappings[$column]];
if ($rsm->columnOwnerMap[$column] !== $root)
array_splice($path, 0, 0, $this->getParent($root, $rsm, $rsm->columnOwnerMap[$column]));
return $path;
}
private function getParent($root, ResultSetMapping $rsm, $entityAlias) {
$path = [];
if (isset($rsm->parentAliasMap[$entityAlias])) {
$path[] = $rsm->relationMap[$entityAlias];
array_splice($path, 0, 0, $this->getParent($root, $rsm, array_search($rsm->parentAliasMap[$entityAlias], $rsm->relationMap)));
}
return $path;
}
private function attach($object, $field, $place) {
if (count($place) > 1) {
$prop = $place[0];
array_splice($place, 0, 1);
if (!isset($object->{$prop})) $object->{$prop} = new \stdClass();
$this->attach($object->{$prop}, $field, $place);
} else {
$prop = $place[0];
$object->{$prop} = $field;
}
}
}
With that class you can get any structure and attach any entities however you like:
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$em = $this->getDoctrine()->getManager();
$rsm = new ResultSetMapping();
$rsm->addEntityResult('SomeBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $em->createNativeQuery($sql, $rsm);
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundle\CustomHydrator');
$results = $query->getResult('CustomHydrator');
Hope that may help someone :)
Yes, it is possible, you need to use QueryBuilder to achieve that:
$result = $em->getRepository('AppBundle:Product')
->createQueryBuilder('p')
->select('p, count(r.id) as countResult')
->leftJoin('p.Review', 'r')
->groupBy('r.id')
->getQuery()
->getArrayResult();
and now you can do something like:
foreach ($result as $row) {
echo $row['countResult'];
echo $row['anyOtherProductField'];
}
If you're on Doctrine 2.1+, consider using EXTRA_LAZY associations:
They allow you to implement a method like yours in your entity, doing a straight count on the association instead of retrieving all the entities in it:
/**
* #ORM\OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;
public function getReviewsCount() {
return $this->Reviews->count();
}
The previous answers didn't help me, but I found a solution doing the following:
My use case was different so the code is a mock. But the key is to use addScalarResult and then cleanup the result while setting the aggregate on the entity.
use Doctrine\ORM\Query\ResultSetMappingBuilder;
// ...
$sql = "
SELECT p.*, COUNT(r.id) AS reviewCount
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
";
$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em, ResultSetMappingBuilder::COLUMN_RENAMING_CUSTOM);
$rsm->addRootEntityFromClassMetadata('App\Entity\Product', 'p');
$rsm->addScalarResult('reviewCount', 'reviewCount');
$query = $em->createNativeQuery($sql, $rsm);
$result = $query->getResult();
// APPEND the aggregated field to the Entities
$aggregatedResult = [];
foreach ($result as $resultItem) {
$product = $resultItem[0];
$product->setReviewCount( $resultItem["reviewCount"] );
array_push($aggregatedResult, $product);
}
return $aggregatedResult;

Laravel - Disable Updated At when updating

Get a problem with update using query builder on laravel 5. I've tried to disabled the updated_at but keep failing.
Here is my code:
$query = StockLog::where('stock_id', $id)->whereBetween('created_at', $from, $to])->update(['batch_id' => $max + 1]);
I've tried 2 ways:
first one at my model i set:
public function setUpdatedAtAttribute($value)
{
/*do nothing*/
}
Second one:
$stocklog = new StockLog;
$stocklog->timestamps = false;
$query = $stocklog::where('stock_id', $id)->whereBetween('created_at', [$from, $to])->update([
'batch_id' => $max + 1]);
both of them are failed. is there anyway to disabled the updated_at?
Thanks in advance
By default, Eloquent will maintain the created_at and updated_at columns on your database table automatically. Simply add these timestamp columns to your table and Eloquent will take care of the rest.
I don't really suggest removing them. But if you want use the following way.
add the following to your model:
public $timestamps = false;
This will disable the timestamps.
EDIT: it looks like you want to keep the created_at field, you can override the getUpdatedAtColumn in your model.
Use the following code:
public function getUpdatedAtColumn() {
return null;
}
In your model, add this method:
/**
* #param mixed $value
* #return $this
*/
public function setUpdatedAt($value)
{
return $this;
}
UPDATE: In Laravel 5.5:
Just try to use this in your model:
const CREATED_AT = null;
const UPDATED_AT = null;
The accepted answer didn't work for me, but led me in the right direction to this solution that did:
class Whatever extends Model {
//...
const UPDATED_AT=NULL;
//...
Laravel 5.3
In this case it's better to use Query Builder instead of Eloquent because Query Builder doesn't implicitely edits timestamps fields. The use of Query Builder will have the advantage of targetting only the concerned update operation without alterate all your model.
In one line you could do:
$query = \DB::table('stocklogs')->where('stock_id', $id)->whereBetween('created_at', [$from, $to])->update(['batch_id' => $max + 1]);
You can use following if you want make it off permanently.
Add following to your model...
public $timestamps = false;
And if you want to keep using created_at, then add following.
static::creating( function ($model) {
$model->setCreatedAt($model->freshTimestamp());
});
OR use following way...
/**
* Set the value of the "updated at" attribute.
*
* #param mixed $value
* #return void
*/
public function setUpdatedAt($value)
{
$this->{static::UPDATED_AT} = $value;
}
Before updating you need to add ->toBase()
For example
Model::query()->where([...])->toBase()->update([...]);
in your case it will be
StockLog::where('stock_id', $id)->whereBetween('created_at', $from, $to])->toBase()->update(['batch_id' => $max + 1]);

Question mark operator in query

In my laravel 5 app, I'm using PostgreSQL's jsonb data type and it has ? operator.
But I can't get it work in my model, because laravel uses question marks as bindings.
Specifically, in whereRaw() method:
$query->whereRaw("jsonb_column ? 'a_key'")
How can I use question mark in my queries?
you can consider using the function call instead of operator.
First you should find out which function ? operator uses via following query on your PostgresSQL database:
SELECT oprname, oprcode FROM pg_operator WHERE oprname = '?'
on my development database it's jsonb_exists function,
then you can update your query as:
$query->whereRaw("jsonb_exists(jsonb_column, 'a_key')")
I hope it helps, happy coding.
Basically you have 2 options:
Getting your hand dirty by extending the current way Laravel's Query Builder implement whereRaw, otherwise stated doing it the hard way.
Send feature request to the Laravel team (ie asking them to extend all Query Builder component dealing with more PostgresQL specifics) and cross your fingers they will address it if you are patient enough.
Here are my takes for the [1.] option:
The namespace "Illuminate\Database\Query" is good for you:
You need particularly to delve into the following Laravel 5.0 source codes of interest:
Builder.php
Grammar.php
PostgresGrammar.php
Code fragments of interest:
whereRaw in Builder.php (excerpt):
/**
* Add a raw where clause to the query.
*
* #param string $sql
* #param array $bindings
* #param string $boolean
* #return $this
*/
public function whereRaw($sql, array $bindings = array(), $boolean = 'and')
{
$type = 'raw';
$this->wheres[] = compact('type', 'sql', 'boolean');
$this->addBinding($bindings, 'where');
return $this;
}
compileWheres in Grammar.php (excerpt):
/**
* Compile the "where" portions of the query.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileWheres(Builder $query)
{
$sql = array();
if (is_null($query->wheres)) return '';
// Each type of where clauses has its own compiler function which is responsible
// for actually creating the where clauses SQL. This helps keep the code nice
// and maintainable since each clause has a very small method that it uses.
foreach ($query->wheres as $where)
{
$method = "where{$where['type']}";
$sql[] = $where['boolean'].' '.$this->$method($query, $where);
}
// If we actually have some where clauses, we will strip off the first boolean
// operator, which is added by the query builders for convenience so we can
// avoid checking for the first clauses in each of the compilers methods.
if (count($sql) > 0)
{
$sql = implode(' ', $sql);
return 'where '.$this->removeLeadingBoolean($sql);
}
return '';
}
$operators array in PostgresGrammar.php (excerpt):
/**
* All of the available clause operators.
*
* #var array
*/
protected $operators = array(
'=', '<', '>', '<=', '>=', '<>', '!=',
'like', 'not like', 'between', 'ilike',
'&', '|', '#', '<<', '>>',
);
notice that ? is not a valid operator ;-)
Specialized PostgreSQL protected methods in PostgresGrammar.php (excerpt):
/**
* Compile the additional where clauses for updates with joins.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileUpdateWheres(Builder $query)
{
$baseWhere = $this->compileWheres($query);
if ( ! isset($query->joins)) return $baseWhere;
// Once we compile the join constraints, we will either use them as the where
// clause or append them to the existing base where clauses. If we need to
// strip the leading boolean we will do so when using as the only where.
$joinWhere = $this->compileUpdateJoinWheres($query);
if (trim($baseWhere) == '')
{
return 'where '.$this->removeLeadingBoolean($joinWhere);
}
return $baseWhere.' '.$joinWhere;
}
/**
* Compile the "join" clauses for an update.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileUpdateJoinWheres(Builder $query)
{
$joinWheres = array();
// Here we will just loop through all of the join constraints and compile them
// all out then implode them. This should give us "where" like syntax after
// everything has been built and then we will join it to the real wheres.
foreach ($query->joins as $join)
{
foreach ($join->clauses as $clause)
{
$joinWheres[] = $this->compileJoinConstraint($clause);
}
}
return implode(' ', $joinWheres);
}
consider these as a sort of specialization of the compileWheres mentioned above, the remaining cases apart from the two (only 2!!!) specific ones are compiled using the parent class method (Illuminate\Database\Query\Grammars\Grammar).
Other recommended relevant resources
You may find valuable posts in this blog (SOFTonSOFA).
I particularly recommend:
Laravel Query Builder global scope – how to use custom Connection and Query Builder in Laravel 4, not in right version but shows how to extend the Query Builder.
Laravel 5 Eloquent Global Scope how-to, in Laravel 5.0 but orthogonal to your question yet instructive (right version, plumbing details).
Last but not the least, the Laravel documentation is the best place to grab more about its Architecture Foundations (Service Providers, Service Container, Facades and so on). Mastering it is handy to devise a robust extension of the framework.
Hopefully my input gives you enough hints for the possible extension point of the Laravel Query Builder offered and may it serve as a good starting point for you to write the PostgreSQL extension addressing the ?operator issue in whereRaw.
Please give back/share when done.
Try escaping it using a backslash, like this
SELECT * FROM table WHERE id = \?;

select random row for each category in array [duplicate]

Here is how I query my database for some words
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy('w.frequency', 'DESC')
->getQuery()
->setMaxResults(100);
I'm using mysql and I'd like to get random rows that match the criteria, I would use order by rand() in my query.
I found this similar question which basically suggests since ORDER BY RAND is not supported in doctrine, you can randomize the primary key instead. However, this can't be done in my case because I have a search criteria and a where clause so that not every primary key will satisfy that condition.
I also found a code snippet that suggests you use the OFFSET to randomize the rows like this:
$userCount = Doctrine::getTable('User')
->createQuery()
->select('count(*)')
->fetchOne(array(), Doctrine::HYDRATE_NONE);
$user = Doctrine::getTable('User')
->createQuery()
->limit(1)
->offset(rand(0, $userCount[0] - 1))
->fetchOne();
I'm a little confused as to whether this will help me work around the lack of support for order by random in my case or not. I was not able to add offset after setMaxResult.
Any idea how this can be accomplished?
The Doctrine team is not willing to implement this feature.
There are several solutions to your problem, each having its own drawbacks:
Add a custom numeric function: see this DQL RAND() function
(might be slow if you have lots of matching rows)
Use a native query
(I personally try to avoid this solution, which I found hard to maintain)
Issue a raw SQL query first to get some IDs randomly, then use the DQL WHERE x.id IN(?) to load the associated objects, by passing the array of IDs as a parameter.
This solution involves two separate queries, but might give better performance than the first solution (other raw SQL techniques than ORDER BY RAND() exist, I won't detail them here, you'll find some good resources on this website).
Follow these steps:
Define a new class at your project as:
namespace My\Custom\Doctrine2\Function;
use Doctrine\ORM\Query\Lexer;
class Rand extends \Doctrine\ORM\Query\AST\Functions\FunctionNode
{
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'RAND()';
}
}
Register the class config.yml:
doctrine:
orm:
dql:
numeric_functions:
Rand: My\Custom\Doctrine2\Function\Rand
Use it directly as:
$qb->addSelect('RAND() as HIDDEN rand')->orderBy('rand()'); //Missing curly brackets
In line with what Hassan Magdy Saad suggested, you can use the popular DoctrineExtensions library:
See mysql implementation here: https://github.com/beberlei/DoctrineExtensions/blob/master/src/Query/Mysql/Rand.php
# config.yml
doctrine:
orm:
dql:
numeric_functions:
rand: DoctrineExtensions\Query\Mysql\Rand
Tested in Doctrine ORM 2.6.x-dev, you can then actually do:
->orderBy('RAND()')
Or you could do this -->
$words = $em->getRepository('Entity\Word')->findAll();
shuffle($words);
Of course this would be very inefficient if you have many records so use with caution.
Why not to use repository?
<?php
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# make query
return $this->getEntityManager()->createNativeQuery("
SELECT p.id FROM {$table} p ORDER BY RAND() LIMIT 0, {$amount}
", $rsm);
}
}
For me, the most useful way was to create two arrays where i say order type and different properties of the Entity. For example:
$order = array_rand(array(
'DESC' => 'DESC',
'ASC' => 'ASC'
));
$column = array_rand(array(
'w.id' => 'w.id',
'w.date' => 'w.date',
'w.name' => 'w.name'
));
You could add more entries to array $column like criteria.
Afterwards, you can build your query with Doctrine adding $column and $order inside ->orderBy. For example:
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy($column, $order)
->getQuery()
->setMaxResults(100);
This way improved the performance of my application. I hope this helps someone.
Shuffling can be done on the query (array) result, but shuffling does not pick randomly.
In order to pick randomly from an entity I prefer to do this in PHP, which might slow the random picking, but it allows me to keep control of testing what I am doing and makes eventual debugging easier.
The example below puts all IDs from the entity into an array, which I can then use to "random-treat" in php.
public function getRandomArt($nbSlotsOnPage)
{
$qbList=$this->createQueryBuilder('a');
// get all the relevant id's from the entity
$qbList ->select('a.id')
->where('a.publicate=true')
;
// $list is not a simple list of values, but an nested associative array
$list=$qbList->getQuery()->getScalarResult();
// get rid of the nested array from ScalarResult
$rawlist=array();
foreach ($list as $keyword=>$value)
{
// entity id's have to figure as keyword as array_rand() will pick only keywords - not values
$id=$value['id'];
$rawlist[$id]=null;
}
$total=min($nbSlotsOnPage,count($rawlist));
// pick only a few (i.e.$total)
$keylist=array_rand($rawlist,$total);
$qb=$this->createQueryBuilder('aw');
foreach ($keylist as $keyword=>$value)
{
$qb ->setParameter('keyword'.$keyword,$value)
->orWhere('aw.id = :keyword'.$keyword)
;
}
$result=$qb->getQuery()->getResult();
// if mixing the results is also required (could also be done by orderby rand();
shuffle($result);
return $result;
}
#Krzysztof's solution is IMHO best here, but RAND() is very slow on large queries, so i updated #Krysztof's solution to gives less "random" results, but they are still random enough. Inspired by this answer https://stackoverflow.com/a/4329492/839434.
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# sql query
$sql = "
SELECT * FROM {$table}
WHERE id >= FLOOR(1 + RAND()*(
SELECT MAX(id) FROM {$table})
)
LIMIT ?
";
# make query
return $this->getEntityManager()
->createNativeQuery($sql, $rsm)
->setParameter(1, $amount);
}
}
I hope this would help others:
$limit = $editForm->get('numberOfQuestions')->getData();
$sql = "Select * from question order by RAND() limit $limit";
$statement = $em->getConnection()->prepare($sql);
$statement->execute();
$questions = $statement->fetchAll();
Note here the table question is an AppBundle:Question Entity. Change the details accordingly. The number of questions is taken from the edit form, make sure to check the variable for the form builder and use accordingly.
First get the MAX value from DB table & then use this as offset in PHP i.e
$offset = mt_rand(1, $maxId)
I know this is an old question. But I used the following solution to get the random row.
Using an EntityRepository method:
public function findOneRandom()
{
$id_limits = $this->createQueryBuilder('entity')
->select('MIN(entity.id)', 'MAX(entity.id)')
->getQuery()
->getOneOrNullResult();
$random_possible_id = rand($id_limits[1], $id_limits[2]);
return $this->createQueryBuilder('entity')
->where('entity.id >= :random_id')
->setParameter('random_id', $random_possible_id)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
Probably the easiest (but not necessarily the smartest) way to get a single object result ASAP would be implementing this in your Repository class:
public function findOneRandom()
{
$className = $this->getClassMetadata()->getName();
$counter = (int) $this->getEntityManager()->createQuery("SELECT COUNT(c) FROM {$className} c")->getSingleScalarResult();
return $this->getEntityManager()
->createQuery("SELECT ent FROM {$className} ent ORDER BY ent.id ASC")
->setMaxResults(1)
->setFirstResult(mt_rand(0, $counter - 1))
->getSingleResult()
;
}
Just add the following:
->orderBy('RAND()')

Categories