在 Android 中打开外部数据库时出现错误消息

Error message when opening external database in Android

我想在 Android 中加载一个外部数据库。我使用了教程 Read External Database with SQLiteOpenHelper on Android 中的方法。我在 src/main/assets 中创建了一个名为“assets”的文件夹,并在其中插入了数据库 'Test_DB'。不幸的是,当我启动应用程序时,我收到错误消息:

Cannot open database 'C:/Users/user/Projects/AndroidStudioProjects/Bapp/Bapp_Projekt/app/src/main/assets/Test_DB.db': Directory C:/Users/user/Projects/AndroidStudioProjects/Bapp/Bapp_Projekt/app/src/main/assets doesn't exist

尽管数据库 'Test_DB.db' 存在于上述路径中。您知道导致问题的原因以及我如何读取此外部数据库文件吗?

这里可以看到'DatabaseContext'class读取外部数据库的代码:

package com.example.td.bapp;


import android.content.Context;
import android.content.ContextWrapper;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import android.util.Log;

import java.io.File;

public class DatabaseContext extends ContextWrapper {

    private static final String DEBUG_CONTEXT = "DatabaseContext";

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

    @Override
    public File getDatabasePath(String name)  {

        String dbfile = "C:/Users/user/Projects/AndroidStudioProjects/Bapp/Bapp_Projekt/app/src/main/assets/" + 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);

        if (Log.isLoggable(DEBUG_CONTEXT, Log.WARN)) {
            Log.w(DEBUG_CONTEXT, "openOrCreateDatabase(" + name + ",,) = " + result.getPath());
        }
        return result;
    }
}

并且这个 class 在另一个 class 'DataBaseHelper' 中调用,它扩展了 SQLiteOpenHelper 并且具有以下构造函数

public DataBaseHelper(@Nullable Context context, String name) {
   // super(context, DATABASE, null, 1);
    super(new DatabaseContext(context), name, null, 1);

}

我会感谢您的每一个意见,并非常感谢您的帮助。

我认为问题可能是我自己创建了文件夹 'assets',因为它以前不存在。我如何告诉 Android 这个 'assets' 文件夹应该包含在应用程序中?

更新: 我按照 MikeT 的建议做了,并利用了 SQLiteAssetHelper 。现在可以加载数据库了。但是,数据库的每个查询都不会 return 任何东西,就好像数据库是空的一样。绝对不是这样。例如,以下查询产生的 rowCount 为 0,这是不正确的

public Cursor getDataDB_TableItemNamesByItemType(String itemType) {

    SQLiteDatabase db = this.getWritableDatabase();
    Cursor res = db.rawQuery("select * from " + TABLE_ITEM + " where "
            + ITEM_TYPE + " = '" + itemType+ "'", null);
    Log.e("LogTag", "res.getCount(): " + res.getCount());
    return res;

}

如果我在 SQL 数据库上执行完全相同的查询,我会得到正的行计数(取决于参数 itemType)。你能想象为什么我的外部读取数据库看起来是空的吗?

你的主要问题是这一行

String dbfile = "C:/Users/user/Projects/AndroidStudioProjects/Bapp/Bapp_Projekt/app/src/main/assets/" + name;

您基本上是在尝试让设备查看您的源代码,而不是在设备资产文件夹中查找资产文件(数据库)。

Android Studio(假设您正在使用它)在制作安装包时将文件放置在资产文件夹中,而不是 c:/Users/user/Projects ....

在教程中(不使用资产文件)但将文件放在它使用的 SD 卡上:-

String dbfile = sdcard.getAbsolutePath() + File.separator+ "database" + File.separator + name;

但是,由于您已将文件复制到项目的资产文件夹中,因此您要做的是将文件(数据库)从资产文件夹复制到一个位置(您不能直接使用资产文件夹中的数据库因为它被压缩了)。

这里有一个快速指南,说明如何完成上述操作(请注意,第 10 步是针对 tables/columns 的,必须相应地进行更改how to launch app with SQLite darabase on Android Studio emulator?

另一种方法是使用 SQLiteAssetHelper 这是一个使用 SQLiteAssetHelper 的指南



基于您的代码的示例

阶段 1 - 创建外部数据库

使用 Navicat 作为 SQLite 工具(我的首选工具)

数据库是用一个 table 创建的,即 item table 有 2 列,即 item_nameitem_type

4 行插入了 2 行 item_type 类型 1 和 2 行 item_type 类型 2 根据 :-

数据库连接已关闭,因此已保存,然后重新打开连接以确认数据库确实已填充,然后再次关闭连接。

然后确认数据库文件的存在位于适当的位置,如下所示:-

阶段 2 - 将数据库文件复制到资产文件夹中

在本例中,项目名为 SO66390748(问题 #)。

assets 文件夹是在 Apps src/main 文件夹中创建的,因为默认情况下它不存在,然后将文件复制到该文件夹​​中,注意大小是正如预期的那样。

然后在 Android Studio 中测试了项目文件结构,以确认资产文件夹中存在数据库:-

阶段 3 - 根据

创建 DatabaseHelper class
class DataBaseHelper extends SQLiteOpenHelper {

    private static final String TAG = "DBHELPER";
    public static final String DBNAME = "TestDB.db"; //<< Name of the database file in the assets folder
    public static final int DBVERSION = 1;
    public static final String TABLE_ITEM = "item";
    public static final String ITEM_NAME = "item_name";
    public static final String ITEM_TYPE = "item_type";

    SQLiteDatabase mDB;

    public DataBaseHelper(Context context) {
        super(context,DBNAME,null,DBVERSION);
        if (!ifDBExists(context)) {
            if (!copyDBFromAssets(context)) {
                throw new RuntimeException("Failed to Copy Database From Assets Folder");
            }
        }
        mDB = this.getWritableDatabase();
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        // Do NOTHING in here as the database has been copied from the assets
        // if it did not already exist
        Log.d(TAG, "METHOD onCreate called");
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        Log.d(TAG,"METHOD onUpgrade called");
    }

    public Cursor getDataDB_TableItemNamesByItemType(String itemType) {
        SQLiteDatabase db = this.getWritableDatabase();
        Cursor res = db.rawQuery("select * from " + TABLE_ITEM + " where "
                + ITEM_TYPE + " = '" + itemType+ "'", null);
        Log.e("LogTag", "res.getCount(): " + res.getCount());
        return res;
    }

    /*
        Copies the database from the assets folder to the apps database folder (with logging)
        note databases folder is typically data/data/the_package_name/database
             however using getDatabasePath method gets the actual path (should it not be as above)
        This method can be significantly reduced one happy that it works.
     */
    private boolean copyDBFromAssets(Context context) {
        Log.d("CPYDBINFO","Starting attemtpt to cop database from the assets file.");
        String DBPATH = context.getDatabasePath(DBNAME).getPath();
        InputStream is;
        OutputStream os;
        int buffer_size = 8192;
        int length = buffer_size;
        long bytes_read = 0;
        long bytes_written = 0;
        byte[] buffer = new byte[length];

        try {

            is = context.getAssets().open(DBNAME);
        } catch (IOException e) {
            Log.e("CPYDB FAIL - NO ASSET","Failed to open the Asset file " + DBNAME);
            e.printStackTrace();
            return false;
        }

        try {
            os = new FileOutputStream(DBPATH);
        } catch (IOException e) {
            Log.e("CPYDB FAIL - OPENDB","Failed to open the Database File at " + DBPATH);
            e.printStackTrace();
            return false;
        }
        Log.d("CPYDBINFO","Initiating copy from asset file" + DBNAME + " to " + DBPATH);
        while (length >= buffer_size) {
            try {
                length = is.read(buffer,0,buffer_size);
            } catch (IOException e) {
                Log.e("CPYDB FAIL - RD ASSET",
                        "Failed while reading in data from the Asset. " +
                                String.valueOf(bytes_read) +
                                " bytes read successfully."
                );
                e.printStackTrace();
                return false;
            }
            bytes_read = bytes_read + length;
            try {
                os.write(buffer,0,buffer_size);
            } catch (IOException e) {
                Log.e("CPYDB FAIL - WR ASSET","failed while writing Database File " +
                        DBPATH +
                        ". " +
                        String.valueOf(bytes_written) +
                        " bytes written successfully.");
                e.printStackTrace();
                return false;

            }
            bytes_written = bytes_written + length;
        }
        Log.d("CPYDBINFO",
                "Read " + String.valueOf(bytes_read) + " bytes. " +
                        "Wrote " + String.valueOf(bytes_written) + " bytes."
        );
        try {
            os.flush();
            is.close();
            os.close();
        } catch (IOException e ) {
            Log.e("CPYDB FAIL - FINALISING","Failed Finalising Database Copy. " +
                    String.valueOf(bytes_read) +
                    " bytes read." +
                    String.valueOf(bytes_written) +
                    " bytes written."
            );
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /*
    Checks to see if the database exists if not will create the respective directory (database)
    Creating the directory overcomes the NOT FOUND error
 */
    private boolean ifDBExists(Context context) {
        String dbparent = context.getDatabasePath(DBNAME).getParent();
        File f = context.getDatabasePath(DBNAME);
        if (!f.exists()) {
            Log.d("NODB MKDIRS","Database file not found, making directories."); //<<<< remove before the App goes live.
            File d = new File(dbparent);
            d.mkdirs();
            //return false;
        }
        return f.exists();
    }
}
  • 如您所见,您的原始 getDataDB_TableItemNamesByItemType 已原封不动地包含在内。
  • 根据评论(我建议阅读它们),上面的内容有点啰嗦,但这可以让您看到正在发生的事情。 显然在分发应用程序之前删除日志记录。

阶段 4 - 调用数据库助手并从数据库中提取数据

在本例中,应用程序的主要activity用于调用。

使用的activity是:-

public class MainActivity extends AppCompatActivity {

    DataBaseHelper myDBHlpr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Get an instance of the DatabaseHelper class (copy will be done IF DB does not exist)
        myDBHlpr = new DataBaseHelper(this); 

        // Get some data from the database using your method
        Cursor csr = myDBHlpr.getDataDB_TableItemNamesByItemType("type2");
        while(csr.moveToNext()){
            Log.d("DB_ROWINFO",
                    "ITEM NAME is " + csr.getString(csr.getColumnIndex(DataBaseHelper.ITEM_NAME))
                    + "ITEM TYPE is "
                    + csr.getString((csr.getColumnIndex(DataBaseHelper.ITEM_TYPE))
                    )
            );
        }
        // ========================================
        // ALWAYS CLOSE CURSORS WHEN DONE WITH THEM
        // ========================================
        csr.close();
    }
}

第 5 阶段 - 结果(日志)

应用程序如果存在则卸载 运行 并生成:-

03-06 09:32:52.759 5341-5341/a.a.so66390748 I/art: Rejecting re-init on previously-failed class java.lang.Class<androidx.core.view.ViewCompat>
03-06 09:32:52.787 5341-5341/a.a.so66390748 D/NODB MKDIRS: Database file not found, making directories.
03-06 09:32:52.787 5341-5341/a.a.so66390748 D/CPYDBINFO: Starting attemtpt to cop database from the assets file.
03-06 09:32:52.787 5341-5341/a.a.so66390748 D/CPYDBINFO: Initiating copy from asset fileTestDB.db to /data/user/0/a.a.so66390748/databases/TestDB.db
03-06 09:32:52.787 5341-5341/a.a.so66390748 D/CPYDBINFO: Read 8191 bytes. Wrote 8191 bytes.
03-06 09:32:52.805 5341-5341/a.a.so66390748 D/DBHELPER: METHOD onCreate called
03-06 09:32:52.811 5341-5341/a.a.so66390748 E/LogTag: res.getCount(): 2
03-06 09:32:52.811 5341-5341/a.a.so66390748 D/DB_ROWINFO: ITEM NAME is Item2ITEM TYPE is type2
03-06 09:32:52.811 5341-5341/a.a.so66390748 D/DB_ROWINFO: ITEM NAME is Item4ITEM TYPE is type2
03-06 09:32:52.822 5341-5355/a.a.so66390748 D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true

应用程序重新运行第二次(未卸载),日志为:-

03-06 09:35:37.876 5465-5465/a.a.so66390748 I/art: Rejecting re-init on previously-failed class java.lang.Class<androidx.core.view.ViewCompat>
03-06 09:35:37.908 5465-5465/a.a.so66390748 E/LogTag: res.getCount(): 2
03-06 09:35:37.908 5465-5465/a.a.so66390748 D/DB_ROWINFO: ITEM NAME is Item2ITEM TYPE is type2
03-06 09:35:37.908 5465-5465/a.a.so66390748 D/DB_ROWINFO: ITEM NAME is Item4ITEM TYPE is type2
03-06 09:35:37.956 5465-5498/a.a.so66390748 D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true

即数据库,因为它已经存在并且数据库因此存在,不会复制数据库但仍会按预期提取数据。

注意上面的内容是在我方便的时候写的,所以只使用了 Main activity 和数据库助手。您显然必须相应地调整代码

假定您遵循了评论中给出的建议并尝试了 SELECT * FROM the_table_name(即没有 WHERE 子句)。我这样说是因为您使用的查询区分大小写,如果传递给 getDataDB_TableItemNamesByItemType 方法的参数不完全匹配,那么您将不会提取任何内容。 (例如,传递 Type2 而不是 type2 显示计数为 0)

补充

关于评论:-

Basically the database is changed all the time dynamically by the user as new items are added and removed based on the user iteraction. Further, the database should be changed from time to time externally. As stated before, I can easily archieve the update by just renaming the database and I was wondering whether a more convenient way exists for that (normally it should exist). However, when having a look at your suggested code it looks way more complex than just changing the database name. So I guess there is no easy solution for that.

我认为您需要考虑两个单独的功能。

即 1) 维护 App 用户的数据和 2) 更改 App 提供的数据。

您可以通过使用单独但连接的数据库将它们分开,另一种方法是根据实体命名来区分这两种类型(作为表的实体会触发视图等)。

  • 例如您将 App 提供的实体称为 as_ 或将用户的实体称为 user_ 或两者。

当检测到新资产文件时,您可以将现有文件重命名为 old_TestDB.db,然后将新文件复制到 TestDB.db。然后,您可以删除用户实体中的所有行(如果您从不提供用户数据,则可能不需要),然后连接 old_TestDB.db 将数据从旧的复制到新的并将连接删除到 old_TestDB.db,然后最后删除 old_TestDB.db 文件(或者保留它以防万一出现问题,这样您就可以从中恢复一些东西)。

使用文件名检测新资产文件可能会出现问题,因为您最终可能会在资产文件夹中找到许多旧文件。因此,如果使用相同的文件名但使用了 sqlite user_version,则应用程序的大小可能会更简单、更有利。

sqliteuser_version是一个版本号,保存在数据库的header数据中。它是 4 个字节,在 header 中偏移 60 个字节。您可以访问它,而无需使用标准 IO 打开数据库的开销(例如复制资产文件时)。您可以将其与现有数据库进行比较,如果不同,则处理新的资产文件。

您可以使用 SQL PRAGMA user_version = n 更改用户版本(某些编辑器允许您通过 window 更改此版本)https://www.sqlite.org/pragma.html#pragma_user_version

另一种方法是不尝试检测已更改的资产,也不使用 SQLite user_version,而是始终假定已更改的资产,因此在引入新版本的应用程序时额外增加传递给 SQLiteOpenHelper 的数据库版本,然后执行资产文件的复制并在 onUpgrade 方法中应用用户数据。但是,这可能会充满 errors/issues,因为数据库已打开(要提取数据库的用户版本,user_version 将更改为应用程序的数据库版本)。

但是,在开始编写代码之前,您应该先相应地设计数据库。