SQLiteOpenHelper问题:完全限定的数据库路径名有问题

24

在我的应用中,我使用...

myFilesDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
                      + "/Android/data/" + packageName + "/files");
myFilesDir.mkdirs();

这很好,结果路径是...

/mnt/sdcard/Android/data/com.mycompany.myApp/files

我需要一个SQLite数据库,希望将其存储在SD卡上,因此我扩展了SQLiteOpenHelper如下...

public class myDbHelper extends SQLiteOpenHelper {

    public myDbHelper(Context context, String name, CursorFactory factory, int version) {
        // NOTE I prefix the full path of my files directory to 'name'
        super(context, myFilesDir + "/" + name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // Create tables and populate with default data...
    }
}

到目前为止还不错 - 第一次调用getReadableDatabase()getWriteableDatabase()方法时,空的数据库将在SD卡上创建,并且onCreate()方法将填充它。

这里出现了问题 - 应用程序处于beta测试阶段,可能有5或6个人参与测试,他们都运行Android v2.2,一切正常。然而,我有一个测试者运行v2.1,当myDbHelper尝试在第一次使用时创建数据库时,应用程序崩溃,提示以下错误信息...

E/AndroidRuntime( 3941): Caused by: java.lang.IllegalArgumentException: File /nand/Android/data/com.mycompany.myApp/files/myApp-DB.db3 contains a path separator
E/AndroidRuntime( 3941): at android.app.ApplicationContext.makeFilename(ApplicationContext.java:1445)
E/AndroidRuntime( 3941): at android.app.ApplicationContext.openOrCreateDatabase(ApplicationContext.java:473)
E/AndroidRuntime( 3941): at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:193)
E/AndroidRuntime( 3941): at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:98)
E/AndroidRuntime( 3941): at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:158)

获取文件目录的路径是一个奇怪的过程(“/nand”),因为它是内部存储器,虽然不是手机自身的内部存储器 - 但对于该设备,getExternalStorageDirectory() 返回的确实是这个路径。

我看到了三种可能的答案...

  1. 虽然在v2.2上可接受,但为DB名称指定完全限定路径并不推荐,并且在早期版本上会失败。
  2. 全限定路径适用于SD卡存储,但"/nand"路径被解释为“内部”,在这种情况下仅接受相对路径。
  3. 还有其他一些我完全没有注意到的东西。

如果上述任何一个或所有的内容都适用,我希望有人能帮助我应该如何处理。

谢谢。


之前不是已经问过了吗?好像旧的被删除了。 - Codemonkey
@Klaus:是的,我发了这个问题,但是过去14个小时只有大约10个浏览量,没有任何评论,所以我稍微编辑了一下,然后通过删除旧帖子重新发布来“顶”它。 - Squonk
5个回答

47

如果您可以在目标目录中具有写访问权限,并且提供了自定义的ContextClass,则可以使用带有自定义路径的SQLiteOpenHelper

public class DatabaseHelper extends SQLiteOpenHelper {
  private static final int DATABASE_VERSION = 3;
    .....

  DatabaseHelper(final Context context, String databaseName)  {
    super(new DatabaseContext(context), databaseName, null, DATABASE_VERSION);
  }
}

这里是定制的DatabaseContext类,它完成所有的魔法:

class DatabaseContext extends ContextWrapper {

  private static final String DEBUG_CONTEXT = "DatabaseContext";

  public DatabaseContext(Context base) {
    super(base);
  }

  @Override
  public File getDatabasePath(String name)  {
    File sdcard = Environment.getExternalStorageDirectory();    
    String dbfile = sdcard.getAbsolutePath() + File.separator+ "databases" + File.separator + name;
    if (!dbfile.endsWith(".db")) {
      dbfile += ".db" ;
    }

    File result = new File(dbfile);

    if (!result.getParentFile().exists()) {
      result.getParentFile().mkdirs();
    }

    if (Log.isLoggable(DEBUG_CONTEXT, Log.WARN)) {
      Log.w(DEBUG_CONTEXT, "getDatabasePath(" + name + ") = " + result.getAbsolutePath());
    }

    return result;
  }

  /* this version is called for android devices >= api-11. thank to @damccull for fixing this. */
  @Override
  public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
    return openOrCreateDatabase(name,mode, factory);
  }

  /* this version is called for android devices < api-11 */
  @Override
  public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) {
    SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
    // SQLiteDatabase result = super.openOrCreateDatabase(name, mode, factory);
    if (Log.isLoggable(DEBUG_CONTEXT, Log.WARN)) {
      Log.w(DEBUG_CONTEXT, "openOrCreateDatabase(" + name + ",,) = " + result.getPath());
    }
    return result;
  }
}

2012年6月更新:
这是如何工作的(@barry的问题):

普通的Android应用程序将其本地数据库文件相对于应用程序文件夹。通过使用一个自定义上下文并覆盖getDatabasePath(),现在数据库相对于SD卡上的不同目录。

2015年2月更新:
在用新的Android 4.4设备替换我的旧Android 2.2设备后,我发现我的解决方案不再起作用。 感谢@damccull-s的回答,我能够解决问题。我已经更新了这个答案,所以这应该是一个可行的示例。

2017年5月更新:

统计数据:这种方法在200多个GitHub项目中使用


不错的例子。我使用了内部存储器来存储数据库,但将来可能会重新考虑使用SD卡。感谢分享你的代码 - 我会收藏这个并在下次更新应用程序时尝试它。 - Squonk
谢谢。好的例子。我在摩托罗拉Mileston上遇到了相同的问题。 - kakopappa
它确实似乎可以工作,但我不确定如何-在重写的openOrCreateDatabase中,它使用File对象调用super.openOrCreateDatabase。文档说这相当于调用Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY)。但是getPath()只返回带有分隔符的路径,因此我不明白这与原始问题有什么不同...有什么想法吗? - barry
2
太棒了,谢谢!在1.5(杯子蛋糕)上似乎也能正常工作。在我看来,这应该被标记为采纳的答案。 - Mark
你做得很好!这是唯一一个优雅的解决方案,它实际上可以在所有 Android 版本上运行。干得好! - George Pligoropoulos
显示剩余2条评论

17

历史上,您无法在 SQLiteOpenHelper 中使用路径。它只适用于简单的文件名。我没有意识到在 Android 2.2 中放宽了这个限制。

如果您希望在SD卡上使用数据库,并且希望支持 Android 2.1 及更早版本,则不能使用 SQLiteOpenHelper

抱歉!


1
不用道歉,你给出了明确的答案,而且基本上是我预期的(但不确定)。重新调整/重新思考我的代码可能会有点麻烦,但至少我现在知道我需要做什么了。非常感谢。 - Squonk
2
我刚刚创建了一个解决方案,适用于我的Android 2.2(可能也适用于其他版本)。请参见下面的答案。 - k3b
1
这是一个相当无用的答案。下面的答案应该被接受。 - zeeshan

11

k3b的回答非常棒。它让我实现了目标。然而,在使用API等级为11或更高版本的设备上,您可能会发现它停止工作。这是因为添加了openOrCreateDatabase()方法的新版本。现在它包含以下签名:

openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags, DatabaseErrorHandler errorHandler)

这似乎是一些设备默认调用的方法。

为了让这个方法在这些设备上工作,你需要进行以下修改:

首先,编辑你已有的方法,使其简单地返回对新方法的调用结果。

@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
        CursorFactory factory) {
    return openOrCreateDatabase(name, mode, factory, null);

}

其次,使用以下代码添加新的覆盖。

@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
    SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name).getAbsolutePath(),null,errorHandler);

    return result;
}

这段代码与k3b的代码非常相似,但请注意,SQLiteDatabase.openOrCreateDatabase接受一个字符串而不是文件,我使用了允许 DatabaseErrorHandler 对象的版本。


3
这让我的应用程序从安卓2.2升级到安卓4.4后得以修复。我已将此添加到我的回答中,以便其他用户可以获得一个可行的例子。+1 - k3b

2

user2371653的回答非常好。 但我发现了一个问题:

@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
        CursorFactory factory) {
    return openOrCreateDatabase(name, mode, factory, null);
}

如果在安卓2.x上安装您的应用程序,可能会导致崩溃。

因此,我们可以进行如下修改:

@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
        CursorFactory factory) {
    return super.openOrCreateDatabase(getDatabasePath(name).getAbsolutePath(), mode, factory);
}

由于Android 2.x没有API支持,

openOrCreateDatabase(String name, int mode, 
CursorFactory factory, DatabaseErrorHandler errorHandler)

1

我认为在这里找到了一个更好的解决方案(SD卡上的SQLite数据库),想要告诉你。请注意构造函数中的条目。

public class TestDB extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "usertest.db";
    private static final int DATABASE_VERSION = 1;

    public TestDB (Context context){
        super(context, context.getExternalFilesDir(null).getAbsolutePath() + "/" +  DATABASE_NAME, null, DATABASE_VERSION );
    }
    ...
}

用户网站引用:
“应用程序将在SD卡上的应用程序文件夹中创建数据库:/sdcard/Android/data/[your_package_name]/files。这样,通过Android,数据库将被视为应用程序的一部分,并在用户卸载应用程序时自动删除。”

“在我的应用程序中,我有一个大型数据库,在大多数情况下,它不适合旧手机的内部存储器,例如HTC Desire。它在SD卡上运行良好,并且大多数应用程序本身都会“移至SD卡”,因此不必担心无法访问数据库,因为应用程序本身也不可访问。”


你能告诉我们这个程序适用于哪个Android版本吗?我在我的Android API 8设备上尝试了一下,但是它没有起作用。可能只适用于更新的API。 - k3b
@k3b:抱歉,我使用的是API级别14。 - KingAlex1985

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接