4

在软件安装程序中,我需要自动创建一个 (My)SQL ALTER 脚本,刚刚给定一个处于未知状态的正在运行的数据库(例如数据结构版本 x.5)和几个完整的 DB (My)SQL CREATE 脚本(比如版本x.1 至 x.9)。

首先我需要找到当前正在运行的(或者如果可能是最接近的版本,可能某些安装有一些较早的更新错误,但这个功能是次要的)。然后我想创建一个 ALTER 脚本来修复正在运行的版本可能出现的错误。

之后我想自动为最新版本(x.9)创建一个 ALTER 脚本并应用这个脚本。再次比较两个版本并重复,直到版本是最新的。

我不能使用 GUI 应用程序,因为它必须在安装程序中盲目运行。目标平台是 Windows XP/7。分期付款的数量将在很长一段时间内低于 300(用于高度专业化的行业软件的更新软件)。所以我的问题是:

是否有任何用于 C++/NSIS/Some-Other-Installer-Frameworks 的好的 (My)SQL compare/diff/script-generation 库?

谢谢你的支持!

4

5 回答 5

3

很长一段时间以来我一直在思考同一个话题,但还没有找到一个像样的方法。我将分享我的工作,希望对您有所帮助。

我目前的方法是应用一个 SQL 查询列表,这些查询的设计方式适用于任何以前的数据库模式版本。如果命令已经被应用,那么它只会失败(例如添加字段或添加索引)。

这种方法限制了人们可以更改数据库模式的方式并且也容易出错 - 例如,如果错误地有查询将 ENUM(a,b) 字段扩展到 ENUM(a,b,c) 然后到 ENUM(a ,b,c,d)d如果您再次运行脚本,则现有的具有值的记录将被损坏。如果只有一个查询最新格式,这很容易解决。

我还在稍后添加了模式版本控制,目前使用一种简单但易于管理的更新文件格式 - 每行一个查询以;和额外的行分隔模式版本:

-- version 105

有了这个升级代码,可以大大简化并统一在一个处理所有版本转换的函数中。该函数只需处理--version <current version>行后的查询。到达-- version一行后,该函数会更新数据库内的模式版本。

此格式还允许使用 mysql -f mydb < myfile 命令进行手动处理。在这种情况下,版本行只是作为注释被忽略,所有更改的所有命令都将在当前架构上尝试 - 这可用于修复错误(错误我假设您的意思是比预期更旧的架构)。更新存储过程的代码也有类似的技巧:

drop procedure if exists procname;
delimiter //
create procedure procname ...
//
delimiter ;

在您询问数据库模式差异/补丁的问题中 - 这只能在添加新字段/索引/等的情况下进行概括。但不能自动处理重命名的字段或删除的字段。仅通过查看现有模式和新模式并保留现有数据(我假设现有数据必须保持完整),a自动化流程无法知道table1应该将字段重命名为。b

总结一下——在一般情况下,没有自动生成数据库模式更新脚本的方法。

于 2013-07-14T02:22:41.057 回答
2

有两种方法可以解决这个问题。

  1. 更改脚本旨在影响数据库的架构而不关心数据。

  2. 更改脚本旨在影响架构,同时保留数据。

第一种方法中,这很容易通过删除当前数据库并生成一个新数据库来完成。但我确信这不是你想要的,数据是你等式的重要组成部分。

第二种方法中,首先,您需要知道无论您要处理什么 DBMS,这都无法完成,因为 SQL 并不像听起来那么标准。考虑到一些特定的 DBMS,解决此问题的一般方法可能是在 DBMS 中创建模式的最新版本,并将其与当前版本进行比较。以下是您可能会发现对 MySQL 有用的工具列表。

可以在这种方法中做的事情:

  • 检查并查看是否删除了表。

  • 检查并查看表是否是新的。

  • 检查并查看是否删除了字段。

  • 检查并查看字段是否是新的。

  • 检查并查看表的属性是否已更改。

  • 检查并查看字段的属性是否已更改。

在这种方法中你不能做的事情:

  • 检查表是否被重命名。

  • 检查并查看是否重命名了字段。

换句话说,重命名的实体将产生一个DROP声明和一个CREATE将导致丢失数据的声明。这是这种方法的逻辑问题,不可能克服它。查看表或字段是否被重命名的唯一方法是查看alter 命令列表并查找适当的命令(如果您有alter 语句列表而不仅仅是最终模式)。实现这一点本身就很麻烦。

还有一个重要的这种方法也有问题;由于我们正在采用最接近目标模式的路径,因此我们可能会错过该过程中的一些重要步骤。即,考虑您可能执行的脚本,这些脚本影响了数据库的数据,但不影响其架构。此类语句无法使用任何 diff 工具提取,因为您没有数据参考(除非您实际上有我认为不是您的情况)。在这种情况下,您唯一的选择是按照应该应用的顺序一个接一个地应用脚本列表。只有当你有一个版本控制机制或者一个人应该通过分析得出这个列表时,才有可能拥有这样的列表。在这种情况下,我几乎想不出有什么工具可以帮助您(如果您的数据库没有版本)。至少我一个都不知道!

于 2013-07-14T05:49:07.807 回答
1

What I've done within my application is to keep a database version value in the database.

My application has a required Database version.

Here is a piece of my Pascal-Oracle code. Hope it will give you a good idea.

const
  ApplicationsDBVersion = 26 ;
.....
.....
if CurrentDBVersion = ApplicationsDBVersion then
  Exit ;
if CurrentDBVersion < 0 then // just in a case that the database is out of sync.
  Exit;
updtScript := tStringList.Create ;
if CurrentDBVersion < 1 then
  Upgrade2Version1 ;
if CurrentDBVersion < 2 then
  Upgrade2Version2 ;
if CurrentDBVersion < 3 then
  upgrade2Version3 ;
.....
.....
.....
procedure Upgrade2Version3 ;
begin
  UpdateParameter(-3) ; // set that database is in inconsitent state
  AddField('tableX','ColX','CHAR(1)') ; // I've plenty of such routines (AddRef, AlterField, DropField,AddTable etc...
  AddField('tableX','ColY','char(1) constraint CKC_checkConstraint check (ColY is null or (Coly in (''E'',''H'')))') ;
  AddField('TableY','Colz','NUMBER(3)') ;
  UpdateParameter(3); // set that database is in consistent state ( no fail in scripts )
  runScript(3) ; // actually do the job...
end;
...
procedure UpdateParameter (_dbVersion : Integer) ;
begin
  if CurrentDBVersion = 0 then
    updtScript.Add('Insert into parametre (parametre,sira_no,deger) values ('+QuotedStr(cPRM_VeriTabaniSurumu)+',1,''1'')')
  else
    updtScript.Add('update parametre set deger = '+IntToStr(_dbVersion) + ' where parametre = '+QuotedStr(cPRM_VeriTabaniSurumu));
end ;
于 2013-07-09T09:24:41.623 回答
1

我能想到的最好的方法是与您分享我的脚本,它正是这样做的:分别获取列定义列表并更改数据库表。它可以添加、删除、更改(甚至重命名)列和更改主键。不幸的是,它是 PHP,所以重新编码是必要的,但也许你会发现一般的想法很有用。

我已经成功使用这个脚本几个月来升级我的 CMS 的各种安装。

函数接受(作为第二个参数)一个数组数组,其中每个数组都包含在位置:

0 - Column name
1 - MySql column type (ex. "int" or "varchar(30)").
2 - whether columns is nullable (true for allow null, false for forbid)
3 - The default value for column (ie. "0").
4 - true, when column is part of primary key
5 - old name of a column (thus column of name in 5., if exists, is going to be renamed to column of name in 0.)

第一个参数是表名,第三个参数是函数是否应该删除数据库表中存在但在提供的数组中被跳过的列。

很抱歉这个恶心的合同,但这个功能从来都不是公共接口的一部分。:-)

这里是CreateOrUpdateTable函数体(参考后面解释):

function CreateOrUpdateTable($tablename, array $columns, $allowdropcolumn = false)
{       
    foreach($columns as &$column)
    {
        if ((!isset($column[0])) || (!preg_match('/^[a-zA-Z0-9_\-]+$/', $column[0])))
            $column[0] = 'TableColumn' . array_search($column, $columns);
        if ((!isset($column[1])) || (!preg_match('/^(int|date|datetime|decimal\([0-9]+,[0-9]+\)|varchar\([0-9]+\)|char\([0-9]+\)|text|tinyint)$/', $column[1])))
            $column[1] = 'int';
        if ((!isset($column[2])) || (!is_bool($column[2])))
            $column[2] = ALLOW_NULL;
        if ((!isset($column[3])) || (!is_string($column[3])))
            $column[3] = (($column[2] == ALLOW_NULL || $column[1] === 'text') ? 'NULL' : ($column[1] == 'int' ? "'0'" : ($column[1] == 'tinyint' ? "'0'" : ($column[1] == 'decimal' ? "'0.00'" : ($column[1] == 'date' ? "'1900-01-01'" : ($column[1] == 'datetime' ? "'1900-01-01 00:00:00'" : "''"))))));
        else
            $column[3] = "'" . Uti::Sql($column[3]) . "'";
        if ((!isset($column[4])) || (!is_bool($column[4])))
            $column[4] = false;
    }
    unset($column);

    if (!$this->TableExists($tablename))
    {
        $statements = array();
        foreach ($columns as $column)
        {
            $statement = $this->ColumnCreationStatement($column);
            if ($statement !== '')
                $statements[] = $statement;
        }

        $this->Query("create table " . $tablename . "(" . implode(',', $statements) . ") ENGINE=InnoDB DEFAULT CHARSET=latin2");
    }
    else
    {
        $this->Select("show columns in " . $tablename);
        $existing = $this->AllRows(null, 'Field');

        $oldkeys = array(); $newkeys = array();         
        foreach ($existing as $e)
            if ($e['Key'] === 'PRI')
                $oldkeys[] = $e['Field'];

        sort($oldkeys);
        $oldkeys = implode(',', $oldkeys);

        $lastcolumn = ''; // not 'FIRST' as we can extend existing table here providing only extending columns

        foreach ($columns as $column)
        {
            if ($column[4])
                $newkeys[] = $column[0];

            $newtype = $column[1] . ($column[1] === 'int' ? '(11)' : ($column[1] === 'tinyint' ? '(4)' : ''));
            $newnull = ($column[2] === ALLOW_NULL ? 'YES' : 'NO');
            $newdefault = $column[3];                   

            if (isset($existing[$column[0]]))
            {
                $oldtype = $existing[$column[0]]['Type'];
                $oldnull = $existing[$column[0]]['Null'];
                $olddefault = isset($existing[$column[0]]['Default']) ? "'" . Uti::Sql($existing[$column[0]]['Default']) . "'" : "NULL";

                if (($oldtype != $newtype) || ($oldnull != $newnull) || ($olddefault != $newdefault))
                {
                    $this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], changing: type [" .
                        $oldtype . "] => [" . $newtype . "] nullability [" . $oldnull . "] => [" . $newnull . "] default [" . $olddefault . "] => [" . $newdefault . "]", true);
                    $statement = $this->ColumnCreationStatement($column, false);
                    if ($statement !== '')
                        $this->Query("alter table " . $tablename . " change " . $column[0] . " " . $statement);
                }

                unset($existing[$column[0]]);
            }
            else if (isset($column[5]) && (Uti::AnyExists(array_keys($existing), $column[5]) !== false))
            {
                $oldcolumn = Uti::AnyExists(array_keys($existing), $column[5]);

                $this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], renaming: name [" . $oldcolumn . "] => [" . $column[0] . "] " .
                    " type [" . $newtype . "] nullability [" . $newnull . "] default [" . $newdefault . "]", true);

                $statement = $this->ColumnCreationStatement($column, false);
                if ($statement !== '')
                    $this->Query("alter table " . $tablename . " change " . $oldcolumn . " " . $statement);

                unset($existing[$oldcolumn]);
            }
            else
            {
                $this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], adding: name [" . $column[0] . "] " .
                    " type [" . $newtype . "] nullability [" . $newnull . "] default [" . $newdefault . "]", true);

                $statement = $this->ColumnCreationStatement($column, false);
                if ($statement !== '')
                    $this->Query("alter table " . $tablename . " add " . $statement . " " . $lastcolumn);                   
            }

            $lastcolumn = 'AFTER ' . $column[0];
        }

        if ($allowdropcolumn)
        {
            foreach ($existing as $e)
            {
                $this->SaveToLog("Altering table [" . $tablename . "], column [" . $e['Field'] . "], dropping", true);

                $this->Query("alter table " . $tablename . " drop " . $e['Field']);
            }
        }

        sort($newkeys);
        $newkeys = implode(',',$newkeys);

        if ($oldkeys != $newkeys)
        {
            $this->SaveToLog("Altering table [" . $tablename . "], changing keys [" . $oldkeys . "] => [" . $newkeys . "]", true);

            if ($oldkeys !== '')
                $this->Query("alter table " . $tablename . " drop primary key");
            if ($newkeys !== '')    
                $this->Query("alter table " . $tablename . " add primary key (" . $newkeys . ")");
        }
    }
}

以下外部函数需要说明:

ColumnCreationStatement提供更改/创建表片段:

private function ColumnCreationStatement(array $columninfo, $includekey = true)
{
    $r = '';

    if ((count($columninfo) > 0) && (preg_match('/^[a-zA-Z0-9_\-]+$/', $columninfo[0])))
    {
        $r .= $columninfo[0];
        if ((count($columninfo) > 1) && (preg_match('/^(int|date|datetime|decimal\([0-9]+,[0-9]+\)|varchar\([0-9]+\)|char\([0-9]+\)|text|tinyint)$/', $columninfo[1])))
            $r .= ' ' . $columninfo[1];
        else
            $r .= ' int';
        if ((count($columninfo) > 2) && is_bool($columninfo[2]))
            $r .= ($columninfo[2] === NOT_NULL ? ' not null' : ' null');
        if ((count($columninfo) > 3) && is_string($columninfo[3]) && ($columninfo[3] !== '') && ($columninfo[1] !== 'text'))
            $r .= " default " . $columninfo[3];
        if ((count($columninfo) > 4) && is_bool($columninfo[4]) && $includekey)
            $r .= ($columninfo[4] === true ? ', primary key(' . $columninfo[0] . ')' : '');
    }

    return $r;
}

TableExists只是验证表在数据库中是否可用(使用show tables like)。

查询执行 MySql 语句(是的:不返回结果;])

SelectAllRows是将行作为哈希表集合返回的快捷方式。

SaveToLog - 我猜 - 很明显。:-)

Uti::AnyExists看起来像这样:

public static function AnyExists($haystack, $needles, $separator = ';')
{
    if (!is_array($needles))
        $needles = explode($separator, $needles);

    foreach ($needles as $needle)
    {
        if (array_search($needle, $haystack) !== false)
            return $needle;
    }

    return false;
}

我希望这一切都有帮助。如有任何问题,请随时在评论中提问。:-)

于 2013-07-15T08:45:23.350 回答
1

听起来这些脚本是静态的。您能否包含所有脚本(版本 x.1 到 x.2,以及 x.2 到 x.3,..etc)并运行用户需要的特定脚本?

于 2013-07-08T17:48:49.500 回答