让 Laravel 优雅地创建 MySQL 全文索引

分享 coodeer ⋅ 于 2017-01-09 02:22:41 ⋅ 最后回复由 reatang 2017-01-23 15:09:02 ⋅ 1029 阅读

最近在浏览社区话题的时候,看到了一位同仁发表的一篇教程: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方法:

<?php

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时我们需要添加两个新的方法compileFulltextcompileDropFulltext,分别用于创建与删除全文索引,参考以下代码:

<?php

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

<?php

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);
});

至此,我们应该可以结束工作了,但事实不是这样的,如果你按照以下的方式来组织你的代码,你会发现,它并不能得到你预想的结果:

<?php

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:

<?php

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索引了:

<?php

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

  1. 这篇文章我们并不关注MySQL全文索引对中文的支持,所以请读者选对你的应用场景。
  2. 其实正如@wyg27 所写教程呈现的,一行代码就能解决的问题,为什么还要如此大费周章?仁者见仁,或许你能从这里学会如何按需拓展你的Laravel应用。
  3. 这篇帖子没有半点反驳@wyg27 的意思,他的方法已然是最快速有效的了,我只是在此和大家分享一种新思路而已,所以不喜勿喷,2333333
本帖已被设为精华帖!
本帖由 Summer 于 1月前 加精
回复数量: 3
  • wyg27
    2017-01-10 00:22:29

    受教了,完全没觉得你的文章是在喷我的方法。其实,我的方法只是因为懒惰,只求完成任务而不求甚解。如果能像你这样去动手改造框架,绝对是最好的办法,也是学习的应有态度。

  • coodeer PHPer
    2017-01-10 14:08:04

    @wyg27 主要还是得益于自由的Laravel,使得拓展如此方便。

  • reatang
    2017-01-23 15:09:02

    如果引擎不支持fulltext怎么办啊?

暂无评论~~
  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
Ctrl+Enter