【问题标题】:ALTER TABLE ADD COLUMN IF NOT EXISTS in SQLite如果 SQLite 中不存在,请更改表添加列
【发布时间】:2011-04-05 23:33:09
【问题描述】:

我们最近需要向我们现有的一些 SQLite 数据库表中添加列。这可以通过ALTER TABLE ADD COLUMN 完成。当然,如果表已经改变,我们想不管它。不幸的是,SQLite 不支持 ALTER TABLE 上的 IF NOT EXISTS 子句。

我们当前的解决方法是执行 ALTER TABLE 语句并忽略任何“重复的列名”错误,就像 this Python example(但在 C++ 中)。

然而,我们设置数据库模式的常用方法是使用包含CREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS 语句的.sql 脚本,可以使用sqlite3_execsqlite3 命令行工具执行这些脚本。我们不能将ALTER TABLE 放在这些脚本文件中,因为如果该语句失败,则不会执行它之后的任何内容。

我希望将表定义放在一个位置,而不是在 .sql 和 .cpp 文件之间拆分。有没有办法在纯 SQLite SQL 中为 ALTER TABLE ADD COLUMN IF NOT EXISTS 编写解决方法?

【问题讨论】:

    标签: sqlite alter-table


    【解决方案1】:

    我有一个 99% 纯 SQL 方法。这个想法是对您的架构进行版本控制。您可以通过两种方式做到这一点:

    • 使用 'user_version' pragma 命令 (PRAGMA user_version) 存储数据库架构版本的增量编号。

    • 将您的版本号存储在您自己定义的表中。

    这样,当软件启动时,它可以检查数据库模式,如果需要,运行ALTER TABLE 查询,然后增加存储的版本。这比“盲目”尝试各种更新要好得多,尤其是当您的数据库多年来不断增长和更改时。

    【讨论】:

    • user_version的初始值是多少?我假设为零,但很高兴看到记录在案。
    • 即便如此,是否可以在纯 SQL 中完成,因为 sqlite 不支持 IF 并且 ALTER TABLE 没有条件? “99% 纯 SQL”是什么意思?
    • @CraigMcQueen 至于user_version的初始值,貌似是0,其实是用户自定义的值,所以可以自己做初始值。
    • user_version 初始值的问题在你有一个现有的数据库时是相关的,并且你以前从未使用过user_version,但是你想开始使用它,所以你需要假设 sqlite 将其设置为特定的初始值。
    • @CraigMcQueen 我同意,但似乎没有记录。
    【解决方案2】:

    一种解决方法是仅创建列并捕获列已存在时出现的异常/错误。添加多个列时,请将它们添加到单独的 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));
        }
    }
    

    【讨论】:

      【解决方案3】:

      如果您在数据库升级语句中执行此操作,也许最简单的方法是在您尝试添加可能已经存在的字段时捕获引发的异常。

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

      【讨论】:

      • 我不喜欢异常风格的编程,但它非常干净。也许你有点动摇了我。
      • 我也不喜欢它,但 C++ 是有史以来最异常风格的编程语言。所以我想人们可能仍然认为它是“有效的”。
      • 我的 SQLite 用例 = 我不想为其他语言 (MSSQL) 中的一些愚蠢的简单/一个衬里做大量额外的编码。很好的答案......虽然它是“异常风格编程”,但它在升级功能/隔离中,所以我认为它是可以接受的。
      • 虽然其他人不喜欢,但我认为这是最好的解决方案,哈哈
      【解决方案4】:

      SQLite 还支持名为“table_info”的 pragma 语句,该语句返回表中的每列一行,并带有列名(以及有关该列的其他信息)。您可以在查询中使用它来检查缺少的列,如果不存在则更改表。

      PRAGMA table_info(foo_table_name)
      

      示例输出:

      cid name type notnull dflt_value pk
      0 id integer 0 null 1
      1 type text 0 null 0
      2 data json 0 null 0

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

      【讨论】:

      • 如果您提供用于完成该搜索的代码而不仅仅是一个链接,您的答案会更加出色。
      • 有没有办法通过在较大的 SQL 语句的一部分中组合编译指示来做到这一点,这样如果该列不存在但不存在,则仅在单个查询中添加该列?
      • @迈克尔。据我所知,不,你不能。 PRAGMA 命令的问题是您无法对其进行查询。该命令不向 SQL 引擎呈现数据,它直接返回结果
      • 这不会造成竞争条件吗?假设我检查了列名,发现我的列丢失了,但与此同时另一个进程添加了该列。然后我将尝试添加该列,但会出现错误,因为它已经存在。我想我应该先锁定数据库还是什么?恐怕我是 sqlite 的菜鸟 :)。
      • @BenFarmer 可能你想在交易中这样做。
      【解决方案5】:

      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("select * from tableName" , null); columns = cursor.getColumnNames();
      • 我猜你忘了关闭光标:-)
      • @VaheGharibyan,所以您只需选择数据库中的所有内容即可获取列名?!你只是说we give no shit about performance :))。
      • 注意,最后一个查询不正确。正确的查询是:SELECT * FROM pragma_table_info(...)(注意编译指示和表信息之间的 SELECT 和下划线)。不确定他们实际添加的版本,它在 3.16.0 上不起作用,但在 3.22.0 上起作用。
      【解决方案6】:

      对于那些想要将pragma table_info() 的结果用作更大 SQL 的一部分的人。

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

      关键部分是使用pragma_table_info('&lt;table_name&gt;')而不是pragma table_info('&lt;table_name&gt;')


      这个答案的灵感来自@Robert Hawkey 的回复。我将其发布为新答案的原因是我没有足够的声誉将其发布为评论。

      【讨论】:

        【解决方案7】:

        如果您在 flex/adobe air 中遇到此问题并首先发现自己在这里,我找到了解决方案,并将其发布在相关问题上:ADD COLUMN to sqlite db IF NOT EXISTS - flex/air sqlite?

        我在这里评论:https://stackoverflow.com/a/24928437/2678219

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

            您也可以将 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') 
            

            【讨论】:

            • 这里我们如何修改表?什么时候有列名匹配?
            【解决方案10】:

            这是我的解决方案,但在 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 命令添加列。

            【讨论】:

              【解决方案11】:

              如果您一次执行一行,所有这些答案都很好。但是,最初的问题是输入一个 sql 脚本,该脚本将由单个 db execute 执行,并且所有解决方案(例如提前检查列是否存在)都需要执行程序要么知道哪些表,要么正在更改/添加列或对输入脚本进行预处理和解析以确定此信息。通常,您不会实时或经常运行它。所以捕获异常的想法是可以接受的,然后继续。这就是问题所在……如何继续前进。幸运的是,错误消息为我们提供了执行此操作所需的所有信息。这个想法是如果在alter table调用上出现异常则执行sql,我们可以在sql中找到alter table行并返回剩余的行并执行直到它成功或找不到更多匹配的alter table行。这是一些示例代码,其中我们在数组中有 sql 脚本。我们迭代执行每个脚本的数组。我们调用它两次以使 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 ) ) 
                      )
              -------------
              

              【讨论】:

                【解决方案12】:

                我想出了这个查询

                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):“ALTER”附近的 SQL 逻辑错误:System.Data.SQLite.SQLite3.Prepare 出现语法错误跨度>
                • 字符串(product 和 purchaseCopy)周围的 2 个简单引号出现拼写错误,但由于“THEN ALTER TABLE”,我无法使其工作。你确定有可能吗?如果这可行,它应该是公认的答案。
                【解决方案13】:
                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%'
                

                【讨论】:

                  【解决方案14】:

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

                  【讨论】:

                    【解决方案15】:

                    显然……在 SQLite 中……如果列已经存在,“alter table”语句不会产生异常。

                    在支持论坛找到this的帖子并进行了测试。

                    【讨论】:

                      猜你喜欢
                      • 2014-12-03
                      • 1970-01-01
                      • 2020-11-25
                      • 1970-01-01
                      • 2011-02-06
                      • 2012-04-15
                      • 1970-01-01
                      • 2011-07-05
                      相关资源
                      最近更新 更多