SQLiteに存在しない場合はALTERTABLE ADD COLUMN


89

最近、既存のSQLiteデータベーステーブルのいくつかに列を追加する必要がありました。これはで行うことができますALTER TABLE ADD COLUMN。もちろん、テーブルがすでに変更されている場合は、そのままにしておきます。残念ながら、SQLiteはのIF NOT EXISTS句をサポートしていませんALTER TABLE

現在の回避策は、このPythonの例のように(ただしC ++で)、ALTER TABLEステートメントを実行し、「列名の重複」エラーを無視することです。

ただし、データベーススキーマを設定するための通常のアプローチはCREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTSステートメントを含む.sqlスクリプトを作成することです。これは、sqlite3_execまたはsqlite3コマンドラインツールを使用して実行できます。ALTER TABLEこれらのスクリプトファイルを挿入することはできません。そのステートメントが失敗すると、それ以降は実行されないためです。

テーブル定義を1つの場所に置き、.sqlファイルと.cppファイルに分割したくない。ALTER TABLE ADD COLUMN IF NOT EXISTS純粋なSQLiteSQLで回避策を書く方法はありますか?

回答:


64

私は99%純粋なSQLメソッドを持っています。アイデアは、スキーマをバージョン管理することです。これは2つの方法で実行できます。

  • 'user_version'プラグマコマンド(PRAGMA user_version)を使用して、データベーススキーマバージョンの増分番号を格納します。

  • 独自に定義したテーブルにバージョン番号を保存します。

このようにして、ソフトウェアの起動時に、データベーススキーマを確認し、必要に応じてALTER TABLEクエリを実行して、保存されているバージョンをインクリメントできます。これは、さまざまな更新を「ブラインド」で試行するよりもはるかに優れています。特に、データベースが何年にもわたって数回大きくなり、変更される場合はなおさらです。


7
の初期値はuser_version?私はゼロだと思いますが、それが文書化されているのを見るといいでしょう。
Craig McQueen

これでも、sqliteはサポートIFしてALTER TABLEおらず、条件付きがないため、純粋なSQLで実行できますか?「99%純粋なSQL」とはどういう意味ですか?
Craig McQueen

1
@CraigMcQueenの初期値はuser_version0のように見えますが、実際はユーザー定義の値なので、独自の初期値を作成できます。
MPelletier 2014

7
user_version初期値に関する質問は、既存のデータベースがあり、これまで使用したことがないが、使用user_versionを開始したい場合に関連します。そのため、sqliteが特定の初期値に設定していると想定する必要があります。
Craig McQueen

1
@CraigMcQueen同意しますが、文書化されていないようです。
MPelletier 2014

30

回避策の1つは、列を作成し、列がすでに存在する場合に発生する例外/エラーをキャッチすることです。複数の列を追加する場合は、それらを別々のALTER TABLEステートメントに追加して、1つの重複が他の列の作成を妨げないようにします。

sqliteのネット、我々はこのような何かをしました。重複するsqliteエラーを他のsqliteエラーと区別できないため、完全ではありません。

Dictionary<string, string> columnNameToAddColumnSql = new Dictionary<string, string>
{
    {
        "Column1",
        "ALTER TABLE MyTable ADD COLUMN Column1 INTEGER"
    },
    {
        "Column2",
        "ALTER TABLE MyTable ADD COLUMN Column2 TEXT"
    }
};

foreach (var pair in columnNameToAddColumnSql)
{
    string columnName = pair.Key;
    string sql = pair.Value;

    try
    {
        this.DB.ExecuteNonQuery(sql);
    }
    catch (System.Data.SQLite.SQLiteException e)
    {
        _log.Warn(e, string.Format("Failed to create column [{0}]. Most likely it already exists, which is fine.", columnName));
    }
}

28

SQLiteは、「table_info」と呼ばれるプラグマステートメントもサポートしています。このステートメントは、列の名前(および列に関するその他の情報)を含むテーブルの列ごとに1つの行を返します。これをクエリで使用して、欠落している列を確認し、存在しない場合はテーブルを変更できます。

PRAGMA table_info(foo_table_name)

http://www.sqlite.org/pragma.html#pragma_table_info


30
リンクだけでなく、検索を完了するためのコードを提供すれば、答えははるかに優れたものになります。
Michael Alan Huff

PRAGMA table_info(table_name)。このコマンドは、table_nameの各列を結果の行としてリストします。この結果に基づいて、列が存在するかどうかを判断できます。
ハオグエン

2
単一のクエリで、列が存在しない場合は列が追加されるように、より大きなSQLステートメントの一部でプラグマを組み合わせてこれを行う方法はありますか?
Michael

1
@マイケル。私の知る限り、できません。PRAGMAコマンドの問題は、クエリを実行できないことです。このコマンドはSQLエンジンにデータを提示せず、結果を直接返します
Kowlown 2015

1
これは競合状態を引き起こしませんか?列名を確認して、列が欠落していることを確認するとしますが、その間に別のプロセスが列を追加します。次に、列を追加しようとしますが、すでに存在しているためエラーが発生します。私は最初にデータベースをロックすることになっていると思いますか?私はsqliteの初心者です:)。
ベンファーマー

25

DBアップグレードステートメントでこれを実行している場合、おそらく最も簡単な方法は、すでに存在している可能性のあるフィールドを追加しようとしたときにスローされた例外をキャッチすることです。

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}

2
私は例外スタイルのプログラミングは好きではありませんが、これは驚くほどきれいです。多分あなたは私を少し揺さぶったでしょう。
スティーブンJ

私もそれが好きではありませんが、C ++はこれまでで最も例外的なスタイルのプログラミング言語です。ですから、それでも「有効」と見なされるかもしれません。
tmighty 2017

SQLiteの私のユースケース=他の言語(MSSQL)のばかげた単純な/ワンライナーのために大量の余分なコーディングをしたくありません。良い答え...それは「例外スタイルのプログラミング」ですが、アップグレード機能/分離されているので、許容できると思います。
maplemale 2018年

他の人はそれを好きではありませんが、これが最善の解決策だと思いますlol
AdamVarhegyi19年

13

プラグマのメソッドはtable_info(table_name)で、テーブルのすべての情報を返します。

これは、チェック列が存在するかどうかに使用する方法の実装です。

    public boolean isColumnExists (String table, String column) {
         boolean isExists = false
         Cursor cursor;
         try {           
            cursor = db.rawQuery("PRAGMA table_info("+ table +")", null);
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndex("name"));
                    if (column.equalsIgnoreCase(name)) {
                        isExists = true;
                        break;
                    }
                }
            }

         } finally {
            if (cursor != null && !cursor.isClose()) 
               cursor.close();
         }
         return isExists;
    }

ループを使用せずにこのクエリを使用することもできます。

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);

カーソルcursor = db.rawQuery( "select * from tableName"、null); columns = cursor.getColumnNames();
Vahe Gharibyan 2017

1
カーソルを閉じるのを忘れたと思います:
Pecana19年

@VaheGharibyanなので、列名を取得するためだけにDB内のすべてを選択するだけですか?!あなたが単に言っているのはwe give no shit about performance:))です。
Farid

最後のクエリが正しくないことに注意してください。適切なクエリは次のとおりですSELECT * FROM pragma_table_info(...)(プラグマとテーブル情報の間のSELECTとアンダースコアに注意してください)。実際にどのバージョンで追加されたかはわかりません。3.16.0では機能しませんでしたが、3.22.0では機能します。
PressingOnAlways

3

pragma table_info()より大きなSQLの一部としての結果を使用したい場合。

select count(*) from
pragma_table_info('<table_name>')
where name='<column_name>';

重要な部分は、のpragma_table_info('<table_name>')代わりに使用することですpragma table_info('<table_name>')


この回答は、@ RobertHawkeyの回答に触発されています。新しい回答として投稿する理由は、コメントとして投稿するのに十分な評判がないためです。


1

私はこのクエリを思い付く

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • 列が存在する場合、内部クエリは0または1を返します。
  • 結果に基づいて、列を変更します

コード=エラー(1)、メッセージ= System.Data.SQLite.SQLiteException(0x800007BF):「ALTER」付近のSQLロジックエラー:System.Data.SQLite.SQLite3.Prepareでの構文エラー
イン


0

上記の回答をC#/。Netで取得し、Qt / C ++用に書き直しましたが、あまり変更はありませんでしたが、将来C ++の「っぽい」回答を探している人のためにここに残したいと思いました。

    bool MainWindow::isColumnExisting(QString &table, QString &columnName){

    QSqlQuery q;

    try {
        if(q.exec("PRAGMA table_info("+ table +")"))
            while (q.next()) {
                QString name = q.value("name").toString();     
                if (columnName.toLower() == name.toLower())
                    return true;
            }

    } catch(exception){
        return false;
    }
    return false;
}

0

または、CASE-WHEN TSQLステートメントをpragma_table_infoと組み合わせて使用​​して、列が存在するかどうかを確認することもできます。

select case(CNT) 
    WHEN 0 then printf('not found')
    WHEN 1 then printf('found')
    END
FROM (SELECT COUNT(*) AS CNT FROM pragma_table_info('myTableName') WHERE name='columnToCheck') 

ここでどのようにテーブルを変更しますか?列名が一致する場合は?
user2700767

0

これが私の解決策ですが、Pythonでは(Pythonに関連するトピックに関する投稿を見つけようとしましたが失敗しました):

# modify table for legacy version which did not have leave type and leave time columns of rings3 table.
sql = 'PRAGMA table_info(rings3)' # get table info. returns an array of columns.
result = inquire (sql) # call homemade function to execute the inquiry
if len(result)<= 6: # if there are not enough columns add the leave type and leave time columns
    sql = 'ALTER table rings3 ADD COLUMN leave_type varchar'
    commit(sql) # call homemade function to execute sql
    sql = 'ALTER table rings3 ADD COLUMN leave_time varchar'
    commit(sql)

PRAGMAを使用してテーブル情報を取得しました。列に関する情報でいっぱいの多次元配列(列ごとに1つの配列)を返します。配列の数を数えて列の数を取得します。十分な列がない場合は、ALTERTABLEコマンドを使用して列を追加します。


0

一度に1行実行すれば、これらすべての答えは問題ありません。ただし、元の質問は、単一のデータベース実行によって実行されるSQLスクリプトを入力することであり、すべてのソリューション(列が事前に存在するかどうかを確認するなど)では、実行プログラムがどのテーブルの知識を持っているか、この情報を判別するために、列が変更/追加されているか、入力スクリプトの前処理と解析が行われています。通常、これをリアルタイムまたは頻繁に実行することはありません。したがって、例外をキャッチして次に進むという考えは受け入れられます。そこに問題があります...どのように進むか。幸い、エラーメッセージには、これを行うために必要なすべての情報が表示されます。アイデアは、alter table呼び出しで例外が発生した場合に、sqlを実行することです。sqlでalter table行を見つけ、残りの行を返し、成功するか、一致するaltertable行が見つからなくなるまで実行します。配列にSQLスクリプトがあるサンプルコードを次に示します。各スクリプトを実行する配列を繰り返します。これを2回呼び出して、alter tableコマンドを失敗させますが、SQLからalter tableコマンドを削除し、更新されたコードを再実行するため、プログラムは成功します。

#!/bin/sh
# the next line restarts using wish \

exec /opt/usr8.6.3/bin/tclsh8.6  "$0" ${1+"$@"}
foreach pkg {sqlite3 } {
    if { [ catch {package require {*}$pkg } err ] != 0 } {
    puts stderr "Unable to find package $pkg\n$err\n ... adjust your auto_path!";
    }
}
array set sqlArray {
    1 {
    CREATE TABLE IF NOT EXISTS Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      );
    CREATE TABLE IF NOT EXISTS Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        );
    INSERT INTO Version(version) values('1.0');
    }
    2 {
    CREATE TABLE IF NOT EXISTS Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        );
    ALTER TABLE Notes ADD COLUMN dump text;
    INSERT INTO Version(version) values('2.0');
    }
    3 {
    ALTER TABLE Version ADD COLUMN sql text;
    INSERT INTO Version(version) values('3.0');
    }
}

# create db command , use in memory database for demonstration purposes
sqlite3 db :memory:

proc createSchema { sqlArray } {
    upvar $sqlArray sql
    # execute each sql script in order 
    foreach version [lsort -integer [array names sql ] ] {
    set cmd $sql($version)
    set ok 0
    while { !$ok && [string length $cmd ] } {  
        try {
        db eval $cmd
        set ok 1  ;   # it succeeded if we get here
        } on error { err backtrace } {
        if { [regexp {duplicate column name: ([a-zA-Z0-9])} [string trim $err ] match columnname ] } {
            puts "Error:  $err ... trying again" 
            set cmd [removeAlterTable $cmd $columnname ]
        } else {
            throw DBERROR "$err\n$backtrace"
        }
        }
    }
    }
}
# return sqltext with alter table command with column name removed
# if no matching alter table line found or result is no lines then
# returns ""
proc removeAlterTable { sqltext columnname } {
    set mode skip
    set result [list]
    foreach line [split $sqltext \n ] {
    if { [string first "alter table" [string tolower [string trim $line] ] ] >= 0 } {
        if { [string first $columnname $line ] } {
        set mode add
        continue;
        }
    }
    if { $mode eq "add" } {
        lappend result $line
    }
    }
    if { $mode eq "skip" } {
    puts stderr "Unable to find matching alter table line"
    return ""
    } elseif { [llength $result ] }  { 
    return [ join $result \n ]
    } else {
    return ""
    }
}
               
proc printSchema { } {
    db eval { select * from sqlite_master } x {
    puts "Table: $x(tbl_name)"
    puts "$x(sql)"
    puts "-------------"
    }
}
createSchema sqlArray
printSchema
# run again to see if we get alter table errors 
createSchema sqlArray
printSchema

期待される出力

Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------
Error:  duplicate column name: dump ... trying again
Error:  duplicate column name: sql ... trying again
Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------

0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

ロジック:sqlite_masterのsql列にはテーブル定義が含まれているため、列名の文字列が確実に含まれています。

部分文字列を検索しているとき、それには明らかな制限があります。したがって、ColumnNameでさらに制限的な部分文字列を使用することをお勧めします。たとえば、次のようなものです( '`'文字が常に存在するとは限らないため、テストの対象となります)。

select * from sqlite_master where type = 'table' and tbl_name = 'MyTable' and sql like '%`MyColumn` TEXT%'

0

私は2つのクエリでそれを解決します。これは、System.Data.SQLiteを使用したUnity3Dスクリプトです。

IDbCommand command = dbConnection.CreateCommand();
            command.CommandText = @"SELECT count(*) FROM pragma_table_info('Candidat') c WHERE c.name = 'BirthPlace'";
            IDataReader reader = command.ExecuteReader();
            while (reader.Read())
            {
                try
                {
                    if (int.TryParse(reader[0].ToString(), out int result))
                    {
                        if (result == 0)
                        {
                            command = dbConnection.CreateCommand();
                            command.CommandText = @"ALTER TABLE Candidat ADD COLUMN BirthPlace VARCHAR";
                            command.ExecuteNonQuery();
                            command.Dispose();
                        }
                    }
                }
                catch { throw; }
            }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.