【问题标题】:Changing Laravel MYSQL to utf8mb4 for Emoji Support in Existing Database将 Laravel MYSQL 更改为 utf8mb4 以在现有数据库中支持表情符号
【发布时间】:2017-08-17 14:47:23
【问题描述】:

我正在使用 Laravel 5.3,并且我已经设置了我的生产服务器。所有数据库迁移均已使用以下数据库配置创建:

'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

但是现在,我的一些用户报告说,他们在尝试保存包含表情符号的表单时遇到错误????其中的图标。搜索后我发现我需要将 mysql 字符集设置为 utf8mb4 才能正常工作,所以我的配置应该是这样的:

'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

由于这是在生产服务器中,我不能这样做migrate:refresh。所以我的问题是:

  1. 如何更改我使用 laravel 迁移创建的现有数据库以使用 utf8mb4 而不是 utf8 并同时更新 laravel?有没有更简单的方法来做到这一点?
  2. 如果上述情况可行,我最好为所有表格设置utf8mb4,还是仅将其用于我将真正使用表情符号的 2 个表格列。

感谢您的帮助。

【问题讨论】:

标签: mysql laravel laravel-5.3 emoji laravel-migrations


【解决方案1】:
  1. 使用原始mysql查询编写更新表迁移脚本并运行php artisan migrate命令

     use Illuminate\Database\Migrations\Migration;
    
     class UpdateTableCharset extends Migration {
    
         /**
          * Run the migrations.
          *
          * @return void
          */
         public function up() {
                 DB::unprepared('ALTER TABLE `table_name` CONVERT TO CHARACTER SET utf8mb4');
         }
    
         /**
          * Reverse the migrations.
          *
          * @return void
          */
         public function down() {
                 DB::unprepared('ALTER TABLE `table_name` CONVERT TO CHARACTER SET utf8');
         }
     }
    
  2. 我的个人喜好,更新表格。我没有证据表明它更好

注意:您仍然必须将数据库配置保留为utf8mb4

【讨论】:

    【解决方案2】:

    基于@insomniak-dev 答案,但仅当它们超过限制并在索引中使用时才会缩小 varchar。否则,它们将被转换,但大小保持不变。如果一列将被缩小,它会检查是否有任何数据将被截断。

    这还处理所有文本类型并将每个表的所有转换批处理为单个语句以提高速度。

    Dryrun 标志输出可用的 sql,而不是直接应用。

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        $dryRun = true;
        $this->convertDb('mysql', 'utf8mb4', 'utf8mb4_unicode_ci', $dryRun);
        $this->convertDb('archive', 'utf8mb4', 'utf8mb4_unicode_ci', $dryRun);
    }
    
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        $dryRun = true;
        $this->convertDb('archive', 'utf8', 'utf8_unicode_ci', $dryRun);
        $this->convertDb('mysql', 'utf8', 'utf8_unicode_ci', $dryRun);
    }
    
    private function convertDb($connection, $charset, $collate, $dryRun)
    {
        $dbName = config("database.connections.{$connection}.database");
    
        $varchars = \DB::connection($connection)
            ->select(\DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE = 'varchar' and (CHARACTER_SET_NAME != '{$charset}' or COLLATION_NAME != '{$collate}') AND TABLE_SCHEMA = '{$dbName}'"));
        // Check if shrinking field size will truncate!
        $skip = [];  // List of table.column that will be handled manually
        $indexed = [];
        if ($charset == 'utf8mb4') {
            $error = false;
            foreach($varchars as $t) {
                if ($t->CHARACTER_MAXIMUM_LENGTH > 191) {
                    $key = "{$t->TABLE_NAME}.{$t->COLUMN_NAME}";
    
                    // Check if column is indexed
                    $index = \DB::connection($connection)
                        ->select(\DB::raw("SHOW INDEX FROM `{$t->TABLE_NAME}` where column_name = '{$t->COLUMN_NAME}'"));
                    $indexed[$key] = count($index) ? true : false;
    
                    if (count($index)) {
                        $result = \DB::connection($connection)
                            ->select(\DB::raw("select count(*) as `count` from `{$t->TABLE_NAME}` where length(`{$t->COLUMN_NAME}`) > 191"));
                        if ($result[0]->count > 0) {
                            echo "-- DATA TRUNCATION: {$t->TABLE_NAME}.{$t->COLUMN_NAME}({$t->CHARACTER_MAXIMUM_LENGTH}) => {$result[0]->count}" . PHP_EOL;
                            if (!in_array($key, $skip)) {
                                $error = true;
                            }
                        }
                    }
                }
            }
            if ($error) {
                throw new \Exception('Aborting due to data truncation');
            }
        }
    
        $query = "SET FOREIGN_KEY_CHECKS = 0";
        $this->dbExec($query, $dryRun, $connection);
    
        $query = "ALTER SCHEMA {$dbName} DEFAULT CHARACTER SET {$charset} DEFAULT COLLATE {$collate}";
        $this->dbExec($query, $dryRun, $connection);
    
        $tableChanges = [];
        foreach($varchars as $t) {
            $key = "{$t->TABLE_NAME}.{$t->COLUMN_NAME}";
            if (!in_array($key, $skip)) {
                if ($charset == 'utf8mb4' && $t->CHARACTER_MAXIMUM_LENGTH > 191 && $indexed["{$t->TABLE_NAME}.{$t->COLUMN_NAME}"]) {
                    $tableChanges["{$t->TABLE_NAME}"][] = "CHANGE `{$t->COLUMN_NAME}` `{$t->COLUMN_NAME}` VARCHAR(191) CHARACTER SET {$charset} COLLATE {$collate}";
                    echo "-- Shrinking: {$t->TABLE_NAME}.{$t->COLUMN_NAME}({$t->CHARACTER_MAXIMUM_LENGTH})" . PHP_EOL;
                } else if ($charset == 'utf8' && $t->CHARACTER_MAXIMUM_LENGTH == 191) {
                    $tableChanges["{$t->TABLE_NAME}"][] = "CHANGE `{$t->COLUMN_NAME}` `{$t->COLUMN_NAME}` VARCHAR(255) CHARACTER SET {$charset} COLLATE {$collate}";
                    echo "-- Expanding: {$t->TABLE_NAME}.{$t->COLUMN_NAME}({$t->CHARACTER_MAXIMUM_LENGTH})";
                } else {
                    $tableChanges["{$t->TABLE_NAME}"][] = "CHANGE `{$t->COLUMN_NAME}` `{$t->COLUMN_NAME}` VARCHAR({$t->CHARACTER_MAXIMUM_LENGTH}) CHARACTER SET {$charset} COLLATE {$collate}";
                }
            }
        }
    
        $texts = \DB::connection($connection)
            ->select(\DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE like '%text%' and (CHARACTER_SET_NAME != '{$charset}' or COLLATION_NAME != '{$collate}') AND TABLE_SCHEMA = '{$dbName}'"));
        foreach($texts as $t) {
            $tableChanges["{$t->TABLE_NAME}"][] = "CHANGE `{$t->COLUMN_NAME}` `{$t->COLUMN_NAME}` {$t->DATA_TYPE} CHARACTER SET {$charset} COLLATE {$collate}";
        }
    
        $tables = \DB::connection($connection)
            ->select(\DB::raw("select * from INFORMATION_SCHEMA.TABLES where TABLE_COLLATION != '{$collate}' and TABLE_SCHEMA = '{$dbName}';"));
        foreach($tables as $t) {
            $tableChanges["{$t->TABLE_NAME}"][] = "CONVERT TO CHARACTER SET {$charset} COLLATE {$collate}";
            $tableChanges["{$t->TABLE_NAME}"][] = "DEFAULT CHARACTER SET={$charset} COLLATE={$collate}";
        }
    
        foreach ($tableChanges as $table => $changes) {
            $query = "ALTER TABLE `{$table}` ".implode(",\n", $changes);
            $this->dbExec($query, $dryRun, $connection);
        }
    
        $query = "SET FOREIGN_KEY_CHECKS = 1";
        $this->dbExec($query, $dryRun, $connection);
    
        echo "-- {$dbName} CONVERTED TO {$charset}-{$collate}" . PHP_EOL;
    }
    
    private function dbExec($query, $dryRun, $connection)
    {
        if ($dryRun) {
            echo $query . ';' . PHP_EOL;
        } else {
            \DB::connection($connection)->getPdo()->exec($query);
        }
    }
    

    【讨论】:

    • 这对我来说很有效,但在获得General error: 1347 'table_name' is not BASE TABLE 后,我有一些视图需要排除。您可以将列转换更改为包括INNER JOIN INFORMATION_SCHEMA.TABLES t ON c.TABLE_CATALOG = t.TABLE_CATALOG AND c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME AND t.TABLE_TYPE = 'BASE TABLE',并将表转换更改为包括WHERE TABLE_TYPE = 'BASE_TABLE'
    【解决方案3】:

    对于任何愿意在整个数据库上实现这一点的人,我发现自己需要这样一个脚本:

    <?php
    
    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Config;
    
    class ChangeDbCharset extends Migration
    {
    
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            $charset = "utf8mb4";
            $collate = $charset."_unicode_ci";
            $dbName = Config::get('database.connections.'.Config::get('database.default').'.database');
            $query = "ALTER SCHEMA $dbName DEFAULT CHARACTER SET $charset DEFAULT COLLATE $collate;\n"; 
            DB::connection()->getPdo()->exec($query);
    
            $dbName = Config::get('database.connections.'.Config::get('database.default').'.database');
            $result = DB::select(DB::raw('show tables'));
            $test = DB::select(DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE = 'varchar' AND TABLE_SCHEMA = '$dbName';"));
            //var_dump($test);
            foreach($test as $t)
            {
                $query = "ALTER TABLE $t->TABLE_NAME CHANGE $t->COLUMN_NAME $t->COLUMN_NAME VARCHAR(191) CHARACTER SET $charset COLLATE $collate; \n";
                echo $query;
                DB::connection()->getPdo()->exec($query);
            }
            $test = DB::select(DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE = 'text' AND TABLE_SCHEMA = '$dbName';"));
            foreach($test as $t)
            {
                $query = "ALTER TABLE $t->TABLE_NAME CHANGE $t->COLUMN_NAME $t->COLUMN_NAME TEXT CHARACTER SET $charset COLLATE $collate; \n";
                echo $query;
                DB::connection()->getPdo()->exec($query);
            }
    
    
            $result = DB::select(DB::raw('show tables'));
            foreach($result as $r)
            {
                foreach($r as $k => $t)
                {
                    $query = "ALTER TABLE `$t` CONVERT TO CHARACTER SET $charset COLLATE $collate; \n";
                    echo $query;
                    DB::connection()->getPdo()->exec($query);
                }
            }
            echo "DB CHARSET set to $charset , $collate";
        }
    
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            $charset = "utf8";
            $collate = $charset."_unicode_ci";
            $dbName = Config::get('database.connections.'.Config::get('database.default').'.database');
            $query = "ALTER SCHEMA $dbName DEFAULT CHARACTER SET $charset DEFAULT COLLATE $collate;\n"; 
            DB::connection()->getPdo()->exec($query);
    
            $dbName = Config::get('database.connections.'.Config::get('database.default').'.database');
            $result = DB::select(DB::raw('show tables'));
            $test = DB::select(DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE = 'varchar' AND TABLE_SCHEMA = '$dbName';"));
            //var_dump($test);
            foreach($test as $t)
            {
                $query = "ALTER TABLE $t->TABLE_NAME CHANGE $t->COLUMN_NAME $t->COLUMN_NAME VARCHAR(255) CHARACTER SET $charset COLLATE $collate; \n";
                echo $query;
                DB::connection()->getPdo()->exec($query);
            }
            $test = DB::select(DB::raw("select * from INFORMATION_SCHEMA.COLUMNS where DATA_TYPE = 'text' AND TABLE_SCHEMA = '$dbName';"));
            foreach($test as $t)
            {
                $query = "ALTER TABLE $t->TABLE_NAME CHANGE $t->COLUMN_NAME $t->COLUMN_NAME TEXT CHARACTER SET $charset COLLATE $collate; \n";
                echo $query;
                DB::connection()->getPdo()->exec($query);
            }
    
    
            $result = DB::select(DB::raw('show tables'));
            foreach($result as $r)
            {
                foreach($r as $k => $t)
                {
                    $query = "ALTER TABLE `$t` CONVERT TO CHARACTER SET $charset COLLATE $collate; \n";
                    echo $query;
                    DB::connection()->getPdo()->exec($query);
                }
            }
            echo "DB CHARSET set to $charset , $collate";
        }
    
    }
    

    【讨论】:

    • 使用 VARCHAR($t->CHARACTER_MAXIMUM_LENGTH) 代替 VARCHAR(191) 似乎更好
    猜你喜欢
    • 2016-11-04
    • 2018-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-24
    • 2019-11-02
    • 2016-12-06
    相关资源
    最近更新 更多