【问题标题】:Android P - 'SQLite: No Such Table Error' after copying database from assetsAndroid P - 从资产复制数据库后出现“SQLite:没有这样的表错误”
【发布时间】:2018-11-01 18:13:16
【问题描述】:

我的应用程序资产文件夹中保存了一个数据库,当应用程序首次打开时,我使用以下代码复制数据库。

inputStream = mContext.getAssets().open(Utils.getDatabaseName());

        if(inputStream != null) {

            int mFileLength = inputStream.available();

            String filePath = mContext.getDatabasePath(Utils.getDatabaseName()).getAbsolutePath();

            // Save the downloaded file
            output = new FileOutputStream(filePath);

            byte data[] = new byte[1024];
            long total = 0;
            int count;
            while ((count = inputStream.read(data)) != -1) {
                total += count;
                if(mFileLength != -1) {
                    // Publish the progress
                    publishProgress((int) (total * 100 / mFileLength));
                }
                output.write(data, 0, count);
            }
            return true;
        }

上面的代码运行没有问题,但是当你尝试查询数据库时,你得到一个 SQLite: No such table 异常。

此问题仅在 Android P 中出现,所有早期版本的 Android 都可以正常工作。

这是 Android P 的一个已知问题还是发生了一些变化?

【问题讨论】:

  • 你能检查你的 inputStream 是否不为空吗?使用 Android 调试器?
  • 我可以确认 InputStream 不为空。
  • return true 之前添加Log.d("COPYINFO","Bytes Copied = " + String.valueOf(totalcount) + " to " + filepath); 日志的结果输出是什么?
  • 运行 Android 8.1 的设备上的输出是 (D/COPYINFO: Bytes Copied = 1687552 to /data/user/0/am.radiogr/databases/s.db) 输出在 Android P 上是 (D/COPYINFO: Bytes Copied = 1687552 to /data/user/0/am.radiogr/databases/s.db) 它们完全一样。
  • 我的下一步,显然正在复制数据库,将检查数据库中的表。要么查询 sqlite_master 表,要么我个人从这里使用 logDatabaseInfo 方法[是否有任何方法可以帮助解决常见的 SQLite 问题? ](stackoverflow.com/questions/46642269/…)。

标签: android sqlite android-9.0-pie


【解决方案1】:

遇到了类似的问题,并将其添加到我的 SQLiteOpenHelper 中解决了这个问题

    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        db.disableWriteAheadLogging();
    }

显然,Android P 设置了 PRAGMA Log 的东西不同。仍然不知道是否会产生副作用,但似乎有效!

【讨论】:

  • 很好的答案,但对于 nitpick,onConfigure 是一个更好的地方。 onConfigure 的 javadoc 特别提到它是 enableWriteAheadLogging 之类的地方。在我的测试中,这两个地方都可以解决 Android 9 上的问题。
  • 从我这里工作!如果在我的情况下仅在 9 Android 版本上出现“问题”: if(Build.VERSION.SDK_INT >= 28) {database.disableWriteAheadLogging();}
  • 我也遇到了同样的问题,它在版本 9 以下的设备上运行良好,现在使用您的解决方案,它在 Pie 上也运行良好,谢谢
  • 它对我有用。我在 Android9 上遇到了错误。谢谢
  • 这对我有用,但是如果我们禁用 WAL,它会根据 source.android.com/devices/tech/perf/compatibility-wal 将写入量减少 10% 到 15%
【解决方案2】:

我的 Android P 问题通过添加解决了 'this.close()' 在 createDataBase() 方法中 this.getReadableDatabase() 之后,如下所示。

private void createDataBase() throws IOException {
    this.getReadableDatabase();
    this.close(); 
    try {           
        copyDataBase();            
    } catch (IOException e) {           
        throw new RuntimeException(e);
    }
}

【讨论】:

  • 我遇到了确切的问题,解决方案也有效,但我很好奇这个问题,为什么以及如何解决 Android P 中的问题?
  • 谢谢!我花了 3 天时间寻找解决方案,但为什么?
【解决方案3】:

与以前的版本相比,这个问题在 Android P 上导致崩溃的频率似乎要高得多,但这并不是 Android P 本身的错误。

问题在于,您将值分配给 String filePath 的行打开了与数据库的连接,当您从资产复制文件时,该连接仍保持打开状态。

要解决问题,请替换该行

String filePath = mContext.getDatabasePath(Utils.getDatabaseName()).getAbsolutePath();

用代码获取文件路径值然后关闭数据库:

MySQLiteOpenHelper helper = new MySQLiteOpenHelper();
SQLiteDatabase database = helper.getReadableDatabase();
String filePath = database.getPath();
database.close();

并且还添加了一个内部辅助类:

class MySQLiteOpenHelper extends SQLiteOpenHelper {

    MySQLiteOpenHelper(Context context, String databaseName) {
        super(context, databaseName, null, 2);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

【讨论】:

  • 感谢您的持续帮助。我用您的代码替换了代码以获取 filePath,但相同的结果仍然存在。创建了一个空数据库,并且没有使用 assets 文件夹中的数据库填充。我应该补充一下,我正在使用运行 Android P 而不是实际设备的模拟器进行测试,这不应该阻止代码按预期工作吗?
  • 我可以在模拟器上重现这个问题,这不是问题。不过,也许您应该确保关闭即时运行。无论如何,当我确保在从资产复制文件之前没有打开与数据库的连接时,问题就解决了。这就是关键。
  • 抱歉,您是对的。我忽略了我之前调用过这个(相当无意义的)代码:openOrCreateDatabase(Utils.getDatabaseName(), MODE_PRIVATE, null); 这保持了与数据库的开放连接并导致了问题。我已经删除了该代码并且一切正常。感谢您的帮助!
  • 不错!很高兴你明白了。
  • 禁用预写日志,正如 Ramon 在下面指出的那样对我有用。 stackoverflow.com/a/51953955/1172181
【解决方案4】:

我遇到了类似的问题。我正在复制数据库,但不是从资产中复制。我发现这个问题根本与我的数据库文件复制代码无关。它也与打开、未关闭、刷新或同步的文件无关。我的代码通常会覆盖现有的未打开数据库。 Android Pie 似乎是新的/不同的,并且与以前的 Android 版本不同的是,当 Android Pie 创建 SQLite 数据库时,它默认将 journal_mode 设置为 WAL(预写日志记录)。我从来没有使用过 WAL 模式,SQLite 文档说 journal_mode 默认应该是 DELETE。问题是如果我覆盖现有的数据库文件,我们称之为 my.db,预写日志 my.db-wal 仍然存在,并且有效地“覆盖”了新复制的 my.db 文件中的内容。当我打开我的数据库时,sqlite_master 表通常只包含一行 android_metadata。我期待的所有桌子都不见了。我的解决方案是在打开数据库后简单地将 journal_mode 设置回 DELETE,尤其是在使用 Android Pie 创建新数据库时。

PRAGMA journal_mode=DELETE;

也许 WAL 更好,并且可能有某种方法可以关闭数据库,这样预写日志就不会妨碍我,但我并不真正需要 WAL,并且对于所有以前的 Android 版本都不需要它.

【讨论】:

  • 这是一个很好的解释,比其他解释更详细。谢谢
【解决方案5】:

不幸的是,在非常具体的情况下,公认的答案只是“碰巧起作用”,但它并没有给出始终如一的工作建议来避免在 Android 9 中出现此类错误。

这里是:

  1. 在您的应用程序中有一个 SQLiteOpenHelper 类实例来访问您的数据库。
  2. 如果您需要重写/复制数据库,请使用此实例的 SQLiteOpenHelper.close() 方法关闭数据库(并关闭与此数据库的所有连接),并且不再使用此 SQLiteOpenHelper 实例。

调用 close() 后,不仅所有与数据库的连接都被关闭,而且额外的数据库日志文件被刷新到主 .sqlite 文件并被删除。所以你只有一个 database.sqlite 文件,可以重写或复制。

  1. 在复制/重写等之后,创建一个新的 SQLiteOpenHelper 单例,getWritableDatabase() 方法将返回 SQLite 数据库的新实例!并使用它直到下次您需要复制/重写您的数据库...

这个答案帮助我弄清楚了:https://stackoverflow.com/a/35648781/297710

我在 Android 9 中的 AndStatus 应用程序 https://github.com/andstatus/andstatus 中遇到了这个问题,它有相当大的自动化测试套件,在此提交之前一直在 Android 9 模拟器中重现“SQLiteException:没有这样的表”: https://github.com/andstatus/andstatus/commit/1e3ca0eee8c9fbb8f6326b72dc4c393143a70538 所以如果你真的很好奇,你可以在这个提交之前和之后运行所有测试来看看有什么不同。

【讨论】:

    【解决方案6】:

    不禁用 WAL 的解决方案

    Android 9 引入了一种名为 Compatibility WAL(预写登录)的 SQLiteDatabase 特殊模式,它允许数据库使用“journal_mode=WAL”,同时保留每个数据库最多保持一个连接。

    这里有详细信息:
    https://source.android.com/devices/tech/perf/compatibility-wal

    这里详细讲解SQLite WAL模式:
    https://www.sqlite.org/wal.html

    在官方文档中,WAL 模式添加了一个 第二个数据库文件,名为 databasename 和“-wal”。因此,如果您的数据库名为“data.db”,则它在同一目录中称为“data-wal.db”。

    现在的解决方案是在 Android 9 上保存和恢复 BOTH 文件(data.db 和 data-wal.db)。

    之后它就像在早期版本中一样工作。

    【讨论】:

    • 我想如何生成一个 data-wal.db 文件?模拟器可以生成吗?因为问题出现在真实设备上,而不是模拟器上。
    【解决方案7】:

    我有同样的事情,我在 android 版本 4 中有一个应用程序,当更新我的具有 android 9 的手机时,我花了 2 天时间试图找到错误,感谢我的案例中的 cmets,我只需要添加this.close();

    private void createDataBase () throws IOException {
         this.getReadableDatabase ();
         this.close ();
         try {
             copyDataBase ();
         } catch (IOException e) {
             throw new RuntimeException (e);
         }
    }
    

    准备运行所有版本!

    【讨论】:

      【解决方案8】:

      首先,感谢您发布这个问题。我也发生了同样的事情。一切运行良好,但是在针对 Android P Preview 进行测试时,我遇到了崩溃。这是我为这段代码发现的错误:

      private void copyDatabase(File dbFile, String db_name) throws IOException{
          InputStream is = null;
          OutputStream os = null;
      
          SQLiteDatabase db = context.openOrCreateDatabase(db_name, Context.MODE_PRIVATE, null);
          db.close();
          try {
              is = context.getAssets().open(db_name);
              os = new FileOutputStream(dbFile);
      
              byte[] buffer = new byte[1024];
              while (is.read(buffer) > 0) {
                  os.write(buffer);
              }
          } catch (IOException e) {
              e.printStackTrace();
              throw(e);
          } finally {
              try {
                  if (os != null) os.close();
                  if (is != null) is.close();
      
              } catch (IOException e) {
                  e.printStackTrace();
              }
      
          }
      }
      

      我遇到的问题是这段代码运行良好,但在 SDK 28+ openOrCreateDatabase 中不再自动为您创建 android_metadata 表。因此,如果您执行“select * from TABLE”的查询,它将找不到该 TABLE,因为该查询开始关注应该是元数据表的“第一个”表。我通过手动添加 android_metadata 表解决了这个问题,一切都很好。希望其他人觉得这很有用。花了很长时间才弄清楚,因为特定的查询仍然可以正常工作。

      【讨论】:

      • 如何添加android_metadata 表?你没有在回答中说出来
      • 我使用 SQLite 的 DB 浏览器添加了它。您可以使用该程序手动添加表格。
      【解决方案9】:

      类似问题,只有 Android P 设备受到影响。所有以前的版本都没有问题。

      在 Android 9 设备上关闭了自动恢复功能。

      我们这样做是为了解决问题。不推荐用于生产案例。

      在数据库助手中调用复制数据库函数之前,自动恢复将数据库文件的副本放置在数据目录中。因此 a file.exists() 返回 true。

      从开发设备备份的数据库缺少表。因此“未找到表”实际上是正确的。

      【讨论】:

      • 嘿,我目前面临同样的问题。我无法理解它如何恢复文件的副本但缺少一个表,这怎么可能?关于如何在生产中解决它的任何线索?
      【解决方案10】:

      这里是这个问题的完美解决方案:

      只需在 SQLiteOpenHelper 类中重写此方法即可:

      @Override
      public void onOpen(SQLiteDatabase db) {
          super.onOpen(db);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
              db.disableWriteAheadLogging();
          }
      }
      

      【讨论】:

        【解决方案11】:

        您似乎没有关闭输出流。虽然它可能无法解释为什么没有真正创建数据库(除非 Android P 添加了多 MB 缓冲区),但使用 try-with-resource 是一个好习惯,例如:

        // garantees that the data are flushed and the resources freed
        try (FileOutputStream output = new FileOutputStream(filePath)) {
            byte data[] = new byte[1024];
            long total = 0;
            int count;
            while ((count = inputStream.read(data)) != -1) {
                total += count;
                if (mFileLength != -1) {
                    // Publish the progress
                    publishProgress((int) (total * 100 / mFileLength));
                }
                output.write(data, 0, count);
            }
        
            // maybe a bit overkill
            output.getFD().sync();
        }
        

        【讨论】:

        • 不幸的是,关闭FileOutputStream 没有效果。同样切换到“使用资源尝试”方法会导致相同的空数据库。
        【解决方案12】:

        在 Android PIE 及以上版本中使用以下行作为数据库文件路径的最简单答案:

        DB_NAME="xyz.db";
        DB_Path = "/data/data/" + BuildConfig.APPLICATION_ID + "/databases/"+DB_NAME;
        

        【讨论】:

        • 这不能回答问题。尽管这是适用于 Android 6.0 及更低版本的正确数据库路径,但它在高于该版本的版本上持平,因为它使用每个用户的应用程序存储。最好使用context.getDatabasePath()
        【解决方案13】:

        我无法对已接受的答案发表评论,因此我必须打开一个新答案。

        mContext.getDatabasePath() 不会打开数据库连接,它甚至不需要现有的文件名即可成功(参见 sources/android-28/android/app/ContextImpl.java):

        @Override
        public File getDatabasePath(String name) {
            File dir;
            File f;
        
            if (name.charAt(0) == File.separatorChar) {
                // snip
            } else {
                dir = getDatabasesDir();
                f = makeFilename(dir, name);
            }
        
            return f;
        }
        
        private File makeFilename(File base, String name) {
            if (name.indexOf(File.separatorChar) < 0) {
                return new File(base, name);
            }
            throw new IllegalArgumentException(
                    "File " + name + " contains a path separator");
        }
        

        【讨论】:

          【解决方案14】:

          在版本 P 中,主要变化是 WAL(预写日志)。需要以下两个步骤。

          1. 在资源下的 values 文件夹中的 config.xml 中的以下行禁用它。

          1. 在 createDatabase 方法中的 DBAdapter 类中进行以下更改。否则,具有早期 Android 版本的手机会崩溃。

            private void createDataBase() 抛出 IOException {

            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
                            this.getWritableDatabase();
                try {           
                    copyDataBase();            
                } catch (IOException e) {           
                    throw new RuntimeException(e);
                }
            }
            

            }

          【讨论】:

            【解决方案15】:

            Android Pie 中出现的问题, 解决办法是:

             SQLiteDatabase db = this.getReadableDatabase();
                    if (db != null && db.isOpen())
                        db.close();
               copyDataBase();
            

            【讨论】:

              猜你喜欢
              • 2017-05-14
              • 1970-01-01
              • 2019-03-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多