Jak w Laravelu logować zapytania SQL

Dziś poruszymy temat logowania i debugowania zapytań SQL we frameworku Laravel.

Opis problemu

Biorąc pod uwagę następujący kod:


DB::table('users')->get();
/pre>

Chcę uzyskać nieprzetworzony ciąg zapytania SQL, który wygeneruje powyższy konstruktor zapytań do bazy danych. W tym przykładzie będzie to select * from users;.

Jak mam to zrobic?

Rozwiązanie problemu

Aby wyświetlić na ekranie ostatnie uruchomione zapytania w Laravelu, można użyć następującej składni:


DB::enableQueryLog(); // Enable query log

dd(DB::getQueryLog()); // Show results of log

Najnowsze zapytania będą na dole tablicy. Będziesz miał coś takiego:


array(1) {
  [0]=>
  array(3) {
    ["query"]=>
    string(21) "select * from "users""
    ["bindings"]=>
    array(0) {
    }
    ["time"]=>
    string(4) "0.92"
  }
}

Ale możesz także użyć metody toSql() w instancji QueryBuilder:


DB::table('users')->toSql();

która zwróci select * from users;

Jest to łatwiejsze niż podłączenie detektora zdarzeń, a także pozwala sprawdzić, jak zapytanie będzie wyglądać w dowolnym momencie podczas jego budowania.

Pamiętaj także, że DB::queryLog() działa tylko po wykonaniu zapytania $builder->get(). jeśli chcesz uzyskać zapytanie przed jego wykonaniem, możesz użyć metody $builder->toSql (). Zobaczmy przykład:

$query = str_replace(array('?'), array('\'%s\''), $builder->toSql()); $query = vsprintf($query, $builder->getBindings()); dump($query); $result = $builder->get();

Natomiast na skróty możesz zrobić błąd zapytania, np. Wywołanie nieistniejącej tabeli lub kolumny, zobaczysz wygenerowane zapytanie w wyjątku.

Możesz to także zrobić za pomocą Event i listenera:


Event::listen('illuminate.query', function($query, $params, $time, $conn) 
{ 
    dd(array($query, $params, $time, $conn));
});

DB::table('users')->get();
Ten przykład wydrukuje następujący log:


array(4) {
  [0]=>
  string(21) "select * from "users""
  [1]=>
  array(0) {
  }
  [2]=>
  string(4) "0.94"
  [3]=>
  string(6) "sqlite"
}

Można także napisać makro:


\Illuminate\Database\Query\Builder::macro('toRawSql', function(){
    return array_reduce($this->getBindings(), function($sql, $binding){
        return preg_replace('/\?/', is_numeric($binding) ? $binding : "'".$binding."'" , $sql, 1);
    }, $this->toSql());
});

I dodać alias w Eloquent Builderze:


\Illuminate\Database\Eloquent\Builder::macro('toRawSql', function(){
    return ($this->getQuery()->toRawSql());
});

A na koniec tylko debugować:


\Log::debug(\App\User::limit(1)->toRawSql());

Jeśli chcesz otrzymywać każde zapytanie SQL wykonane przez aplikację, możesz użyć metody Listen. Ta metoda jest przydatna do rejestrowania zapytań lub debugowania. Możesz zarejestrować swój detektor zapytań w service providerze:


<?php

namespace App\Providers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        DB::listen(function ($query) {
            // $query->sql
            // $query->bindings
            // $query->time
        });
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Ale jeżeli nie używasz Eloquenta, użyj tego kodu:

use \Illuminate\Database\Capsule\Manager as Capsule; use \Illuminate\Events\Dispatcher; use \Illuminate\Container\Container; $capsule = new Capsule; $capsule->addConnection([ // connection details ]); // Set the event dispatcher used by Eloquent models... (optional) $capsule->setEventDispatcher(new Dispatcher(new Container)); // Make this Capsule instance available globally via static methods... (optional) $capsule->setAsGlobal(); // Setup the Eloquent ORM...(optional unless you've used setEventDispatcher()) $capsule->bootEloquent(); // Listen for Query Events for Debug $events = new Dispatcher; $events->listen('illuminate.query', function($query, $bindings, $time, $name) { // Format binding data for sql insertion foreach ($bindings as $i => $binding) { if ($binding instanceof \DateTime) { $bindings[$i] = $binding->format('\'Y-m-d H:i:s\''); } else if (is_string($binding)) { $bindings[$i] = "'$binding'";`enter code here` } } // Insert bindings into query $query = str_replace(array('%', '?'), array('%%', '%s'), $query); $query = vsprintf($query, $bindings); // Debug SQL queries echo 'SQL: [' . $query . ']'; }); $capsule->setEventDispatcher($events);

Do debugowania w konsoli możesz użyć Tinkera:


$php artisan tinker
Psy Shell v0.9.9 (PHP 7.3.5 — cli) by Justin Hileman
>>> DB::listen(function ($query) { dump($query->sql); dump($query->bindings); dump($query->time); });
=> null
>>> App\User::find(1)
"select * from `users` where `users`.`id` = ? limit 1"
array:1 [
  0 => 1
]
6.99
=> App\User {#3131
     id: 1,
     name: "admin",
     email: "admin@example.com",
     created_at: "2019-01-11 19:06:23",
     updated_at: "2019-01-11 19:06:23",
   }
>>>

Możesz także napisać taką funkcję:

DB::listen(function ($sql, $bindings, $time) { $bound = preg_replace_callback("/\?/", function($matches) use ($bindings) { static $localBindings; if (!isset($localBindings)) { $localBindings = $bindings; } $val = array_shift($localBindings); switch (gettype($val)) { case "boolean": $val = ($val === TRUE) ? 1 : 0; // mysql doesn't support BOOL data types, ints are widely used // $val = ($val === TRUE) ? "'t'" : "'f'"; // todo: use this line instead of the above for postgres and others break; case "NULL": $val = "NULL"; break; case "string": case "object": $val = "'". addslashes($val). "'"; // correct escaping would depend on the RDBMS break; } return $val; }, $sql); array_map(function($x) { (new \Illuminate\Support\Debug\Dumper)->dump($x); }, [$sql, $bindings, $bound]); });

Przeczytaj komentarze w kodzie. Wiem, że nie jest idealny, ale do codziennego debugowania jest w porządku. Ten kod jest zadowlająco niezawodny, jednak nie ufaj mu całkowicie, ponieważ silniki baz danych inaczej uciekają od wartości, których ta krótka funkcja nie implementuje. Więc naprawdę ostrożnie przeglądaj wyniki.