laravel how to use force index in eloquent - php

I have an SQL query that looks like:
SELECT * FROM orders FORCE INDEX(order_type_index) WHERE `type` = 1
I can use query builder to recreate this:
DB::table(DB::raw('orders force index(orders_type_index)'))
->where('type', 1)
->get();
But is there anyway to make Eloquent use this index, like:
Order::forceIndex('orders_type_index')->where('type', 1)->get();

You can do this by creating a local scope that changes the builder's table name when applied. The table should be checked for the index before it's used. You could throw an exception or ignore it if an invalid index name is supplied (I chose the latter approach.)
<?php
namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/*
* Class Order
*
* #method \Illuminate\Database\Eloquent\Builder useIndex(string $index)
* #method \Illuminate\Database\Eloquent\Builder forceIndex(string $index)
*/
class Order extends Model
{
private function tableIndexExists(string $index): boolean
{
$table = $this->getTable();
$index = strtolower($index);
$indices = Schema::getConnection()
->getDoctrineSchemaManager()
->listTableIndexes($table);
return array_key_exists($index, $indices);
}
public function scopeUseIndex(Builder $query, string $index): Builder
{
$table = $this->getTable();
return $this->tableIndexExists($index)
? $query->from(DB::raw("`$table` USE INDEX(`$index`)"))
: $query;
}
public function scopeForceIndex(Builder $query, string $index): Builder
{
$table = $this->getTable();
return $this->tableIndexExists($index)
? $query->from(DB::raw("`$table` FORCE INDEX(`$index`)"))
: $query;
}
}
If you need to do this on multiple models, it can easily be added to a trait and imported. The docblock ensures that your IDE is aware of the magic methods for code completion.
Then you can use it like this:
$orders = Order::forceIndex("orders_type_index")->where("type", 1)->get();
// or this:
$orders = Customer::find(234)
->orders()
->forceIndex("orders_type_index")
->where("type", 1)
->get();

https://packagist.org/packages/shaburov/laravel-mysql-index-hints-scope
Order::useIndex('test_index')
->ignoreIndex('test_index')
->useIndex(['test_index', 'example_index']);

Related

Extending Laravel/Lumen Query Builder to automagically add SQL comments

I use a large RDS database instance that is shared among several different projects (not microservices to be exact) and this database's performance is critical. Hence I monitor the queries whenever support team raise tickets related to performance of our services. So in order for me to track where each query originated from i.e, which app, file and line number, I want to automatically add a SQL comment for all queries. So when I call toSql() on the query builder object it must show me the comment
-- lumen-api:app/Http/Controllers/APIController.php:85
select * from users;
env(app_name) . ':'. __FILE__ . ':' . __LINE__.
I tried to extend query builder and grammar classes and bind them to the service container but I think I'm doing something wrong. Please take a look at my implementation of how I extended those classes.
<?php
// app/Classes/Database/Query/Grammars/QueryGrammar.php
namespace App\Classes\Database\Query\Grammars;
use App\Classes\Database\Query\QueryBuilder;
use Illuminate\Database\Query\Grammars\Grammar;
class QueryGrammar extends Grammar
{
/**
* #param QueryBuilder $query
* #param $comment
* #return string
*/
public function compileComment(QueryBuilder $query, $comment)
{
$this->selectComponents[] = 'comment';
return '-- ' . $comment . PHP_EOL;
}
}
<?php
// app/Classes/Database/Query/QueryBuilder.php
<?php
namespace App\Classes\Database\Query;
use Illuminate\Database\Query\Builder;
class QueryBuilder extends Builder {
/**
* #param $comment
* #return $this
*/
public function comment($comment): QueryBuilder
{
$this->comment = $comment;
return $this;
}
}
<?php
//app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use App\Classes\Database\Query\Grammars\QueryGrammar;
use App\Classes\Database\Query\QueryBuilder;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
/**
* Extending Query Builder to support SQL comments
*/
$this->app->bind(Grammar::class, function () {
return new QueryGrammar();
});
$this->app->bind(Builder::class, function () {
return new QueryBuilder(/* how to send params?*/);
});
}
}
I know this implementation is not for auto adding the sql comments. So when I use this in my controller:
return Admin::where('login_email','bhargav.nanekalva#mpokket.com')->comment(__FILE__ . __LINE__)->toSql();
Laravel throws the following error: (which means the binding didn't happen)
(1/1) BadMethodCallException
Call to undefined method Illuminate\Database\Eloquent\Builder::comment()
what I need help is for
The right way to modify these classes
automatically adding the sql comment
Overriding the Grammer class directly may be possible but it internally delegates its work to Database Specific grammer classes
For example if you have configured Mysql in config/database.php then the Grammer class delegates the work on to Illuminate\Database\Query\Grammars\MySqlGrammar
Similarly for Postgres it will be Illuminate\Database\Query\Grammars\PostgresGrammar
Based on the database config the
ConnectionFactory[src/Illuminate/Database/Connectors/ConnectionFactory.php->createConnection()]
loads the proper connection manager for a given database
I am not sure if overriding of this classes is even possible or not because of the PSR-4 loading as the namespace is tightly linked with the physical location of the file in the directory tree
So instead of that I would suggest to go for laravel macros by which you may add new functions to existing classes that use Macroable trait
A POC example can be found below, for further advancement you are encouraged to dig the code for update, insert, delete etc in Grammer.php and Builder.php
<?php
namespace App\Providers;
use Illuminate\Support\Arr;
use Illuminate\Support\ServiceProvider;
use DB;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
Grammar::macro("T_compileSelect", function (Builder $query) {
if ($query->unions && $query->aggregate) {
return $this->compileUnionAggregate($query);
}
$original = $query->columns;
if (is_null($query->columns)) {
$query->columns = ['*'];
}
$sql = trim($this->concatenate(
$this->compileComponents($query))
);
if ($query->unions) {
$sql = $this->wrapUnion($sql).' '.$this->compileUnions($query);
}
$query->columns = $original;
return $sql . ' -- ' . (!empty($query->comment)?$query->comment:'');
});
Builder::macro("T_toSql", function () {
$str = $this->grammar->T_compileSelect($this);
return $str;
});
Builder::macro("T_runSelect", function () {
$str = $this->connection->select(
$this->T_toSql(), $this->getBindings(), ! $this->useWritePdo
);
return $str;
});
Builder::macro("addComment", function ($comment, $columns = ['*']) {
$this->comment = $comment;
$res = collect($this->onceWithColumns(Arr::wrap($columns), function () {
return $this->processor->processSelect($this, $this->T_runSelect());
}));
return $res;
});
}
public function boot()
{
}
}
Usage:
Admin::where('login_email','bhargav.nanekalva#mpokket.com')
->addComment(__FILE__ . __LINE__)
->get();

PhpStorm: Is there a way to enforce a type on the return statement using an inline PHPDoc annotation?

Consider the following code:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Car extends Model
{
public static function getTheFirstCar(string $color): ?self
{
/** #var ?self */ // <-- Doesn't apply! Is there any alternative?
return (new self())->newQuery()->firstWhere('color', '=', $color);
}
}
The code is working correctly; nevertheless PhpStorm complains:
Return value is expected to be 'Car|null',
'\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model' returned
Assigning the result of the expression into an annotated variable resolves the warning, but yet introduces a "redundant" variable!
/** #var ?self $redundant */
$redundant = (new self())->newQuery()->firstWhere('color', '=', $color);
return $redundant;
So, is there a way in PhpStorm to enforce an inline type-annotation for the value of the return statement expression explicitly as Car|null, without introducing a redundant variable or specifying all of the expected return types?
You can suppress this warning by adding the #noinspection PhpIncompatibleReturnTypeInspection annotation before your statement.
I personally would not do this, but it's the only answer to you your question about how to "enforce" the return type and suppress warning afaik.
/** #noinspection PhpIncompatibleReturnTypeInspection */
return (new self())->newQuery()->where('color', '=', $color)->first();
If you decide to respect the warning, then this is probably the reason and solution for it:
newQuery() will create a new query on the models table (most likely: cars) without setting the appropriate model (Car).
Internally you're now running a bare query on cars. Therefore you will receive the appropriate record but is not gonna be an instance of Car, but an instance of Model instead. Therefore PhpStorm is expecting multiple additional return types here and printing this warning on your statement since it differs from the methods return type ?self.
The quick solution is to change newQuery() into newModelQuery(). This will create a new query and set the Model Car on the created query and return the appropriate instance or null
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Car extends Model
{
public static function getTheFirstCar(string $color): ?self
{
return (new self())->newModelQuery()->firstWhere('color', '=', $color);
// I'd use this statement though:
// return self::where('color', '=', $color)->first();
}
}
You need to add doc block to your class:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Class Car
* #package App\Models
*
* #method self|Builder newQuery()
* #method ?self firstWhere($column, $operator = null, $value = null, $boolean = 'and')
*/
class Car extends Model
{
public static function getTheFirstCar(string $color): ?self
{
return (new self())->newQuery()->firstWhere('color', '=', $color);
}
}
As of PhpStorm 2022.2 anonymous #var annotations are supported, so your code already should work as is:
/** #var self */
return (new self())->newQuery()->firstWhere('color', '=', $color);

Laravel method that can be used by multiple controllers and commands. Where should it be?

I'd like to define a "global" method that can be used by multiple controllers and commands. Where should it be placed in Laravel 5.4?
Let's say I have the following controller. How would I call the "global" method instead, and where would that "global" method be located exactly?
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Flight;
class FlightsController extends Controller
{
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Index
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$flights = Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
}
When you want a method that fetches many models, and you want to use it in many places, put it in a Repository:
class FlightRepository
{
public function getLastTenFlights()
{
return Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
}
}
For example from your controller:
public function index( FlightRepository $repo )
{
$flights = $repo->getLastTenFlights();
//if you want you can put this additional login in the method too...
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
You can create a Object and call the object when you want.
See example:
FlighRepository = new FlighRepository;
FlighRepository->index();
I personally prefer query scopes to repositories, so I would do something like this:
class Flight extends Model
{
// model setup
/**
* Scope query to get last 10 flights.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeLastTen($query)
{
return $query->where('active', 1)->orderBy('name', 'desc')->take(10);
}
// rest of model
}
And you can use it similarly to how you're currently using it, only it's more readable:
$flights = Flight::lastTen()->get();
This also has the advantage of being able to chain other queries off of it. Say, for example, you wanted the last ten American Airlines flights, you could do:
$flights = Flight::lastTen()->where('airline', 'American')->get();
// equivalent to
// $flights = Flight::where('airline', 'American')->lastTen()->get();
I think that service is the best option to store the functionality which is shared between controllers and commands. You can access them using Service Container (https://laravel.com/docs/5.5/container).

Symfony 2.5 and own models

In Symfony I'm using default ORM Doctrine, but this tool can't give me enough methods to manipulate with different cases. I want to write my own classes and using something like DBAL, just for connections making custom SQL queries and fetch the result. Who can give me some examples? Which classes I should use to make my model layer, extend my functionality.
If you want to write your own SQL query with doctrine you can check that doc page:
http://doctrine-orm.readthedocs.org/en/latest/reference/native-sql.html
but most of the time DQL is more than enough
this is from a project I did, ignore my sql query
//Daily Alerts
$dailyAlertsQuery = $em
->createQuery("
SELECT COUNT (a) AS daily
FROM XXX\xxxBundle\Entity\Alert a
JOIN a.user u
WHERE a.mentionUpdate = '1'
AND u.isActive = '1'
")
->getResult();
$dailyAlerts = new ArrayCollection($dailyAlertsQuery);
As a good practise, you may put all your custom queries (SQL or DQL) in an EntityRepository
Here is an example of a custom EntityRepository
use Doctrine\ORM\EntityRepository;
use \Doctrine\Common\Collections\Criteria;
/**
* RequestRepository
*
*/
class RequestRepository extends EntityRepository
{
public function findByStatus($status = array(), $limit = 5, $orderBy = null)
{
$queryBuilder = $this->_em->createQueryBuilder();
$queryBuilder
->select('n')
->from('BaseBundle:Request', 'n')
->where('n.status IN (:status)')
->setParameter('status', $status)
->setMaxResults($limit);
if (!is_null($orderBy)) {
$queryBuilder->orderBy('n.' . $orderBy[0], $orderBy[1]);
}
$lines = array();
foreach ($queryBuilder->getQuery()->getResult() as $line) {
$lines[] = $line;
}
return $lines;
}
public function getByExpressions($expressions = array(), $limit = 5, $orderBy = null)
{
$criteria = Criteria::create();
if (!empty($expressions)) {
foreach ($expressions as $expression) {
$criteria->andWhere($expression);
}
}
$criteria->setMaxResults($limit);
if (!is_null($orderBy)) {
$criteria->orderBy($orderBy);
}
return $this->matching($criteria);
}
}
And in the Entity code, you define this custom repository as follows:
use Doctrine\ORM\Mapping as ORM;
/**
* Request
*
* #ORM\Table(name="request")
* #ORM\Entity(repositoryClass="BaseBundle\Repository\RequestRepository")
*/
class Request
{
//Entity code, irrelevant here
}

Laravel default orderBy

Is there a clean way to enable certain models to be ordered by a property by default?
It could work by extending the laravel's QueryBuilder, but to do so, you'll have to rewire some of it's core features - bad practice.
reason
The main point of doing this is - one of my models get's heavily reused by many others and right now you have to resort the order over and over again. Even when using a closure for this - you still have to call it. It would be much better to be able to apply a default sorting, so everyone who uses this model, and does not provide custom sorting options, will receive records sorted by the default option. Using a repository is not an option here, because it get's eager loaded.
SOLUTION
Extending the base model:
protected $orderBy;
protected $orderDirection = 'ASC';
public function scopeOrdered($query)
{
if ($this->orderBy)
{
return $query->orderBy($this->orderBy, $this->orderDirection);
}
return $query;
}
public function scopeGetOrdered($query)
{
return $this->scopeOrdered($query)->get();
}
In your model:
protected $orderBy = 'property';
protected $orderDirection = 'DESC';
// ordering eager loaded relation
public function anotherModel()
{
return $this->belongsToMany('SomeModel', 'some_table')->ordered();
}
In your controller:
MyModel::with('anotherModel')->getOrdered();
// or
MyModel::with('anotherModel')->ordered()->first();
Before Laravel 5.2
Nowadays we can solve this problem also with global scopes, introduced in Laravel 4.2 (correct me if I'm wrong). We can define a scope class like this:
<?php namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ScopeInterface;
class OrderScope implements ScopeInterface {
private $column;
private $direction;
public function __construct($column, $direction = 'asc')
{
$this->column = $column;
$this->direction = $direction;
}
public function apply(Builder $builder, Model $model)
{
$builder->orderBy($this->column, $this->direction);
// optional macro to undo the global scope
$builder->macro('unordered', function (Builder $builder) {
$this->remove($builder, $builder->getModel());
return $builder;
});
}
public function remove(Builder $builder, Model $model)
{
$query = $builder->getQuery();
$query->orders = collect($query->orders)->reject(function ($order) {
return $order['column'] == $this->column && $order['direction'] == $this->direction;
})->values()->all();
if (count($query->orders) == 0) {
$query->orders = null;
}
}
}
Then, in your model, you can add the scope in the boot() method:
protected static function boot() {
parent::boot();
static::addGlobalScope(new OrderScope('date', 'desc'));
}
Now the model is ordered by default. Note that if you define the order also manually in the query: MyModel::orderBy('some_column'), then it will only add it as a secondary ordering (used when values of the first ordering are the same), and it will not override. To make it possible to use another ordering manually, I added an (optional) macro (see above), and then you can do: MyModel::unordered()->orderBy('some_column')->get().
Laravel 5.2 and up
Laravel 5.2 introduced a much cleaner way to work with global scopes. Now, the only thing we have to write is the following:
<?php namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class OrderScope implements Scope
{
private $column;
private $direction;
public function __construct($column, $direction = 'asc')
{
$this->column = $column;
$this->direction = $direction;
}
public function apply(Builder $builder, Model $model)
{
$builder->orderBy($this->column, $this->direction);
}
}
Then, in your model, you can add the scope in the boot() method:
protected static function boot() {
parent::boot();
static::addGlobalScope(new OrderScope('date', 'desc'));
}
To remove the global scope, simply use:
MyModel::withoutGlobalScope(OrderScope::class)->get();
Solution without extra scope class
If you don't like to have a whole class for the scope, you can (since Laravel 5.2) also define the global scope inline, in your model's boot() method:
protected static function boot() {
parent::boot();
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('date', 'desc');
});
}
You can remove this global scope using this:
MyModel::withoutGlobalScope('order')->get();
In Laravel 5.7, you can now simply use addGlobalScope inside the model's boot function:
use Illuminate\Database\Eloquent\Builder;
protected static function boot()
{
parent::boot();
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('created_at', 'desc');
});
}
In the above example, I order the model by created_at desc to get the most recent records first. You can change that to fit your needs.
Another way of doing this could be by overriding the newQuery method in your model class. This only works if you never, ever want results to be ordered by another field (since adding another ->orderBy() later won't remove this default one). So this is probably not what you'd normally want to do, but if you have a requirement to always sort a certain way, then this will work:
protected $orderBy;
protected $orderDirection = 'asc';
/**
* Get a new query builder for the model's table.
*
* #param bool $ordered
* #return \Illuminate\Database\Eloquent\Builder
*/
public function newQuery($ordered = true)
{
$query = parent::newQuery();
if (empty($ordered)) {
return $query;
}
return $query->orderBy($this->orderBy, $this->orderDirection);
}
Yes you would need to extend Eloquent to always do this as standard for any query. What's wrong with adding an order by statement to the query when you need it ordered? That is the cleanest way, ie, you dont need to 'unhack' Eloquent to get results by natural order.
MyModel::orderBy('created_at', 'asc')->get();
Other than that the closest thing to what you want would be to create query scopes in your models.
public function scopeOrdered($query)
{
return $query->orderBy('created_at', 'asc')->get();
}
You can then call ordered as a method instead of get to retrieve your ordered results.
$data = MyModel::where('foo', '=', 'bar')->ordered();
If you wanted this across different models you could create a base class and just extend it to the models you want to have access to this scoped method.
you should use eloquent global scope that can apply to all queries(also you can set parameter for it).
And for relations you can use this useful trick:
class Category extends Model {
public function posts(){
return $this->hasMany('App\Models\Post')->orderBy('title');
}
}
this will add order by to all posts when we get them from a category.
If you add an order by to your query, this default order by will cancel!
An slightly improved answer given by Joshua Jabbour
you can use the code he offered in a Trait, and then add that trait to the models where you want them to be ordered.
<?php
namespace App\Traits;
trait AppOrdered {
protected $orderBy = 'created_at';
protected $orderDirection = 'desc';
public function newQuery($ordered = true)
{
$query = parent::newQuery();
if (empty($ordered)) {
return $query;
}
return $query->orderBy($this->orderBy, $this->orderDirection);
}
}
then in whichever model you want the data to be ordered you can use use :
class PostsModel extends Model {
use AppOrdered;
....
now everytime you request that model, data will be ordered, that's somehow more organized, but my answers is Jabbour's answer.
I built a mini Laravel package that can add default orderBy in your Eloquent model.
Using the DefaultOrderBy trait of this package, you can set the default column you want to orderBy.
use Stephenjude/DefaultModelSorting/Traits/DefaultOrderBy;
class Article extends Model
{
use DefaultOrderBy;
protected static $orderByColumn = 'title';
}
You can also set the default orderBy direction by setting the $orderByColumnDirection property.
protected static $orderByColumnDirection = 'desc';
A note from my experience, never to use orderBy and GroupBy such term on global scope. Otherwise you will easily face database errors while fetching related models in other places.
Error may be something like:
"ORDER BY "created_at" is ambiguous"
In such case the solution can be giving table name before column names in your query scope.
"ORDER BY posts.created_at"
Thanks.

Categories