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 比較/差分/スクリプト生成ライブラリはありますか?

ご協力ありがとうございました!

4

5 に答える 5

3

私は長い間同じトピックについて考えてきましたが、適切な方法が見つかりませんでした。役立つことを願って、私がしていることを共有します。

私の現在のアプローチは、データベースの以前のスキーマ バージョンに適した方法で設計された SQL クエリのリストを適用することです。コマンドが既に適用されている場合は、単に失敗します (フィールドの追加やインデックスの追加など)。

このアプローチでは、DB スキーマを変更する方法が制限され、エラーが発生しやすくなります。たとえば、誤って ENUM(a,b) フィールドを ENUM(a,b,c) に拡張してから ENUM(a) に拡張するクエリがある場合などです。 ,b,c,d)dスクリプトを再度実行すると、値を持つ既存のレコードが破損します。最新の形式のクエリが 1 つしかない場合、これは簡単に修正できます。

後でスキーマのバージョン管理も追加し、現在はシンプルだが管理しやすい更新ファイルの形式を使用しています。1 行に 1 つのクエリで終わり、;スキーマ バージョンを区切る追加の行です。

-- version 105

このアップグレードにより、コードは大幅に簡素化され、すべてのバージョン移行を処理する単一の関数に統合されます。関数は、行の後にクエリを処理するだけで済みます--version <current version>。行に到達する-- versionと、関数はデータベース内のスキーマ バージョンを更新します。

また、この形式では、mysql -f mydb < myfile コマンドを使用して手動で処理できます。この場合、バージョン行はコメントとして無視され、すべての変更に対するすべてのコマンドが現在のスキーマで試行されます - これはエラーを修正するために使用できます (エラーによって、予想よりも古いスキーマを意味していると思います)。ストアド プロシージャのコードを更新するための同様のトリックもあります。

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

あなたの質問では、DBスキーマの差分/パッチについて尋ねます-これは、新しいフィールド/インデックス/などを追加する場合にのみ一般化できます。ただし、名前が変更されたフィールドまたは削除されたフィールドを自動的に処理することはできません。自動化されたプロセスが、既存のスキーマと新しいスキーマを調べるだけでフィールドの名前を変更する必要があることaを認識し、既存のデータを保持する方法はありません (既存のデータはそのまま保持する必要があると思います)。table1b

要約すると、一般的なケースでは、DB スキーマ更新スクリプトを自動生成する方法はありません。

于 2013-07-14T02:22:41.057 に答える
1

アプリケーション内で行ったことは、データベースのバージョン値をデータベースに保持することです。

私のアプリケーションには、必要なデータベース バージョンがあります。

これが私の Pascal-Oracle コードの一部です。それがあなたに良い考えを与えることを願っています。

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 のさまざまなインストールをアップグレードしました。

関数は (2 番目の引数として) 配列の配列を受け入れます。後者のそれぞれには次の位置が含まれます。

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.)

最初のパラメーターはテーブル名で、3 番目のパラメーターは、関数がデータベース テーブルに存在するが、提供された配列でスキップされた列を削除するかどうかです。

嫌な契約で申し訳ありませんが、この関数はパブリック インターフェイスの一部になることを意図したものではありませんでした。:-)

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 ステートメントを実行します (そして、はい: 結果を返しません ;])

SelectおよびAllRowsは、行をハッシュテーブル コレクションとして返すためのショートカットです。

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 に答える