Tudo bem então, eu escrevi uma migração para conseguir isso para o meu próprio sistema.
-
Ele permite que você especifique opcionalmente um nome de conexão para fazer referência a uma conexão diferente do padrão.
-
Ele obtém a lista de tabelas do banco de dados da conexão usando umSHOW TABLES
inquerir.
-
Em seguida, ele percorre cada tabela e atualiza todas as colunas do tipo string/caractere para o novo conjunto de caracteres e agrupamento.
-
Eu fiz isso para que um retorno de chamada deve ser fornecido para determinar se uma coluna deve ou não ter seu comprimento alterado para o novo comprimento fornecido. Na minha implementação,VARCHAR
eCHAR
colunas com comprimentos maiores que 191 são atualizadas para ter comprimento 191 durante a migração ascendente eVARCHAR
eCHAR
colunas com comprimento exatamente 191 são atualizadas para ter comprimento 255 na migração reversa/inferior.
-
Depois que todas as colunas de string/caracter forem atualizadas, algumas consultas serão executadas para alterar o conjunto de caracteres e o agrupamento da tabela, convertendo quaisquer agrupamentos restantes para o novo e, em seguida, para alterar o conjunto de caracteres padrão e o agrupamento da tabela.
-
Por fim, o conjunto de caracteres e o agrupamento padrão do banco de dados serão alterados.
Observações
-
Originalmente, tentei simplesmente converter as tabelas para a nova codificação, mas tive problemas com o comprimento das colunas. 191 caracteres é o comprimento máximo de caracteres emutf8mb4
ao usar o InnoDB na minha versão do MySQL/MariaDB e alterar o agrupamento da tabela resultou em um erro.
-
No começo, eu queria apenas atualizar os comprimentos para o novo comprimento, mas também queria fornecer um recurso de reversão, então isso não era uma opção porque no método reverso eu estaria definindo os comprimentos das colunas que eramutf8mb4
para 255, o que seria muito longo, então optei por alterar o agrupamento também.
-
Eu então tentei apenas alterar o comprimento, o conjunto de caracteres e o agrupamento devarchar
echar
colunas que eram muito longas, mas no meu sistema, isso resultava em erros quando eu tinha índices de várias colunas que incluíam essas colunas. Aparentemente, os índices de várias colunas devem usar o mesmo agrupamento.
-
Uma observação importante sobre isso é que a migração reversa/inferior não será 100% perfeita para todos. Eu não acho que seria possível fazer isso sem armazenar informações extras sobre as colunas originais ao migrar. Portanto, minha implementação atual para a migração reversa/inferior é assumir que as colunas com comprimento 191 eram originalmente 255.
-
Uma observação igualmente importante sobre isso é que isso mudará cegamente os agrupamentos de todas as colunas de string/caractere para o novo agrupamento, independentemente do agrupamento original, portanto, se houver colunas com agrupamentos diferentes, todos serão convertidos para o novo e o inverso fará o mesmo, os originais não serão preservados.
<?php
use Illuminate\Database\Migrations\Migration;
class UpgradeDatabaseToUtf8mb4 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$this->changeDatabaseCharacterSetAndCollation('utf8mb4', 'utf8mb4_unicode_ci', 191, function ($column) {
return $this->isStringTypeWithLength($column) && $column['type_brackets'] > 191;
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$this->changeDatabaseCharacterSetAndCollation('utf8', 'utf8_unicode_ci', 255, function ($column) {
return $this->isStringTypeWithLength($column) && $column['type_brackets'] == 191;
});
}
/**
* Change the database referred to by the connection (null is the default connection) to the provided character set
* (e.g. utf8mb4) and collation (e.g. utf8mb4_unicode_ci). It may be necessary to change the length of some fixed
* length columns such as char and varchar to work with the new encoding. In which case the new length of such
* columns and a callback to determine whether or not that particular column should be altered may be provided. If a
* connection other than the default connection is to be changed, the string referring to the connection may be
* provided as the last parameter (This string will be passed to DB::connection(...) to retrieve an instance of that
* connection).
*
* @param string $charset
* @param string $collation
* @param null|int $newColumnLength
* @param Closure|null $columnLengthCallback
* @param string|null $connection
*/
protected function changeDatabaseCharacterSetAndCollation($charset, $collation, $newColumnLength = null, $columnLengthCallback = null, $connection = null)
{
$tables = $this->getTables($connection);
foreach ($tables as $table) {
$this->updateColumnsInTable($table, $charset, $collation, $newColumnLength, $columnLengthCallback, $connection);
$this->convertTableCharacterSetAndCollation($table, $charset, $collation, $connection);
}
$this->alterDatabaseCharacterSetAndCollation($charset, $collation, $connection);
}
/**
* Get an instance of the database connection provided with an optional string referring to the connection. This
* should be null if referring to the default connection.
*
* @param string|null $connection
*
* @return \Illuminate\Database\Connection
*/
protected function getDatabaseConnection($connection = null)
{
return DB::connection($connection);
}
/**
* Get a list of tables on the provided connection.
*
* @param null $connection
*
* @return array
*/
protected function getTables($connection = null)
{
$tables = [];
$results = $this->getDatabaseConnection($connection)->select('SHOW TABLES');
foreach ($results as $result) {
foreach ($result as $key => $value) {
$tables[] = $value;
break;
}
}
return $tables;
}
/**
* Given a stdClass representing the column, extract the required information in a more accessible format. The array
* returned will contain the field name, the type of field (Without the length), the length where applicable (or
* null), true/false indicating the column allowing null values and the default value.
*
* @param stdClass $column
*
* @return array
*/
protected function extractInformationFromColumn($column)
{
$type = $column->Type;
$typeBrackets = null;
$typeEnd = null;
if (preg_match('/^([a-z]+)(?:\\(([^\\)]+?)\\))?(.*)/i', $type, $matches)) {
$type = strtolower(trim($matches[1]));
if (isset($matches[2])) {
$typeBrackets = trim($matches[2]);
}
if (isset($matches[3])) {
$typeEnd = trim($matches[3]);
}
}
return [
'field' => $column->Field,
'type' => $type,
'type_brackets' => $typeBrackets,
'type_end' => $typeEnd,
'null' => strtolower($column->Null) == 'yes',
'default' => $column->Default,
'charset' => is_string($column->Collation) && ($pos = strpos($column->Collation, '_')) !== false ? substr($column->Collation, 0, $pos) : null,
'collation' => $column->Collation
];
}
/**
* Tell if the provided column is a string/character type and needs to have it's charset/collation changed.
*
* @param string $column
*
* @return bool
*/
protected function isStringType($column)
{
return in_array(strtolower($column['type']), ['char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set']);
}
/**
* Tell if the provided column is a string/character type with a length.
*
* @param string $column
*
* @return bool
*/
protected function isStringTypeWithLength($column)
{
return in_array(strtolower($column['type']), ['char', 'varchar']);
}
/**
* Update all of the string/character columns in the database to be the new collation. Additionally, modify the
* lengths of those columns that have them to be the newLength provided, when the shouldUpdateLength callback passed
* returns true.
*
* @param string $table
* @param string $charset
* @param string $collation
* @param int|null $newLength
* @param Closure|null $shouldUpdateLength
* @param string|null $connection
*/
protected function updateColumnsInTable($table, $charset, $collation, $newLength = null, Closure $shouldUpdateLength = null, $connection = null)
{
$columnsToChange = [];
foreach ($this->getColumnsFromTable($table, $connection) as $column) {
$column = $this->extractInformationFromColumn($column);
if ($this->isStringType($column)) {
$sql = "CHANGE `%field%` `%field%` %type%%brackets% CHARACTER SET %charset% COLLATE %collation% %null% %default%";
$search = ['%field%', '%type%', '%brackets%', '%charset%', '%collation%', '%null%', '%default%'];
$replace = [
$column['field'],
$column['type'],
$column['type_brackets'] ? '(' . $column['type_brackets'] . ')' : '',
$charset,
$collation,
$column['null'] ? 'NULL' : 'NOT NULL',
is_null($column['default']) ? ($column['null'] ? 'DEFAULT NULL' : '') : 'DEFAULT \'' . $column['default'] . '\''
];
if ($this->isStringTypeWithLength($column) && $shouldUpdateLength($column) && is_int($newLength) && $newLength > 0) {
$replace[2] = '(' . $newLength . ')';
}
$columnsToChange[] = trim(str_replace($search, $replace, $sql));
}
}
if (count($columnsToChange) > 0) {
$query = "ALTER TABLE `{$table}` " . implode(', ', $columnsToChange);
$this->getDatabaseConnection($connection)->update($query);
}
}
/**
* Get a list of all the columns for the provided table. Returns an array of stdClass objects.
*
* @param string $table
* @param string|null $connection
*
* @return array
*/
protected function getColumnsFromTable($table, $connection = null)
{
return $this->getDatabaseConnection($connection)->select('SHOW FULL COLUMNS FROM ' . $table);
}
/**
* Convert a table's character set and collation.
*
* @param string $table
* @param string $charset
* @param string $collation
* @param string|null $connection
*/
protected function convertTableCharacterSetAndCollation($table, $charset, $collation, $connection = null)
{
$query = "ALTER TABLE {$table} CONVERT TO CHARACTER SET {$charset} COLLATE {$collation}";
$this->getDatabaseConnection($connection)->update($query);
$query = "ALTER TABLE {$table} DEFAULT CHARACTER SET {$charset} COLLATE {$collation}";
$this->getDatabaseConnection($connection)->update($query);
}
/**
* Change the entire database's (The database represented by the connection) character set and collation.
*
* # Note: This must be done with the unprepared method, as PDO complains that the ALTER DATABASE command is not yet
* supported as a prepared statement.
*
* @param string $charset
* @param string $collation
* @param string|null $connection
*/
protected function alterDatabaseCharacterSetAndCollation($charset, $collation, $connection = null)
{
$database = $this->getDatabaseConnection($connection)->getDatabaseName();
$query = "ALTER DATABASE {$database} CHARACTER SET {$charset} COLLATE {$collation}";
$this->getDatabaseConnection($connection)->unprepared($query);
}
}
Por favor, faça backup de seu banco de dados antes de executar isso . Use por sua conta e risco!