用 Laravel 迁移文件添加表注释的一种方法

首先请记住,我仅在 Laravel 8 下对 MySQL 做过测试。

使用 Laravel 的迁移,给表添加注释是个麻烦事儿,要么使用原生 SQL,要么再用数据库管理工具手动添加, 我一直没有找到更好或更方便的方式,网络中流传的方式也都是使用原生 SQL:

  \DB::statement("ALTER TABLE `table_name` comment '表注释'");

这个方式不是不可,但是不够灵活,我一般喜欢添加表前缀,这时候就会变得有点棘手,那样的代码写起来不仅很累,而且似乎也不太像程序员会干的事。

今天我又被这个问题所困扰,因为我又要开始建表啦,很多表。想着 Laravel 的版本已经到 8 了,是不是已经带这个功能呢?于是又开始一番搜索,然并卵,结果都还是上面那个原生 SQL 方案,似乎大家也已经放弃了。好像有个扩展包有提供这个能力,但是因为这点事就要加个包,是不是有点小题大做。顺便说一句,如果你找到了不一样的方法,记得通知我一声。

同时透过网络中的一些讨论,发现 Laravel 团队貌似也并不想把这个功能加进来。可是在我一番探索后,以一个简便的方式实现其实也不难,我不知道把这个功能加到框架里会有什么不妥,也许是我的知识不够吧。

那么,直接来看看结果吧:


  Blueprint::macro('comment', function ($comment) {
      return $this->addCommand('commentTable', compact('comment'));
  });

  Grammar::macro('compileCommentTable', function (Blueprint $blueprint, Fluent $command, Connection $connection) {
      return 'alter table ' . $this->wrapTable($blueprint) . $this->modifyComment($blueprint, $command);
  });

这就是我的方案的核心代码,原理就是利用 macroable,再使用表注释方法之前添加自定义方法到关键类中。主要做两件事,一是给 Blueprint 增加个给表添加注释的方法,以便我们在迁移文件中像其他方法一样使用,这里我的方法名叫 comment;其二是给 Grammar 增加个编译表添加注释的命令方法,用于解析出 SQL 语句。需要注意的是,我这里针对的是 MySQL 数据库,其它数据库的语句会不同,组织语句的方式也有些不同。

肯定还有其他的方法,我也不知道我这个方法会不会有什么奇怪的 bug,而且我这个方法有个问题,我不知道把这个步骤放在哪个地方。我能想到的一种是在 AppServiceProvider 中,但是这个功能仅在做迁移的时候才需要,感觉不太合适;另一个是监听迁移事件,在事件开始前添加这些方法,但是这又是全局范围的,也似乎不妥;还有一种就是自定义 Migration 类,在构造函数中进行,然后让迁移类继承该类。显而易见,我使用的是自定义类。

下面我假设你对迁移代码执行过程不了解,其实我也不了解,我来针对我这个方法简单解释下为什么这样就可以实现给表添加注释,也算是我的一个思路。记住,途径不止这一种,虽然我使用 Laravel 很长时间了,但是框架内部的事,我还不够清楚,一些回调搞得我晕头转向,但我还是探索出了这样一条路。

给表添加注释,可以在创建表的时候,也可以在创建表之后。显然后者更通用,因为它同时解决了后期修改表注释的能力。所以把这个功能进行单独考虑,首先我通过查看其他单个指令来确定我需要做什么,比如,删除表:

  Schema::dropIfExists('users');

其内部是这样的:

  // \Illuminate\Database\Schema\Builder
  /**
   * Drop a table from the schema if it exists.
   *
   * @param  string  $table
   * @return void
   */
  public function dropIfExists($table)
  {
      $this->build(tap($this->createBlueprint($table), function ($blueprint) {
          $blueprint->dropIfExists();
      }));
  }

有看到它调用的是 BlueprintdropIfExists 方法吗? tap 方法会返回 Buleprint 的实例,再转交给 build,在那里面会进行 SQL 解析和执行。但是我们先看看 Blueprint 中有干什么事。

  // Illuminate\Database\Schema\Blueprint
  /**
   * Indicate that the table should be dropped if it exists.
   *
   * @return \Illuminate\Support\Fluent
   */
  public function dropIfExists()
  {
      return $this->addCommand('dropIfExists');
  }

看样子只是添加了一个命令,而 addCommand 方法是这样的:

  /**
   * Add a new command to the blueprint.
   *
   * @param  string  $name
   * @param  array  $parameters
   * @return \Illuminate\Support\Fluent
   */
  protected function addCommand($name, array $parameters = [])
  {
      $this->commands[] = $command = $this->createCommand($name, $parameters);

      return $command;
  }

再多看一眼

  /**
   * Create a new Fluent command.
   *
   * @param  string  $name
   * @param  array  $parameters
   * @return \Illuminate\Support\Fluent
   */
  protected function createCommand($name, array $parameters = [])
  {
      return new Fluent(array_merge(compact('name'), $parameters));
  }

Blueprint 干的事就这些,一切都平平无奇。看到这里是不是该想到什么,如果我要增加给表添加注释的方法,好像只需要加个类似 dropIfExists 的方法到 Blueprint 即可。但是以何种方式呢?继承吧啦吧啦之类的搞起来实在是太麻烦。如果你有打开过 Blueprint 类,你可能有注意到 use Macroable; 这行代码。于是我在迁移文件中直接尝试了下:

Blueprint::macro('comment', function ($comment) {
    return $this->addCommand('comment', compact('comment'));
});

这样是可以把方法添加到 Blueprint 实例中,那么该看看是怎么执行 SQL 的。

Illuminate\Database\Schema\Build 中查看 build 方法

  /**
   * Execute the blueprint to build / modify the table.
   *
   * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
   * @return void
   */
  protected function build(Blueprint $blueprint)
  {
      $blueprint->build($this->connection, $this->grammar);
  }

哈,又到 Blueprint 中去了!

  /**
   * Execute the blueprint against the database.
   *
   * @param  \Illuminate\Database\Connection  $connection
   * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
   * @return void
   */
  public function build(Connection $connection, Grammar $grammar)
  {
      foreach ($this->toSql($connection, $grammar) as $statement) {
          $connection->statement($statement);
      }
  }

显而易见,关键点落到 toSql 方法了,有了 sql 语句,自然就能实现愿望。

  /**
   * Get the raw SQL statements for the blueprint.
   *
   * @param  \Illuminate\Database\Connection  $connection
   * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
   * @return array
   */
  public function toSql(Connection $connection, Grammar $grammar)
  {
      $this->addImpliedCommands($grammar);

      $statements = [];

      // Each type of command has a corresponding compiler function on the schema
      // grammar which is used to build the necessary SQL statements to build
      // the blueprint element, so we'll just call that compilers function.
      $this->ensureCommandsAreValid($connection);

      foreach ($this->commands as $command) {
          $method = 'compile'.ucfirst($command->name);

          if (method_exists($grammar, $method) || $grammar::hasMacro($method)) {
              if (! is_null($sql = $grammar->$method($this, $command, $connection))) {
                  $statements = array_merge($statements, (array) $sql);
              }
          }
      }

      return $statements;
  }

这个方法有点长,但要关心的只有 $sql = $grammar->$method($this, $command, $connection))。可 Grammar 又是什么鬼?根据接收返回值的变量名,可以猜到应该是做 SQL 语句编译的。对于 dropIfExists 的编译方法应该是 compileDropIfExists,直接搜一下,框架支持的每个数据库系统的 Grammar 都有这个方法,我使用的是 MySQL,所以仅查看 MySQL 相关的:

  // Illuminate\Database\Schema\Grammars\MySqlGrammar
  /**
   * Compile a drop table (if exists) command.
   *
   * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
   * @param  \Illuminate\Support\Fluent  $command
   * @return string
   */
  public function compileDropIfExists(Blueprint $blueprint, Fluent $command)
  {
      return 'drop table if exists '.$this->wrapTable($blueprint);
  }

终于见到 SQL 语句啦!到这里,事情就很清晰了,只需要向 Grammar 中添加个类似 compileComment 的方法就可以实现给表添加注释的能力。在所有 Grammar 类的基类 Illuminate\Database\Grammar 中,我同样发现 use Macroable; ,所以可以使用 macro。不过,为了避免与框架的方法产生不必要的冲突,我对方法名做了一点小调整:

  \Illuminate\Database\Schema\Blueprint::macro('comment', function ($comment) {
      return $this->addCommand('commentTable', compact('comment'));
  });

  \Illuminate\Database\Schema\Grammars\Grammar::macro('compileCommentTable', function (Blueprint $blueprint, Fluent $command, Connection $connection) {
      return "alter table table_name comment '这是注释'";
  });

这和开头的内容很很相似,我测试可以执行到最终的 SQL,当然这个样子会报错,而且不优雅,需要做下优化,于是我找到一些方法并利用,加上一些其他的考虑,最终的样子其实是这样的:

<?php

namespace App\Support\Database;

use Exception;
use Illuminate\Support\Fluent;
use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\Grammar;
use Illuminate\Database\Migrations\Migration as AbstractMigration;

class Migration extends AbstractMigration
{
    public function __construct()
    {
        $this->addCommentTableMethod();
    }

    protected function addCommentTableMethod()
    {
        Blueprint::macro('comment', function ($comment) {
            if (!Grammar::hasMacro('compileCommentTable')) {
                Grammar::macro('compileCommentTable', function (Blueprint $blueprint, Fluent $command, Connection $connection) {
                    switch ($database_driver = $connection->getDriverName()) {
                        case 'mysql':
                            return 'alter table ' . $this->wrapTable($blueprint) . $this->modifyComment($blueprint, $command);
                        case 'pgsql':
                            return sprintf(
                                'comment on table %s is %s',
                                $this->wrapTable($blueprint),
                                "'" . str_replace("'", "''", $command->comment) . "'"
                            );
                        case 'sqlserver':
                        case 'sqlite':
                        default:
                            throw new Exception("The {$database_driver} not support table comment.");
                    }
                });
            }

            return $this->addCommand('commentTable', compact('comment'));
        });
    }
}

我给出了 MySQL 和 PostgreSQL 的实现, 我对 SQL Server 不熟,而且它的语句太难写,没有给出示例,而 sqlite 我就更不清楚了。如果只使用明确的数据库,可以去掉其他的内容。需要用到表注释的迁移继承这个类就行(也可以使用其他方式),这不会影响到其他的地方。看个示例:

<?php

use App\Support\Database\Migration;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name', 30)->comment('名称');

            $table->comment('用户表');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

执行结果:

CREATE TABLE `db_users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

还可以做到像 dropIfExists 方法一样直接在 Schema 上调用,比如 Schema::comment('users', '用户表');,但我觉得目前实现的已经足够用。


最后,如果你有其他更好的方法,记得通知我。

首发于个人站:youlinkin.com/posts/11-a-method-of...

本作品采用《CC 协议》,转载必须注明作者和本文链接
? 我的导航网站已经可以公开使用啦:Cootab
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 5

有一个现成可以使用的 github.com/alicfeng/laravel-migrat... 😁

3年前 评论
AlicFeng (作者) 3年前
chuoke (楼主) 3年前

如果官方支持了 也请踢我一下 谢谢

2年前 评论
chuoke (楼主) 2年前

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