最近在浏览社区话题的时候,看到了一位同仁发表的一篇教程:Laravel 5.3 下通过 migrate 添加 “全文索引” 的方法,突然想到自己之前也研究过这一话题,所以今天就和大家分享一下我的实现思路:如何更优雅地创建fulltext索引。
我非常喜欢Laravel框架的原因之一就是它的拓展性简直太赞了,所有框架本身未实现的功能你都可以自定义实现,而且可以实现得非常优雅。
其实,要想让添加全文索引这个动作变得更“优雅”一点,不外乎为Blueprint类添加一个fulltext方法,操作起来就像这样$table->fulltext('column');,这就与我们平时为字段添加unique索引一样方便了。我们先来看看预想效果:
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->fulltext(['title', 'content']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropFulltext(['title', 'content']);
}
Step 1 拓展Blueprint类
为了实现上述所预想的效果,显然我们要对Laravel的Illuminate\Database\Schema\Blueprint类进行拓展。这里所说的拓展,实际就是在继承原Blueprint的前提下定义一个新的Blueprint,并且在新的Blueprint类里面添加我们想要拓展的fulltext方法:
namespace Vendor\Package;
use Illuminate\Database\Schema\Blueprint as BaseBlueprint;
class Blueprint extends BaseBlueprint
{
/**
* Specify a fulltext index for the table.
*
* @param string|array $columns
* @param string $name
* @return \Illuminate\Support\Fluent
*/
public function fulltext($columns, $name = null)
{
return $this->indexCommand('fulltext', $columns, $name);
}
/**
* Indicate that the given fulltext key should be dropped.
*
* @param string|array $index
* @return \Illuminate\Support\Fluent
*/
public function dropFulltext($index)
{
return $this->dropIndexCommand('dropFulltext', 'fulltext', $index);
}
}
Step 2 拓展MySqlGrammar类
光实现一个拓展好的Blueprint肯定还不行,我们追踪一下Laravel的源码就能知道,它的底层是通过Illuminate\Database\Schema\Grammars\Grammar来编译SQL语法的,相应地MySQL的语法编译则由Illuminate\Database\Schema\Grammars\MySqlGrammar完成,因此我们还需要拓展一下MySqlGrammar,否则系统就会因为找不到编译fulltext索引的方法,而导致创建索引不成功。拓展MySqlGrammar时我们需要添加两个新的方法compileFulltext和compileDropFulltext,分别用于创建与删除全文索引,参考以下代码:
namespace Vendor\Package;
use Illuminate\Support\Fluent;
use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseMySqlGrammar;
class MySqlGrammar extends BaseMySqlGrammar
{
/**
* Compile a fulltext key command.
*
* @param \Vendor\Package\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileFulltext(Blueprint $blueprint, Fluent $command)
{
return $this->compileKey($blueprint, $command, 'fulltext');
}
/**
* Compile a drop unique key command.
*
* @param \Vendor\Package\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropFulltext(Blueprint $blueprint, Fluent $command)
{
$table = $this->wrapTable($blueprint);
$index = $this->wrap($command->index);
return "alter table {$table} drop index {$index}";
}
}
Step 3 注入新的MySqlGrammar
实现了以上两个拓展,我们的准备工作就已完毕,接下来就要将这两个拓展好的类注入到系统当中去。究竟如何注入。首先我们要来认识一下Illuminate\Support\Facades\Schema:
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Database\Schema\Builder
*/
class Schema extends Facade
{
/**
* Get a schema builder instance for a connection.
*
* @param string $name
* @return \Illuminate\Database\Schema\Builder
*/
public static function connection($name)
{
return static::$app['db']->connection($name)->getSchemaBuilder();
}
/**
* Get a schema builder instance for the default connection.
*
* @return \Illuminate\Database\Schema\Builder
*/
protected static function getFacadeAccessor()
{
return static::$app['db']->connection()->getSchemaBuilder();
}
}
不难看出,这个Facade实际上返回由数据库连接的getSchemaBuilder方法生成的Illuminate\Database\Schema\Builder实例,我们继续追踪Illuminate\Database\Connection类,找到getSchemaBuilder方法:
/**
* Get a schema builder instance for the connection.
*
* @return \Illuminate\Database\Schema\Builder
*/
public function getSchemaBuilder()
{
if (is_null($this->schemaGrammar)) {
$this->useDefaultSchemaGrammar();
}
return new SchemaBuilder($this);
}
可以看出,我们应该在Laravel实例化SchemaBuilder之前注入拓展好的MySqlGrammar,而伟大的作者早已为我们准备好了接入方法setSchemaGrammar,因此我们只要这样操作就能轻松地完成注入:
use Vendor\Package\MySqlGrammar;
app('db')->connection()->setSchemaGrammar(new MySqlGrammar);
Step 4 注入新的Blueprint
现在只剩Blueprint还没被注入了,继续追踪\Illuminate\Database\Schema\Builder,通读一遍源代码,我们可以发现,我们平时写migration文件时所用的table()、create()、drop()等方法都会调用同一个方法createBlueprint:
/**
* Create a new command set with a Closure.
*
* @param string $table
* @param \Closure|null $callback
* @return \Illuminate\Database\Schema\Blueprint
*/
protected function createBlueprint($table, Closure $callback = null)
{
if (isset($this->resolver)) {
return call_user_func($this->resolver, $table, $callback);
}
return new Blueprint($table, $callback);
}
它会先判断是否存在自定义的resolver,而伟大的作者也为我们提供了接入方法blueprintResolver,所以我们可以这样注入拓展好的Blueprint:
use Vendor\Package\Blueprint;
app('db')->connection()->getSchemaBuilder()->blueprintResolver(function ($table, $callback) {
return new Blueprint($table, $callback);
});
至此,我们应该可以结束工作了,但事实不是这样的,如果你按照以下的方式来组织你的代码,你会发现,它并不能得到你预想的结果:
use Vendor\Package\Blueprint;
use Vendor\Package\MySqlGrammar;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Migrations\Migration;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
app('db')->connection()->setSchemaGrammar(new MySqlGrammar);
app('db')->connection()->getSchemaBuilder()->blueprintResolver(new Blueprint);
Schema::table('posts', function (Blueprint $table) {
$table->fulltext(['title', 'content']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
app('db')->connection()->setSchemaGrammar(new MySqlGrammar);
app('db')->connection()->getSchemaBuilder()->blueprintResolver(new Blueprint);
Schema::dropFulltext(['title', 'content']);
}
}
具体原因是什么呢?这里保留一点想象空间,让大家自己去探寻一下结果,有想法的可以一起在回复区讨论。
Step 5 实现新的Schema Facade
这里我提供了一种解决方案,就是另行实现一个Facade:
namespace Vendor\Package;
use Illuminate\Support\Facades\Facade;
class Schema extends Facade
{
/**
* Get a schema builder instance for the default connection.
*
* @return \Illuminate\Database\Schema\Builder
*/
protected static function getFacadeAccessor()
{
$connection = static::$app['db']->connection();
$connection->setSchemaGrammar(new MySqlGrammar);
$schema = $connection->getSchemaBuilder();
$schema->blueprintResolver(function ($table, $callback) {
return new Blueprint($table, $callback);
});
return $schema;
}
}
这样,我们就可以优雅地添加fulltext索引了:
use Vendor\Package\Blueprint;
use Vendor\Package\Schema;
use Illuminate\Database\Migrations\Migration;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->fulltext(['title', 'content']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropFulltext(['title', 'content']);
}
}
整理好的拓展包
这是我个人整理的一个Package,有需求的可以直接使用,感兴趣的读者也可以下载源码继续研究:mysql-fulltext-laravel。
这篇文章我们并不关注MySQL全文索引对中文的支持,所以请读者选对你的应用场景。
其实正如@wyg27 所写教程呈现的,一行代码就能解决的问题,为什么还要如此大费周章?仁者见仁,或许你能从这里学会如何按需拓展你的Laravel应用。
这篇帖子没有半点反驳@wyg27 的意思,他的方法已然是最快速有效的了,我只是在此和大家分享一种新思路而已,所以不喜勿喷,2333333
我是黄毅,欢迎关注我的 Github 和 博客