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


90

Нещодавно нам довелося додати стовпці до кількох наших існуючих таблиць бази даних 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? Я припускаю, що нуль, але було б непогано бачити це задокументованим.
Крейг МакКвін

Навіть з цим, чи можна це зробити в чистому SQL, оскільки sqlite не підтримує IFі ALTER TABLEне має умовного? Що ви маєте на увазі під "99% чистого SQL"?
Крейг МакКвін

1
@CraigMcQueen Щодо початкового значення user_version, воно, здається, дорівнює 0, але насправді це визначене користувачем значення, тож ви можете зробити власне початкове значення.
MPelletier

7
Питання про user_versionпочаткове значення є актуальним, коли у вас є існуюча база даних, і ви ніколи раніше не використовували її user_version, але ви хочете почати її використовувати, тому вам потрібно припустити, що sqlite встановлює для неї певне початкове значення.
Крейг МакКвін

1
@CraigMcQueen Я згоден, але, схоже, це не задокументовано.
MPelletier 03.03.14

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 також підтримує оператор pragma під назвою "table_info", який повертає по одному рядку на стовпець у таблиці з ім'ям стовпця (та іншу інформацію про стовпець). Ви можете використати це у запиті, щоб перевірити відсутність стовпця, а якщо немає - змінити таблицю.

PRAGMA table_info(foo_table_name)

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


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

PRAGMA таблиця_інформація (ім'я_таблиці). Ця команда перерахує кожен стовпець імені таблиці як рядок у результаті. На основі цього результату ви можете визначити, існувала колонка чи ні.
Hao Nguyen

2
Чи є спосіб зробити це, поєднавши прагму в частині більшого оператора SQL, таким чином, що стовпець додається, якщо він не існує, але в іншому випадку його немає, лише в одному запиті?
Майкл

1
@ Майкл. Наскільки я знаю, ні, ви не можете. Проблема команди PRAGMA полягає в тому, що ви не можете запитувати її. команда не подає дані до механізму SQL, вона безпосередньо повертає результати
Коулоун,

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 ++ - це найпростіша мова програмування в історії. Тож, думаю, хтось все ще може бачити це як „дійсне”.
могутній

Мій випадок використання SQLite = Я не хочу робити тонну додаткового кодування чогось дурного простого / одного вкладиша іншими мовами (MSSQL). Хороша відповідь ... хоча це "програмування у стилі винятків", це функція оновлення / ізольована, тому я вважаю, що це прийнятно.
maplemale

Поки це не подобається іншим, я вважаю, що це найкраще рішення, ха-ха
Адам Вархеджі

13

threre - це метод PRAGMA - table_info (ім'я_таблиці), він повертає всю інформацію таблиці.

Ось реалізація, як використовувати його для перевірки стовпця існує чи ні,

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

Курсор курсору = db.rawQuery ("вибрати * з імені таблиці", нуль); колонки = cursor.getColumnNames ();
Вахе Гарібян,

1
Думаю, ви забули закрити курсор :-)
Пекана

@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 = Помилка (1), message = System.Data.SQLite.SQLiteException (0x800007BF): Помилка логіки SQL біля "ALTER": синтаксична помилка в System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ

0

Якщо у вас виникла така проблема в еластичному / глинобитному повітрі і ви опинитесь тут першим, я знайшов рішення і опублікував його на відповідне питання: ДОДАТИ СТОВПЕЦЬ до sqlite db, ЯКЩО НЕ ІСНУЄ - flex / air sqlite?

Мій коментар тут: https://stackoverflow.com/a/24928437/2678219


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

Ви також можете використовувати оператор CASQL-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, ми можемо знайти рядок таблиці alter в sql і повернути решту рядків та виконати, поки вона не буде успішною або не буде знайдено більше відповідних рядків таблиці alter. Ось приклад коду, де ми маємо скрипти 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.