Laravel5.5源码详解 -- 一次查询的详细执行:从Auth-Login-web中间件到数据库查询结果的全过程
2017-12-26 12:11
1041 查看
Laravel5.5源码详解 – 一次查询的详细执行:从Auth-Login-web中间件到数据库查询结果的全过程
因为没时间做太多整理,只是详细记录了一下事整个查询语句执行的全过程,更多的信息待有时间再整理。在我的Controller中,源代码是这样的,
$flag = Auth::guard('web')->attempt(['email' => $account, 'password' => $password, 'active' => 1, 'status' => 1], $remember);
下面我们从出发到回归,详细进行一次流程追踪。
首先,通过Auth的门面,找到Illuminate\Auth\AuthManger,其中guard函数原型如下,含注释
public function guard($name = null) { // Auth::guard('web') --> 传入'web'中间件, name = 'web' $name = $name ?: $this->getDefaultDriver(); // ($this->guards); 此时是空的 return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); }
因此,Auth::guard返回的就是sessionguard,如下
SessionGuard {#327 ▼ #name: "web" #lastAttempted: null #viaRemember: false #session: Store {#300 ▶} #cookie: CookieJar {#284 ▶} #request: Request {#42 ▶} #events: Dispatcher {#26 ▶} #loggedOut: false #recallAttempted: false #user: null #provider: EloquentUserProvider {#321 ▶} }
源码在 Illuminate\Auth\SessionGuard (继承自 <== Illuminate\Contracts\Auth\StatefulGuard 继承自<== Illuminate\Contracts\Auth\Guard, 这里的StatefulGuard 和 Guard是两个接口interface)。
接下来,SessionGuard会进行一次 attempt操作,
public function attempt(array $credentials = [], $remember = false) { $this->fireAttemptEvent($credentials, $remember); $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); if ($this->hasValidCredentials($user, $credentials)) { $this->login($user, $remember); return true; } $this->fireFailedEvent($user, $credentials); return false; }
我这里传入的参数就是
$credentials = array:4 [▼ "email" => "admin01@123.com" "password" => "123456" "active" => 1 "status" => 1 ] $remember = true
调用
protected function fireAttemptEvent(array $credentials, $remember = false) { if (isset($this->events)) { $this->events->dispatch(new Events\Attempting( $credentials, $remember )); } }
调试一下,dd($this->events)发现是个Dispatcher,
Dispatcher {#26 ▼ #container: Application {#2 ▶} #listeners: array:6 [▼ "App\Events\Event" => array:1 [▶] "Illuminate\Log\Events\MessageLogged" => array:1 [▶] "Illuminate\Database\Events\QueryExecuted" => array:1 [▶] "Illuminate\Database\Events\TransactionBeginning" => array:1 [▶] "Illuminate\Database\Events\TransactionCommitted" => array:1 [▶] "Illuminate\Database\Events\TransactionRolledBack" => array:1 [▶] ] #wildcards: array:4 [▶] #queueResolver: Closure {#27 ▼ class: "Illuminate\Events\EventS 4000 erviceProvider" this: EventServiceProvider {#6 …} use: {▶} file: "D:\wamp64\www\laravel\laraveldb\vendor\laravel\framework\src\Illuminate\Events\EventServiceProvider.php" line: "18 to 20" } }
dispatch出去的,其实是个类 Illuminate\Auth\Events\Attempting ,不过这个类也没做啥太多,
<?php namespace Illuminate\Auth\Events; class Attempting { public $credentials; public $remember; public function __construct($credentials, $remember) { $this->remember = $remember; $this->credentials = $credentials; } }
Dispatch这段到底还有什么?这里还应该有些细节,
$this->fireAttemptEvent($credentials, $remember);
但目前还不是我们要关心的核心功能,先跳过去。
接下来看attempt()中,this->provider在其构造函数中指向的是一个interface,
Illuminate\Contracts\Auth\UserProvider
实质调用的是其实现类,
Illuminate\Auth\EloquentUserProvider
函数retrieveByCredentials()如下,
public function retrieveByCredentials(array $credentials) { if (empty($credentials) || (count($credentials) === 1 && array_key_exists('password', $credentials))) { return; } // 得到queryBuilder $query = $this->createModel()->newQuery(); // 执行一些准备工作 foreach ($credentials as $key => $value) { if (! Str::contains($key, 'password')) { $query->where($key, $value); } } // 进行查询 return $query->first(); }
这里重点是$query->first(),在此之前,\$this- >creatModel()返回的是一个App\User类的对象,它继承自Illuminate\Foundation\Auth\User,它又继承自Illuminate\Database\Eloquent\Model,然后,调用Model里的newQuery()创建一个QueryBuilder,即Illuminate\Database\Query\Builder。
接下来,where实际也是调用了 Illuminate\Database\Eloquent\Builder,这些都是准备工作,
public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column instanceof Closure) { $query = $this->model->newQueryWithoutScopes(); $column($query); $this->query->addNestedWhereQuery($query->getQuery(), $boolean); } else { $this->query->where(...func_get_args()); } return $this; }
然后在其中的where再调用 Illuminate\Database\Query\Builder
public function where($column, $operator = null, $value = null, $boolean = 'and') { if (is_array($column)) { return $this->addArrayOfWheres($column, $boolean); } list($value, $operator) = $this->prepareValueAndOperator( $value, $operator, func_num_args() == 2 ); if ($column instanceof Closure) { return $this->whereNested($column, $boolean); } if ($this->invalidOperator($operator)) { list($value, $operator) = [$operator, '=']; } if ($value instanceof Closure) { return $this->whereSub($column, $operator, $value, $boolean); } if (is_null($value)) { return $this->whereNull($column, $boolean, $operator !== '='); } if (Str::contains($column, '->') && is_bool($value)) { $value = new Expression($value ? 'true' : 'false'); } $type = 'Basic'; $this->wheres[] = compact( 'type', 'column', 'operator', 'value', 'boolean' ); if (! $value instanceof Expression) { // 下面这一句会运行。。。 $this->addBinding($value, 'where'); } return $this; }
看看addBinding()
public function addBinding($value, $type = 'where') { if (! array_key_exists($type, $this->bindings)) { throw new InvalidArgumentException("Invalid binding type: {$type}."); } if (is_array($value)) { $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); } else { // 只有下面这句是有效运行的 $this->bindings[$type][] = $value; } return $this; }
现在来到我们的重点函数Illuminate\Database\Eloquent\Builder中调用的first() ,Eloquent\Builder类中并没有first()函数,实际运行过程它调用的是Illuminate\Database\Concerns\BuildsQueries::first(),其机理如下,
... use Illuminate\Database\Concerns\BuildsQueries; ... class Builder { use BuildsQueries, Macroable { __call as macroCall; } ...
这里简单说一下Macroable这个宏, 在Illuminate\Support\Traits\Macroable中 ,它利用重载实现了可以定义宏的功能,即通过 macro 静态方法添加回调,并定义一个名字。 当前类没有这个函数的时候,利用 __call来处理执行这个函数名注册的回调。
所以,函数first()也就是,
<?php namespace Illuminate\Database\Concerns; use Illuminate\Container\Container; use Illuminate\Pagination\Paginator; use Illuminate\Pagination\LengthAwarePaginator; trait BuildsQueries { public function first($columns = ['*']) { return $this->take(1)->get($columns)->first(); }
这里QueryBuilder中的函数take(1),实际上只做了一件事,就是设置 $this->$property = 1,
public function take($value) { return $this->limit($value); } public function limit($value) { $property = $this->unions ? 'unionLimit' : 'limit'; if ($value >= 0) { $this->$property = $value; //$value = 1 from take(1) return $this; }
再get(),它执行了最重要的查询处理过程,
public function get($columns = ['*']) { $original = $this->columns; if (is_null($original)) { $this->columns = $columns; } // $results是得到的结果,如果得不到信息,就是空的 $results = $this->processor->processSelect($this, $this->runSelect()); $this->columns = $original; return collect($results); }
重点,runSelect()
protected function runSelect() { return $this->connection->select( $this->toSql(), $this->getBindings(), ! $this->useWritePdo ); }
$this->toSql()
"select * from `users` where `email` = ? and `active` = ? and `status` = ? limit 1"
$this->getBindings()
array:3 [▼ 0 => "admin01@123.com" 1 => 1 2 => 1 ]
$this->useWritePdo
false
相当于在mySql中执行查询语句
select * from users where email = 'admin01@123.com' and active = 1 and status = 1;
终于到了我们期待已久的 Illuminate\Database\Connection,执行select()
public function select($query, $bindings = [], $useReadPdo = true) { return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { if ($this->pretending()) { return []; } $statement = $this->prepared($this->getPdoForSelect($useReadPdo) ->prepare($query)); $this->bindValues($statement, $this->prepareBindings($bindings)); $statement->execute(); return $statement->fetchAll(); }); }
这里先说一下run()这个函数,几乎所有的数据库操作都要经过这个函数,这里传入的参数分别是$query, $bindings,和一个闭包函数,其重点核心在这个闭包函数里面,这个放到后面再叙述,这里先看run(),注意源码中的注释部分,
protected function run($query, $bindings, Closure $callback) { // 确保PDO已经正确连接 $this->reconnectIfMissingConnection(); // 得到当前的时间,这是日志记录的需要 $start = microtime(true); // Here we will run this query. If an exception occurs we'll determine if it was // caused by a connection that has been lost. If that is the cause, we'll try // to re-establish connection and re-run the query with a fresh connection. try { // 执行闭包函数 $result = $this->runQueryCallback($query, $bindings, $callback); } catch (QueryException $e) { // 如果执行闭包函数时抛出异常,处理异常情况 $result = $this->handleQueryException( $e, $query, $bindings, $callback ); } // 保存相关信息的日志 $this->logQuery( $query, $bindings, $this->getElapsedTime($start) ); // 返回处理结果,一般情况下,得到的是闭包返回的查询结果 return $result; } // 确保PDO已经正确连接 protected function reconnectIfMissingConnection() { if (is_null($this->pdo)) { $this->reconnect(); } } // 闭包在这个函数中执行 protected function runQueryCallback($query, $bindings, Closure $callback) { try { // 执行闭包函数 $result = $callback($query, $bindings); } catch (Exception $e) { // 如果执行查询时出现异常,处理异常情况 throw new QueryException( $query, $this->prepareBindings($bindings), $e ); } return $result; } // 保存相关信息的日志 public function logQuery($query, $bindings, $time = null) { $this->event(new QueryExecuted($query, $bindings, $time, $this)); if ($this->loggingQueries) { $this->queryLog[] = compact('query', 'bindings', 'time'); } }
现在来看最关键的部分,闭包函数,我们单独把它拎出来,
function ($query, $bindings) use ($useReadPdo) { if ($this->pretending()) { return []; } $statement = $this->prepared($this->getPdoForSelect($useReadPdo) ->prepare($query)); $this->bindValues($statement, $this->prepareBindings($bindings)); $statement->execute(); return $statement->fetchAll(); }
其中,$this->pretending是false,因此返回的也是false (不知道这个有什么用,没仔细看,先跳过)
public function pretending() { return $this->pretending === true; }
这个$statement是
PDOStatement {#178 ▼ +queryString: "select * from `users` where `email` = ? and `active` = ? and `status` = ? limit 1" }
$this - >prepareBindings($bindings)还是前面那个数组
array:3 [▼ 0 => "admin01@123.com" 1 => 1 2 => 1 ]
到这里要说明一下,下面这句代码实现了 数据库的连接操作 和 SQL语句送入mySql服务器进行语句编译
$this->getPdoForSelect($useReadPdo)->prepare($query)
Connection中有个函数bindValues(),
public function bindValues($statement, $bindings) { foreach ($bindings as $key => $value) { $statement->bindValue( is_string($key) ? $key : $key + 1, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR ); } }
这个函数和Illuminate\Database\MySqlConnection中的bindValues基本一样,实际调用的,是MySqlConnection中的这个bindValues(),
public function bindValues($statement, $bindings) { dd($statement); foreach ($bindings as $key => $value) { $statement->bindValue( is_string($key) ? $key : $key + 1, $value, is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR ); } }
前面说过,这个$statement就是PDOStatement。PDOStatement是PHP5.1开始,配合PDO提供的一个数据库操作执行语句的类,具体可参考:
http://php.net/manual/en/class.pdostatement.php
PDOStatement implements Traversable { /* Properties */ readonly string $queryString; /* Methods */ public bool bindColumn ( mixed $column , mixed &$param [, int $type [, int $maxlen [, mixed $driverdata ]]] ) public bool bindParam ( mixed $parameter , mixed &$variable [, int $data_type = PDO::PARAM_STR [, int $length [, mixed $driver_options ]]] ) public bool bindValue ( mixed $parameter , mixed $value [, int $data_type = PDO::PARAM_STR ] ) public bool closeCursor ( void ) public int columnCount ( void ) public void debugDumpParams ( void ) public string errorCode ( void ) public array errorInfo ( void ) public bool execute ([ array $input_parameters ] ) public mixed fetch ([ int $fetch_style [, int $cursor_orientation = PDO::FETCH_ORI_NEXT [, int $cursor_offset = 0 ]]] ) public array fetchAll ([ int $fetch_style [, mixed $fetch_argument [, array $ctor_args = array() ]]] ) public mixed fetchColumn ([ int $column_number = 0 ] ) public mixed fetchObject ([ string $class_name = "stdClass" [, array $ctor_args ]] ) public mixed getAttribute ( int $attribute ) public array getColumnMeta ( int $column ) public bool nextRowset ( void ) public int rowCount ( void ) public bool setAttribute ( int $attribute , mixed $value ) public bool setFetchMode ( int $mode ) }
其中被调用的函数bindValue,接受的实际传入参数分别是,
(1,"admin01@123.com", 2) (2,1,1) (3,1,1) 其中 PDO::PARAM_INT = 1,PARAM_STR = 2,
所以,那个PDOStatement最后就变成了,
(queryString: select * from `users` where `email` = admin01@123.com and `active` = 1 and `status` = 1 limit 1)
闭包中最后这两句,其实都是PDOStatement在干活,没laravel啥事,PDOStatement::fetchAll()得到的是一个数组,里面包含了查询得到的全部结果。
$statement->execute(); return $statement->fetchAll();
归结起来,就是PDO三步走:
编译 PDO::prepare() 值绑定 PDOStatement::bindValue() 执行 PDOStatement::execute()
OK,这里数据查询处理完毕,接下来看回到laravel中对结果的处理,回到这一句,
$results = $this->processor->processSelect($this, $this->runSelect());
这个processor是Illuminate\Database\Query\Processors\Processor,它是在QueryBuilder创建时在构造中注入的,它负责查询结果的后处理,
$this->processor = $processor ?: $connection->getPostProcessor();
函数processSelect()
<?php namespace Illuminate\Database\Query\Processors; use Illuminate\Database\Query\Builder; class Processor { public function processSelect(Builder $query, $results) { return $results; }
啥,什么也没做?好吧!
然后打道回府,一路回到Illuminate\Auth\SessionGuard,如果成功,就是
if ($this->hasValidCredentials($user, $credentials)) { $this->login($user, $remember); return true; }
如果不成功,就是
$this->fireFailedEvent($user, $credentials); return false;
其中,
protected function fireFailedEvent($user, array $credentials) { if (isset($this->events)) { $this->events->dispatch(new Events\Failed($user, $credentials)); } }
传入的Events\Failed也只是实现了构造注入对象,同样没干什么实际性的工作,
<?php namespace Illuminate\Auth\Events; class Failed { public $user; public $credentials; public function __construct($user, $credentials) { $this->user = $user; $this->credentials = $credentials; } }
这里,fireFailedEvent做了和fireAttemptEvent非常类似的工作,就是把结果分发出去。到这里,基本结束了本文的主旨,SessionGuard的attempt会返回一个false,用户可自行定义怎么处理这个返回来的结果。
本次事件分发处理相关的源码
这里顺便记录一点本次事件分发处理相关的源码,它与本文主旨无关,只是为以后事件events的源码分析作准备,更详细的流程,会另文处理。这个Dispatcher是在SessionGuard的
public function setDispatcher(Dispatcher $events) { $this->events = $events; }
里设置的,原型是 Illuminate\Contracts\Events\Dispatcher,当然这只是个接口,真正的实现在
\Illuminate\Events\Dispatcher, 这种关联在Application中可以看得很明白
'events' => [ \Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class ],
当这里查询失败Failed的时候,进行的两次分发事件分别为
Failed {#594 ▼ +user: null +credentials: array:4 [▼ "email" => "admin01@123.com" "password" => "123456" "active" => 1 "status" => 1 ] } RequestHandled {#53 ▼ +request: Request {#42 ▶} +response: JsonResponse {#588 ▼ #data: "{"status":false,"data":[],"message":"\u767b\u5f55\u5931\u8d25\uff0c\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef"}" #callback: null #encodingOptions: 0 +headers: ResponseHeaderBag {#300 ▶} #content: "{"status":false,"data":[],"message":"\u767b\u5f55\u5931\u8d25\uff0c\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef"}" #version: "1.1" #statusCode: 200 #statusText: "OK" #charset: null +original: array:3 [▶] +exception: null } }
\u767b\u5f55\u5931\u8d25\uff0c\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef是我的设置,中文显示出来就是最常见的那句“登录失败,用户名或密码错误”;
因为这里分发的东西比太多了,调试时要特定捕获,我是这样做的,
if($event instanceof \Illuminate\Auth\Events\Failed) dump($event);
下面来看dispatch
public function dispatch($event, $payload = [], $halt = false) { // 如果传入的是事件对象,最后$event会得到对象类名,如"Illuminate\Auth\Events\Failed" list($event, $payload) = $this->parseEventAndPayload( $event, $payload ); // 不进行broadcast if ($this->shouldBroadcast($payload)) { $this->broadcastEvent($payload[0]); } $responses = []; // 获取事件监听 foreach ($this->getListeners($event) as $listener) { $response = $listener($event, $payload); if ($halt && ! is_null($response)) { return $response; } if ($response === false) { break; } $responses[] = $response; } return $halt ? null : $responses; } // 注意下面的调试函数dump的位置, protected function parseEventAndPayload($event, $payload) { echo("-----进入函数开始截获--------------------------------------------"); dump($event); if (is_object($event)) { list($payload, $event) = [[$event], get_class($event)]; } dump($event); dump($payload); return [$event, Arr::wrap($payload)]; } // 取决于用户设置,本次没有broadcast protected function shouldBroadcast(array $payload) { return isset($payload[0]) && $payload[0] instanceof ShouldBroadcast && $this->broadcastWhen($payload[0]); }
首先,用parseEventAndPayload函数利用传入参数是事件名还是事件类实例来确定监听类函数的参数,如果是事件类的实例,那么监听函数的参数就是事件类自身;如果是事件类名,那么监听函数的参数就是触发事件时传入的参数,
我刻意在源码中加了三处信息截获调试函数,得到的两次运行结果如下,
-----进入函数开始截获------------------------------------------------ QueryExecuted {#222 ▼ +sql: "select * from `users` where `email` = ? and `active` = ? and `status` = ? limit 1" +bindings: array:3 [▶] +time: 537.15 +connection: MySqlConnection {#167 ▶} +connectionName: "mysql" } "Illuminate\Database\Events\QueryExecuted" array:1 [▼ 0 => QueryExecuted {#222 ▶} ] -----进入函数开始截获------------------------------------------------ Failed {#21 ▼ +user: null +credentials: array:4 [▶] } "Illuminate\Auth\Events\Failed" array:1 [▼ 0 => Failed {#21 ▶} ] -----进入函数开始截获------------------------------------------------ RequestHandled {#347 ▼ +request: Request {#42 ▶} +response: JsonResponse {#894 ▶} } "Illuminate\Foundation\Http\Events\RequestHandled" array:1 [▼ 0 => RequestHandled {#347 ▶} ]
很明显这里不会broadcast。所以获得事件与参数后,接下来就要获取监听类,
public function getListeners($eventName) { $listeners = $this->listeners[$eventName] ?? []; $listeners = array_merge( $listeners, $this->getWildcardListeners($eventName) ); return class_exists($eventName, false) ? $this->addInterfaceListeners($eventName, $listeners) : $listeners; }
我用的都是laravel默认的配置,这种情况下,只发现QueryExecuted有监听器,其它都没有,如下
"Illuminate\Database\Events\QueryExecuted" array:1 [▼ 0 => Closure {#219 ▼ class: "Illuminate\Events\Dispatcher" this: Dispatcher {#26 …} parameters: {▶} use: {▶} file: "D:\wamp64\www\laravel\laraveldb\vendor\laravel\framework\src\Illuminate\Events\Dispatcher.php" line: "353 to 359" } ] ------------------------------------------ "Illuminate\Auth\Events\Failed" [] ------------------------------------------ "Illuminate\Foundation\Http\Events\RequestHandled" []
顺便说一下,寻找监听类的时候,也要从通配符监听器中寻找,
protected function getWildcardListeners($eventName) { $wildcards = []; foreach ($this->wildcards as $key => $listeners) { if (Str::is($key, $eventName)) { $wildcards = array_merge($wildcards, $listeners); } } return $wildcards; }
当然,这取决于你使用时的设置,再顺便理解一下通配符监听器吧!
通配符事件监听器
你可以使用通配符
*来注册监听器,这样就可以通过同一个监听器捕获多个事件。通配符监听器接收整个事件数据数组作为参数:
$events->listen('event.*', function (array $data) { // });
参考:http://laravelacademy.org/post/6046.html
刨根问底,我也非常想知道上面那个QueryExecuted的listener是什么?
"Illuminate\Database\Events\QueryExecuted" Closure {#177 ▼ class: "Barryvdh\Debugbar\LaravelDebugbar" this: LaravelDebugbar {#151 …} parameters: {▶} use: {▶} file: "D:\wamp64\www\laravel\laraveldb\vendor\barryvdh\laravel-debugbar\src\LaravelDebugbar.php" line: "321 to 336" }
原来不是laravel默认的监听,而是我另外安装的laravel的调试类barryvdh\laravel-debugbar。
到此明白,这些事件在默认的情况下,都是没有设置监听类的。
附:laravel自带的Auth路由参考
如果使用的是laravel默认的Auth路由,那么就是下面的情况,可以根据需要选择。
Illuminate\Routing\Router
public function auth() { // Authentication Routes... $this->get('login', 'Auth\LoginController@showLoginForm')->name('login'); $this->post('login', 'Auth\LoginController@login'); $this->post('logout', 'Auth\LoginController@logout')->name('logout'); // Registration Routes... $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); $this->post('register', 'Auth\RegisterController@register'); // Password Reset Routes... $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request'); $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email'); $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset'); $this->post('password/reset', 'Auth\ResetPasswordController@reset'); }
App\Http\Controllers\Auth\LonginController里面几乎没有有用的信息
class LoginController extends Controller { use AuthenticatesUsers;
其父类App\Http\Controllers\Controller 继承了 Illuminate\Routing\Controller
<?php namespace App\Http\Controllers; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; }
在Illuminate\Foundation\Auth\AuthenticatesUsers.php:
public function showLoginForm() { return view('auth.login'); }
相关文章推荐
- Laravel5.5源码详解 -- 数据库的启动与连接过程
- Laravel5.5源码详解 -- 中间件MiddleWare分析
- 一次数据库执行结果慢的梳理过程
- 数据库中间件 MyCAT 源码分析 —— 【单库单表】查询
- 问题:mybatis查询无结果集,但数据库执行相同的sql有结果集
- laravel5.5前后台登录认证实现过程详解
- 记一次SQL Server2005导入Oracle10G的折腾过程【供多种数据库导入导出数据的C#程序源码参考】
- 数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(三)之查询SQL
- 【Spring MVC】DispatcherServlet详解(容器初始化超详细过程源码分析)
- 数据库中间件 MyCAT 源码分析 —— 【单库单表】查询
- 小觑数据库(SqlServer)查询语句执行过程
- Mybatis底层原理学习(二):从源码角度分析一次查询操作过程
- Laravel 5.3 auth中间件底层实现详解
- 数据库中间件 MyCAT 源码分析 —— 【单库单表】查询
- 记一次SQL Server2005导入Oracle10G的折腾过程【供多种数据库导入导出数据的C#程序源码参考】
- 数据库中间件 MyCAT 源码解析 —— 分片结果合并(一)
- 数据库中间件 MyCAT 源码解析 —— 分片结果合并(一)
- 数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(三)之查询SQL解析
- 数据库中间件 MyCAT 源码分析 —— 【单库单表】查询
- Maven命令详解 模块导入 MyEclipse + Maven开发Web工程的详细配置过程