BẢNG ALTER THÊM CỘT NẾU KHÔNG TỒN TẠI SQLite


89

Gần đây chúng tôi có nhu cầu thêm cột vào một vài bảng cơ sở dữ liệu SQLite hiện có của chúng tôi. Điều này có thể được thực hiện với ALTER TABLE ADD COLUMN. Tất nhiên, nếu bảng đã được thay đổi, chúng tôi muốn để nó yên. Thật không may, SQLite không hỗ trợ IF NOT EXISTSđiều khoản trên ALTER TABLE.

Cách giải quyết hiện tại của chúng tôi là thực thi câu lệnh ALTER TABLE và bỏ qua bất kỳ lỗi "tên cột trùng lặp" nào, giống như ví dụ Python này (nhưng trong C ++).

Tuy nhiên, cách tiếp cận thông thường của chúng tôi để thiết lập lược đồ cơ sở dữ liệu là có một tập lệnh .sql chứa CREATE TABLE IF NOT EXISTSvà các CREATE INDEX IF NOT EXISTScâu lệnh, có thể được thực thi bằng cách sử dụng sqlite3_exechoặcsqlite3 công cụ dòng lệnh. Chúng tôi không thể đưa ALTER TABLEcác tệp script này vào vì nếu câu lệnh đó không thành công, bất kỳ thứ gì sau đó sẽ không được thực thi.

Tôi muốn định nghĩa bảng ở một nơi và không phân chia giữa các tệp .sql và .cpp. Có cách nào để viết giải pháp thay thế cho ALTER TABLE ADD COLUMN IF NOT EXISTSSQLite thuần túy không?

Câu trả lời:


64

Tôi có một phương pháp SQL thuần túy 99%. Ý tưởng là phiên bản lược đồ của bạn. Bạn có thể làm điều này theo hai cách:

  • Sử dụng lệnh pragma 'user_version' ( PRAGMA user_version) để lưu trữ một số gia tăng cho phiên bản giản đồ cơ sở dữ liệu của bạn.

  • Lưu trữ số phiên bản của bạn trong bảng xác định của riêng bạn.

Bằng cách này, khi phần mềm được khởi động, phần mềm có thể kiểm tra lược đồ cơ sở dữ liệu và nếu cần, hãy chạy ALTER TABLEtruy vấn của bạn , sau đó tăng phiên bản được lưu trữ. Điều này tốt hơn nhiều so với việc cố gắng cập nhật các bản cập nhật khác nhau một cách "mù quáng", đặc biệt nếu cơ sở dữ liệu của bạn phát triển và thay đổi một vài lần trong nhiều năm.


7
Giá trị ban đầu của là user_versiongì? Tôi cho rằng số không, nhưng sẽ rất tuyệt nếu thấy điều đó được ghi lại.
Craig McQueen

Ngay cả với điều này, nó có thể được thực hiện trong SQL thuần túy, vì sqlite không hỗ trợ IFALTER TABLEkhông có điều kiện? Bạn hiểu "99% SQL thuần" nghĩa là gì?
Craig McQueen

1
@CraigMcQueen Đối với giá trị ban đầu của user_version, nó có vẻ là 0, nhưng nó thực sự là một giá trị do người dùng xác định, vì vậy bạn có thể tạo giá trị ban đầu của riêng mình.
MPelletier

7
Câu hỏi về user_versiongiá trị ban đầu có liên quan khi bạn đã có một cơ sở dữ liệu hiện có và bạn chưa bao giờ sử dụng cơ sở dữ liệu user_versiontrước đây, nhưng bạn muốn bắt đầu sử dụng nó, vì vậy bạn cần giả sử sqlite đặt nó thành một giá trị ban đầu cụ thể.
Craig McQueen

1
@CraigMcQueen Tôi đồng ý, nhưng nó dường như không được ghi lại.
MPelletier

30

Một cách giải quyết là chỉ tạo các cột và bắt ngoại lệ / lỗi phát sinh nếu cột đó đã tồn tại. Khi thêm nhiều cột, hãy thêm chúng vào các câu lệnh ALTER TABLE riêng biệt để một bản sao không ngăn cản việc tạo các cột khác.

Với sqlite-net , chúng tôi đã làm điều gì đó như thế này. Nó không hoàn hảo, vì chúng tôi không thể phân biệt lỗi sqlite trùng lặp với các lỗi sqlite khác.

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 cũng hỗ trợ một câu lệnh pragma được gọi là "table_info" trả về một hàng trên mỗi cột trong bảng với tên của cột (và thông tin khác về cột). Bạn có thể sử dụng điều này trong một truy vấn để kiểm tra cột bị thiếu và nếu không có, hãy thay đổi bảng.

PRAGMA table_info(foo_table_name)

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


30
Câu trả lời của bạn sẽ tuyệt vời hơn nhiều nếu bạn cung cấp mã để hoàn thành tìm kiếm đó thay vì chỉ một liên kết.
Michael Alan Huff

PRAGMA table_info (tên_bảng). Lệnh này sẽ liệt kê mỗi cột của tên_bảng dưới dạng một hàng trong kết quả. Dựa trên kết quả này, bạn có thể xác định xem cột đó có tồn tại hay không.
Hao Nguyen

2
Có cách nào để thực hiện việc này bằng cách kết hợp pragma trong một phần của câu lệnh SQL lớn hơn để cột được thêm vào nếu nó không tồn tại nhưng nếu không thì không, chỉ trong một truy vấn duy nhất?
Michael

1
@Michael. Theo như tôi biết, không bạn không thể. Vấn đề với lệnh PRAGMA là bạn không thể truy vấn trên nó. lệnh không hiển thị dữ liệu cho công cụ SQL, nó trả về kết quả trực tiếp
Kowlown

1
Điều này không tạo ra một điều kiện đua? Giả sử tôi kiểm tra tên cột, thấy rằng cột của tôi bị thiếu, nhưng trong khi chờ đợi một quy trình khác sẽ thêm cột. Sau đó, tôi sẽ cố gắng thêm cột nhưng sẽ gặp lỗi vì nó đã tồn tại. Tôi đoán tôi phải khóa cơ sở dữ liệu trước hay sao? Tôi là một noob để sqlite tôi sợ :).
Ben Farmer

25

Nếu bạn đang thực hiện điều này trong một câu lệnh nâng cấp DB, có lẽ cách đơn giản nhất là chỉ bắt ngoại lệ được đưa ra nếu bạn đang cố gắng thêm một trường có thể đã tồn tại.

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
Tôi không thích lập trình kiểu ngoại lệ, nhưng điều này rõ ràng một cách đáng kinh ngạc. Có lẽ bạn đã làm tôi lung lay một chút.
Stephen J

Tôi cũng không thích nó, nhưng C ++ là ngôn ngữ lập trình kiểu ngoại lệ nhất từ ​​trước đến nay. Vì vậy, tôi đoán một người vẫn có thể thấy nó là "hợp lệ".
tmighty

Trường hợp sử dụng của tôi cho SQLite = Tôi không muốn viết thêm hàng đống mã cho thứ gì đó đơn giản ngu ngốc / một lớp lót bằng các ngôn ngữ khác (MSSQL). Câu trả lời hay ... mặc dù đó là "lập trình kiểu ngoại lệ" nó nằm trong một chức năng nâng cấp / bị cô lập nên tôi cho rằng nó có thể chấp nhận được.
maplemale

Trong khi những người khác không thích nó, tôi nghĩ rằng đây là giải pháp tốt nhất lol
Adam Varhegyi

13

threre là một phương thức của PRAGMA là table_info (tên_bảng), nó trả về tất cả thông tin của bảng.

Đây là cách triển khai cách sử dụng nó để kiểm tra cột tồn tại hay không,

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

Bạn cũng có thể sử dụng truy vấn này mà không cần sử dụng vòng lặp,

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

Con trỏ con trỏ = db.rawQuery ("select * from tableName", null); cột = cursor.getColumnNames ();
Vahe Gharibyan,

1
Tôi đoán bạn đã quên đóng con trỏ :-)
Pecana

@VaheGharibyan, vì vậy bạn sẽ chỉ cần chọn mọi thứ trong DB của mình chỉ để lấy tên cột ?! Những gì bạn đang nói đơn giản là we give no shit about performance:)).
Farid

Lưu ý, truy vấn cuối cùng không chính xác. Truy vấn thích hợp là: SELECT * FROM pragma_table_info(...)(lưu ý CHỌN và gạch dưới giữa pragma và thông tin bảng). Không chắc họ đã thực sự thêm nó vào phiên bản nào, nó không hoạt động trên 3.16.0 nhưng hoạt động trên 3.22.0.
PressingOnAlways

3

Đối với những người muốn sử dụng pragma table_info()kết quả của như một phần của SQL lớn hơn.

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

Phần quan trọng là sử dụng pragma_table_info('<table_name>')thay vì pragma table_info('<table_name>').


Câu trả lời này được lấy cảm hứng từ câu trả lời của @Robert Hawkey. Lý do tôi đăng nó như một câu trả lời mới là tôi không có đủ danh tiếng để đăng nó dưới dạng bình luận.


1

Tôi nghĩ ra câu hỏi này

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • Truy vấn bên trong sẽ trả về 0 hoặc 1 nếu cột tồn tại.
  • Dựa trên kết quả, hãy thay đổi cột

code = Error (1), message = System.Data.SQLite.SQLiteException (0x800007BF): Lỗi logic SQL gần "ALTER": lỗi cú pháp tại System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ


0

Tôi đã lấy câu trả lời ở trên trong C # /. Net, và viết lại nó cho Qt / C ++, không có nhiều thay đổi, nhưng tôi muốn để nó ở đây cho bất kỳ ai trong tương lai đang tìm kiếm câu trả lời 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

Ngoài ra, bạn có thể sử dụng câu lệnh CASE-WHEN TSQL kết hợp với pragma_table_info để biết liệu một cột có tồn tại hay không:

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

ở đây làm cách nào để chúng ta thay đổi bảng? khi có khớp tên cột?
user2700767

0

Đây là giải pháp của tôi, nhưng trong python (tôi đã thử và không tìm thấy bất kỳ bài đăng nào về chủ đề liên quan đến 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)

Tôi đã sử dụng PRAGMA để lấy thông tin bảng. Nó trả về một mảng đa chiều đầy đủ thông tin về các cột - một mảng trên mỗi cột. Tôi đếm số mảng để lấy số cột. Nếu không có đủ cột, thì tôi thêm các cột bằng lệnh ALTER TABLE.


0

Tất cả những câu trả lời này đều tốt nếu bạn thực hiện từng dòng một. Tuy nhiên, câu hỏi ban đầu là nhập một tập lệnh sql sẽ được thực thi bởi một lệnh thực thi db duy nhất và tất cả các giải pháp (như kiểm tra xem liệu cột có ở đó trước thời hạn hay không) sẽ yêu cầu chương trình thực thi phải có kiến ​​thức về bảng nào và cột đang được thay đổi / thêm vào hoặc xử lý trước và phân tích cú pháp tập lệnh đầu vào để xác định thông tin này. Thông thường, bạn sẽ không chạy nó trong thời gian thực hoặc thường xuyên. Vì vậy, ý tưởng bắt một ngoại lệ có thể chấp nhận được và sau đó tiếp tục. Trong đó, vấn đề nằm ở chỗ ... làm thế nào để tiếp tục. May mắn thay, thông báo lỗi cung cấp cho chúng tôi tất cả thông tin cần thiết để thực hiện việc này. Ý tưởng là thực thi sql nếu nó ngoại lệ trên lệnh gọi bảng thay đổi, chúng ta có thể tìm thấy dòng bảng thay đổi trong sql và trả về các dòng còn lại và thực thi cho đến khi thành công hoặc không tìm thấy dòng bảng thay đổi phù hợp nào nữa. Đây là một số mã ví dụ trong đó chúng ta có các tập lệnh sql trong một mảng. Chúng tôi lặp lại mảng thực thi từng tập lệnh. Chúng tôi gọi nó hai lần để nhận được lệnh bảng thay đổi không thành công nhưng chương trình thành công vì chúng tôi xóa lệnh bảng thay đổi khỏi sql và thực hiện lại mã đã cập nhật.

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

sản lượng dự kiến

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%'

Logic: cột sql trong sqlite_master chứa định nghĩa bảng, vì vậy nó chắc chắn chứa chuỗi với tên cột.

Khi bạn đang tìm kiếm một chuỗi con, nó có những hạn chế rõ ràng. Vì vậy, tôi khuyên bạn nên sử dụng chuỗi con hạn chế hơn nữa trong ColumnName, ví dụ như một cái gì đó như thế này (tùy thuộc vào thử nghiệm vì ký tự '' 'không phải lúc nào cũng có):

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

0

Tôi giải quyết nó trong 2 truy vấn. Đây là tập lệnh Unity3D của tôi sử dụng 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; }
            }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.