ИЗМЕНИТЬ ТАБЛИЦУ ДОБАВИТЬ КОЛОНКУ, ЕСЛИ НЕ СУЩЕСТВУЕТ в SQLite


89

Недавно у нас возникла необходимость добавить столбцы в некоторые из наших существующих таблиц базы данных SQLite. Это можно сделать с помощью ALTER TABLE ADD COLUMN. Конечно, если таблица уже была изменена, мы хотим оставить ее в покое. К сожалению, SQLite не поддерживает IF NOT EXISTSпункт о ALTER TABLE.

Наш текущий обходной путь - выполнить оператор ALTER TABLE и игнорировать любые ошибки «повторяющееся имя столбца», как в этом примере Python (но на C ++).

Тем не менее, наш обычный подход к созданию схемы базы данных должны иметь .sql скрипта , содержащий CREATE TABLE IF NOT EXISTSи CREATE INDEX IF NOT EXISTSзаявление, которые могут быть выполнены с использованием sqlite3_execили sqlite3инструмент командной строки. Мы не можем вставить ALTER TABLEэти файлы сценария, потому что в случае сбоя этого оператора ничего после него не будет выполнено.

Я хочу, чтобы определения таблиц были в одном месте и не разделялись между файлами .sql и .cpp. Есть ли способ написать обходной путь ALTER TABLE ADD COLUMN IF NOT EXISTSв чистом SQLite SQL?

Ответы:


64

У меня есть 99% чистый метод SQL. Идея состоит в том, чтобы версия вашей схемы. Сделать это можно двумя способами:

  • Используйте команду pragma 'user_version' ( PRAGMA user_version), чтобы сохранить инкрементный номер для вашей версии схемы базы данных.

  • Сохраните номер вашей версии в вашей собственной таблице.

Таким образом, когда программное обеспечение запускается, оно может проверить схему базы данных и, при необходимости, выполнить ваш ALTER TABLEзапрос, а затем увеличить сохраненную версию. Это намного лучше, чем «вслепую» пытаться обновлять различные версии, особенно если ваша база данных растет и меняется несколько раз за эти годы.


7
Какая начальная стоимость user_version? Я предполагаю ноль, но было бы хорошо, если бы это задокументировано.
Craig McQueen

Даже с этим, можно ли это сделать на чистом SQL, поскольку sqlite не поддерживает IFи ALTER TABLEне имеет условного выражения? Что вы имеете в виду под «чистым SQL на 99%»?
Craig McQueen

1
@CraigMcQueen Что касается начального значения user_version, оно похоже на 0, но на самом деле это значение, определяемое пользователем, поэтому вы можете создать собственное начальное значение.
MPelletier 03

7
Вопрос о user_versionначальном значении актуален, когда у вас есть существующая база данных, и вы никогда не использовали ее user_versionраньше, но хотите начать ее использовать, поэтому вам нужно предположить, что sqlite установил для нее определенное начальное значение.
Craig McQueen

1
@CraigMcQueen Я согласен, но, похоже, это не задокументировано.
MPelletier

30

Один из способов решения проблемы - просто создать столбцы и перехватить исключение / ошибку, возникающую, если столбец уже существует. При добавлении нескольких столбцов добавьте их в отдельные операторы ALTER TABLE, чтобы один дубликат не препятствовал созданию других.

С sqlite-net мы сделали что-то вроде этого. Это не идеально, поскольку мы не можем отличить повторяющиеся ошибки 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», которая возвращает одну строку для каждого столбца в таблице с именем столбца (и другой информацией о столбце). Вы можете использовать это в запросе для проверки отсутствующего столбца и, если он отсутствует, изменить таблицу.

PRAGMA table_info(foo_table_name)

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


30
Ваш ответ был бы намного лучше, если бы вы предоставили код для завершения этого поиска, а не просто ссылку.
Майкл Алан Хафф

PRAGMA table_info (имя_таблицы). Эта команда перечислит каждый столбец table_name как строку в результате. Основываясь на этом результате, вы можете определить, существовал ли столбец или нет.
Hao Nguyen

2
Есть ли способ сделать это, объединив прагму в части более крупного оператора SQL, чтобы столбец добавлялся, если он не существует, но в противном случае не существует, только в одном запросе?
Майкл

1
@Майкл. Насколько я знаю, нет, не можете. Проблема с командой PRAGMA в том, что вы не можете делать запросы по ней. команда не
передает

1
Разве это не создает состояние гонки? Скажем, я проверяю имена столбцов, вижу, что мой столбец отсутствует, но тем временем другой процесс добавляет столбец. Затем я попытаюсь добавить столбец, но получу сообщение об ошибке, потому что он уже существует. Думаю, я должен сначала заблокировать базу данных или что-то в этом роде? Боюсь, что я новичок в sqlite :).
Бен Фармер

25

Если вы делаете это в операторе обновления БД, возможно, самый простой способ - просто перехватить возникшее исключение, если вы пытаетесь добавить поле, которое может уже существовать.

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
Мне не нравится программирование в стиле исключений, но это удивительно чисто. Может, ты меня немного повлиял.
Stephen J

Мне это тоже не нравится, но C ++ - это язык программирования с наиболее исключительным стилем. Так что я думаю, что кто-то все еще может считать его «действительным».
tmighty

Мой вариант использования SQLite = я не хочу делать тонну дополнительного кода для чего-то глупого простого / одного лайнера на других языках (MSSQL). Хороший ответ ... хотя это «программирование в стиле исключения», оно находится в функции обновления / изолированно, поэтому я полагаю, что это приемлемо.
maplemale

В то время как другим это не нравится, я думаю, что это лучшее решение,
смеется

13

Это метод PRAGMA - 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 cursor = db.rawQuery ("выбрать * из tableName", null); columns = cursor.getColumnNames ();
Ваге Гарибян

1
Я думаю, вы забыли закрыть курсор :-)
Pecana

@VaheGharibyan, так что вы просто выберете все в своей БД, чтобы получить имена столбцов ?! Вы просто говорите we give no shit about performance:)).
Фарид

Обратите внимание, последний запрос неверен. Правильный запрос: 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>').


Этот ответ вдохновлен ответом @Robert Hawkey. Причина, по которой я публикую его как новый ответ, заключается в том, что у меня недостаточно репутации, чтобы опубликовать его как комментарий.


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, если столбец существует.
  • По результату переделать столбец

code = Error (1), message = System.Data.SQLite.SQLiteException (0x800007BF): логическая ошибка SQL рядом с «ALTER»: синтаксическая ошибка в System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ


0

Я взял приведенный выше ответ на C # /. Net и переписал его для Qt / C ++, не сильно изменив его, но я хотел оставить его здесь для всех, кто в будущем будет искать ответ C ++ 'ish'.

    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, чтобы получить информацию о таблице. Он возвращает многомерный массив, полный информации о столбцах - по одному массиву на столбец. Я считаю количество массивов, чтобы получить количество столбцов. Если столбцов недостаточно, я добавляю столбцы с помощью команды ALTER TABLE.


0

Все эти ответы хороши, если вы выполняете по одной строке за раз. Однако исходный вопрос заключался в том, чтобы ввести sql-скрипт, который будет выполняться одним выполнением db, и все решения (например, проверка наличия столбца заранее) потребуют, чтобы выполняющаяся программа либо знала, какие таблицы и столбцы изменяются / добавляются или выполняют предварительную обработку и анализ входного скрипта для определения этой информации. Обычно вы не собираетесь запускать это в реальном времени или часто. Так что идея перехвата исключения приемлема, а затем двигаться дальше. В этом проблема ... как двигаться дальше. К счастью, сообщение об ошибке дает нам всю необходимую для этого информацию. Идея состоит в том, чтобы выполнить sql, если он вызывает исключение при вызове alter table, мы можем найти строку alter table в sql и вернуть оставшиеся строки и выполнить до тех пор, пока он либо не завершится успешно, либо больше не будет найдено соответствующих строк alter table. Вот пример кода, где у нас есть скрипты sql в массиве. Мы перебираем массив, выполняя каждый скрипт. Мы вызываем его дважды, чтобы команда alter table завершилась ошибкой, но программа завершается успешно, потому что мы удаляем команду alter table из sql и повторно выполняем обновленный код.

#!/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%'

Логика: столбец sql в sqlite_master содержит определение таблицы, поэтому он обязательно содержит строку с именем столбца.

Поскольку вы ищете подстроку, у нее есть очевидные ограничения. Поэтому я бы предложил использовать еще более ограничительную подстроку в ColumnName, например, что-то вроде этого (при условии тестирования, поскольку символ '' 'присутствует не всегда):

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

0

Решаю за 2 запроса. Это мой скрипт Unity3D, использующий System.Data.SQLite.

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; }
            }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.