如何从 App 中的 Assets 文件夹更新数据库

How to Update Database from Assets Folder in App

如何从资产文件夹 onUpgrade() 更新数据库

  @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    try {
        File f = new File(mContext.getApplicationInfo().dataDir + DB_PATH_SUFFIX + DATABASE_NAME);
        if (f.exists())
            f.delete();

        openDataBase();

    } catch (Exception e) {
        System.out.println("=== onUpgrade Exception : " + e.getMessage());
        e.printStackTrace();
    }
}

上面的代码是删除旧数据库并从 assets 文件夹中复制新数据库,但它不起作用,它会给出如下错误:

attempt to write a readonly database (code 1032 SQLITE_READONLY_DBMOVED)

因为在打开数据库时会调用onUpgrade (实际上数据库本身已经打开(因此将其传递给onUpgrade方法))你将一无所有尝试在 onUpgrade 方法中删除数据库的问题,因为它旨在通过 SQL(例如 ALTER、CREATE、DROP INSERT 等)升级数据库。

如果你有一个新的资产文件,那么你需要一些方法来检测新的资产文件并在数据库助手打开它之前替换数据库文件(与最初从资产文件复制数据库时非常相似) ).

这些答案可能会有帮助

  • How to read version number from a database file in Android which is placed in asset folder

  • Updating a prepopulated database

工作示例

这是一个使用 SQlite User_Version 的示例(即数据库助手(SQLiteOpenHelper 的子class)使用的版本号)。

它基于两个 classes AssetHanlder class 完成处理资产文件是否被复制以及执行副本是必要的。另一个 class 是一个典型的数据库助手,有一个细微的区别,它实例化一个 AssetHandler 允许它做它的业务。

  1. 确定文件名,重要的是根据传递的参数创建数据库目录(如果它不存在)。
  2. 读取资产文件的前 64 个字节(SQLite Header 的一部分)。它从偏移量 60-63 中提取 4 个字节,即 User_Version.
  3. 如果数据库文件存在,它从数据库文件中获取User_Version。
  4. 如果数据库不存在或者资产的 User_Version 大于当前数据库的 User_version 它将在重命名数据库文件后复制资产文件,如果存在,则使用renamed_
  5. 的前缀
  • 注意 这是仓促进行的有限测试,因此应视为可能需要改进。

这里是 Assethandler :-

public class AssetHandler {

    private static final String DB_RENAME_PREFIX = "renamed_";
    private int buffer_size = 4096;

    /* Asset File related variables */
    private int assetSQLiteVersion = -1;
    private String assetFileName;
    private byte[] assetSQLiteHeader = new byte[64];

    /* Database (current) File related variables */
    private int dbSQLiteVersion = -1;
    private byte[] dbSQLiteHeader = new byte[64];
    private final String databaseFileName;
    private File databaseFile;
    private boolean databaseExists = false;
    private String databasePath;

    private final Context context;
    private boolean showStackTrace = false;

    public AssetHandler(
            Context context, /* Context */
            String assetFileName, /* Name of the asset file (include subfolder if not in the assets folder) */
            String databaseFileName, /* the name of the database aka the name of the database file */
            /* NOTE typically assetFile name (less any sub folders) will be the same as the database file name*/
            boolean performCopy, /* true if you want the copy to be performed */
            boolean showStackTrace /* true if you want stack trace*/
            /* NOTE All IO exceptions are trapped */
    ) {

        this.context = context;
        this.showStackTrace = showStackTrace;
        this.assetFileName = assetFileName;
        this.databaseFileName = databaseFileName;
        this.databaseFile = context.getDatabasePath(databaseFileName);
        this.databasePath = databaseFile.getPath();
        this.databaseExists = databaseFile.exists();
        if (!databaseExists) {
            databaseFile.getParentFile().mkdirs();
        }

        /* Get the database version number */
        InputStream assetFileInputStream;
        try {
            assetFileInputStream = context.getAssets().open(assetFileName);
            assetFileInputStream.read(assetSQLiteHeader,0,64);
            this.assetSQLiteVersion = getVersionFromHeader(assetSQLiteHeader);
            assetFileInputStream.close();
            Log.d("ASSETHANDLER","Asset SQLite Version Number is " + String.valueOf(assetSQLiteVersion));
        } catch (IOException e) {
            if (showStackTrace) {
                e.printStackTrace();
            }
        }

        /* Get the database file version number */
        if (databaseExists) {
            InputStream dbFileInputStream;
            try {
                dbFileInputStream = new FileInputStream(databasePath);
                dbFileInputStream.read(dbSQLiteHeader, 0, 64);
                this.dbSQLiteVersion = getVersionFromHeader(dbSQLiteHeader);
                dbFileInputStream.close();
                Log.d("ASSETHANDLER","Current Database SQLite Version Number is " + String.valueOf(dbSQLiteVersion));
            } catch (IOException e) {
                if (showStackTrace) {
                    e.printStackTrace();
                }
            }
        } else {
            Log.d("ASSETHANDLER","Database does not exist");
        }

        /* If the asset version number is greater than the database version number
            or if the database does not exist copy the database from the asset
         */
        if (performCopy && (assetSQLiteVersion > dbSQLiteVersion || !databaseExists)) {
            Log.d("ASSETHANDLER","Initiating Copy of asset  " + assetFileName + " to  database " + databasePath);
            performCopy();
        } else {
            Log.d("ASSETHANDLER",
                    "Copy not being performed as asset version (" + String.valueOf(assetSQLiteVersion) + ") = " +
                            "database version(" + String.valueOf(dbSQLiteVersion) + ")");
        }
    }

    /* allow the retrieval of the version numbers and also the headers */
    public int getAssetSQLiteVersionNumber() {
        return this.assetSQLiteVersion;
    }
    public int getDbSQLiteVersion() {
        return this.dbSQLiteVersion;
    }

    public byte[] getAssetSQLiteHeader() {
        return this.assetSQLiteHeader;
    }
    public byte[] getDbSQLiteHeader() {
        return this.dbSQLiteHeader;
    }

    /* Get the version number from the header */
    private int getVersionFromHeader(byte[] header) {
        byte[] versionNumber = new byte[]{0,0,0,0};
        for(int i=60;i < 64;i++) {
            versionNumber[i-60] = header[i];
        }
        return ByteBuffer.wrap(versionNumber).getInt();
    }

    /* Rename the database file */
    private void renameDatabaseFile(String prefix) {
        if (databaseFile.exists()) {
            File newdbName = new File(databaseFile.getParent() + File.separator + prefix + databaseFile.getName());
            if (newdbName.exists()) {
                newdbName.delete();
            }
            databaseFile.renameTo(new File(databasePath + File.pathSeparator + prefix + databaseFile.getName()));
        }
    }

    /* Copy the asset database to the default location */
    private void performCopy() {
        renameDatabaseFile(DB_RENAME_PREFIX);
        byte[] buffer = new byte[buffer_size];
        int read_count = 0;
        int write_count = 0;
        try {
            InputStream is = context.getAssets().open(assetFileName);
            OutputStream os =  new FileOutputStream(databasePath);
            int length;
            while ((length=is.read(buffer))>0){
                read_count++;
                os.write(buffer, 0, length);
                write_count++;
            }
            os.flush();
            is.close();
            os.close();

        } catch (IOException e) {
            if (showStackTrace) {
                e.printStackTrace();
            }
        }
    }
}

这是用于测试的数据库助手 DBHelper,它利用 AssetHandler :-

class DBHelper extends SQLiteOpenHelper {

    private static final String DBNAME = "mydb";
    private static int DBVERSION;
    private static volatile DBHelper instance;
    private static AssetHandler assetHandler;

    private DBHelper(@Nullable Context context) {
        super(context, DBNAME, null, DBVERSION);
    }

    public static DBHelper getInstance(Context context) {
        /* instantiate the AsssetHandler if not instantiated */
        /* aka only do this first time database is accessed when the App is run */
        if (assetHandler == null) {
            assetHandler = new AssetHandler(context, DBNAME, DBNAME, true, true);
            DBVERSION = assetHandler.getAssetSQLiteVersionNumber(); /* Need to make the version number same as the asset version number */
        }
        /* return singleton DBHelper instance */
        if (instance == null) {
            instance = new DBHelper(context);
        }
        return instance;
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        super.onDowngrade(db, oldVersion, newVersion);
        Log.e("ONDOWNGRADE","The onDowngrade method was called. This should not be called. Are the versions correct?");
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        Log.e("ONUPGRADE","The onUpgrade method was called. This should not be called. Are the versions correct?");
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    }
}

为了测试以下内容,在 activity MainActivity

中使用
public class MainActivity extends AppCompatActivity {

    DBHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = DBHelper.getInstance(this);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Cursor csr = db.query("sqlite_master",null,null,null,null,null,null);
        DatabaseUtils.dumpCursor(csr);
    }
}

这个:-

  1. 实例化 DBHelper。
  2. 通过 DBHelper 获取一个 SQLiteDatabase 对象。
  3. 从数据库中提取架构(即从 sqlite_master table 中获取所有行)
  4. 转储(输出到日志)游标。即显示架构。

结果

在 assets 文件夹中有同一个数据库的两个副本,但 user_version 编号不同 :-

可以看出,两者都没有被命名为 mydb,这是预期的。

运行 1

这是 运行,既没有现有数据库也没有资产(正确名称)。捕获了两个异常,但失败并出现 java.lang.IllegalArgumentException: Version must be >= 1, was -1 异常(这相对干净,因为没有创建(空)数据库。)

两个被捕获的异常(如果 showStackTrace 为真则显示)都是 java.io.FileNotFoundException: mydb 因为没有资产文件(第一个是在尝试获取版本号时,另一个是尝试复制,因为数据库没有'不存在)。

日志中写入两行:-

D/ASSETHANDLER: Database does not exist
D/ASSETHANDLER: Initiating Copy of asset  mydb to  database /data/user/0/a.a.so67803713javaupdateassetdb/databases/mydb

运行 2

mydb_V1 重命名为 mydb :-

并且应用程序是 运行(未对应用程序进行任何更改)并且日志显示:-

2021-06-03 22:47:40.691 D/ASSETHANDLER: Asset SQLite Version Number is 1
2021-06-03 22:47:40.692 D/ASSETHANDLER: Database does not exist
2021-06-03 22:47:40.692 D/ASSETHANDLER: Initiating Copy of asset  mydb to  database /data/user/0/a.a.so67803713javaupdateassetdb/databases/mydb
2021-06-03 22:47:40.716 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@cb3bb46
2021-06-03 22:47:40.717 I/System.out: 0 {
2021-06-03 22:47:40.718 I/System.out:    type=table
2021-06-03 22:47:40.718 I/System.out:    name=android_metadata
2021-06-03 22:47:40.718 I/System.out:    tbl_name=android_metadata
2021-06-03 22:47:40.718 I/System.out:    rootpage=3
2021-06-03 22:47:40.718 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
2021-06-03 22:47:40.718 I/System.out: }
2021-06-03 22:47:40.718 I/System.out: 1 {
2021-06-03 22:47:40.718 I/System.out:    type=table
2021-06-03 22:47:40.718 I/System.out:    name=shops
2021-06-03 22:47:40.718 I/System.out:    tbl_name=shops
2021-06-03 22:47:40.718 I/System.out:    rootpage=4
2021-06-03 22:47:40.718 I/System.out:    sql=CREATE TABLE shops (_id INTEGER  PRIMARY KEY , shoporder INTEGER  DEFAULT 1000 , shopname TEXT , shopstreet TEXT , shopcity TEXT , shopstate TEXT , shopnotes TEXT )
2021-06-03 22:47:40.718 I/System.out: }
2021-06-03 22:47:40.718 I/System.out: 2 {
....

所以资产被复制并且数据库现在存在。

运行 3 什么都没有改变,应用程序重新运行。日志包括:-

2021-06-03 22:51:10.291 D/ASSETHANDLER: Asset SQLite Version Number is 1
2021-06-03 22:51:10.291 D/ASSETHANDLER: Current Database SQLite Version Number is 1
2021-06-03 22:51:10.291 D/ASSETHANDLER: Copy not being performed as asset version (1) = database version(1)
2021-06-03 22:51:10.295 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@5dfcb21
2021-06-03 22:51:10.296 I/System.out: 0 {
2021-06-03 22:51:10.296 I/System.out:    type=table
2021-06-03 22:51:10.296 I/System.out:    name=android_metadata
....

所以没有复制,因为资产和数据库版本相同。

运行 4

mydb 资产文件已重命名为 mydb_V1,mydb_V2 已重命名为 mydb(反映正在分发新的 APK)。请注意 user_version 是 2(通过 SQLite 工具使用 PRAGMA user_version = 2; 设置(我为 SQLite 使用 DB 浏览器)):-

应用程序再次重新运行。日志包括:-

2021-06-03 22:59:12.311 D/ASSETHANDLER: Asset SQLite Version Number is 2
2021-06-03 22:59:12.311 D/ASSETHANDLER: Current Database SQLite Version Number is 1
2021-06-03 22:59:12.311 D/ASSETHANDLER: Initiating Copy of asset  mydb to  database /data/user/0/a.a.so67803713javaupdateassetdb/databases/mydb
2021-06-03 22:59:12.325 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@cb3bb46
2021-06-03 22:59:12.326 I/System.out: 0 {
2021-06-03 22:59:12.326 I/System.out:    type=table
2021-06-03 22:59:12.326 I/System.out:    name=android_metadata

因此已应用更新的资产文件。

运行 5

应用程序重新运行,没有任何更改。日志包括:-

2021-06-03 23:02:19.485 D/ASSETHANDLER: Asset SQLite Version Number is 2
2021-06-03 23:02:19.485 D/ASSETHANDLER: Current Database SQLite Version Number is 2
2021-06-03 23:02:19.485 D/ASSETHANDLER: Copy not being performed as asset version (2) = database version(2)

所以一切都符合预期。