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

最近在浏览社区话题的时候,看到了一位同仁发表的一篇教程: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
我是黄毅,欢迎关注我的 Github博客
本帖已被设为精华帖!
本帖由 Summer 于 7年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 3

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

7年前 评论

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

7年前 评论

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

7年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!