哪些方法可用于管理预先存在的数据库的不同版本?

Which methods can be used to manage differing versions of pre-existing databases?

注意这是一个根据 share your knowledge, Q&A-style

提出并回答你自己的问题

通常会为使用 SQLite 的 Android 应用程序提供预先存在的数据库。数据库常放在assets文件夹下,如果数据库不存在,App会复制数据库。

对于较新版本的应用程序,更改资产文件夹中的数据库是可以的,因为更改后的数据库将被复制。但是,从旧版本升级应用程序时,数据库将存在,因此不会复制,也无法轻松无缝删除以启用从资产文件夹中复制。

如果需要保留用户数据,可能会更加复杂。

例如,我最初部署的数据库只有一个 table,其中有 3 个列是用 :-

创建的
CREATE TABLE "user" (
  "_id" INTEGER NOT NULL,
  "user" TEXT,
  "password" TEXT,
  PRIMARY KEY ("_id")
)

包含如下数据:-

而该应用程序的下一个化身希望利用更改后的架构,在 user 列上添加一个额外的列和 UNIQUE 约束,例如 :-

CREATE TABLE "user" (
  "_id" INTEGER NOT NULL,
  "user" TEXT,
  "password" TEXT,
  "email" TEXT,
  PRIMARY KEY ("_id"),
  CONSTRAINT "user" UNIQUE ("user")
);

另外还添加了其他数据,例如现在是:-

用户可以添加自己的数据,需要保留。所以删除数据库文件,即使可能,也会删除用户的数据。

可以使用哪些方法来管理不同版本的预先存在的数据库?

简而言之,一种方法是将新资产文件复制到适当的位置,同时保留原始数据库文件的副本,然后您可以应用更新,以便有效地将用户数据保存到新复制的数据库中. BUT 仅当针对该特定更改升级应用程序并且数据库存在时。

如果数据库不存在,则应进行资产文件的标准副本。

这是一个如何做到这一点的例子。

此示例在很大程度上依赖于允许管理和查询资产文件夹中的文件和/或数据库文件本身的例程。

A class 即 DBAssetHandler.java 满足上述要求(以及提取 user_version AKA 数据库版本的能力使用 SQLiteOpenHelper)。

  • 注意 class 也已经过测试,因此适合 Android Pie,因此预写日志记录(WAL 是 Pie 中的默认值)很好as journal mode,之前的默认。

  • 另请注意,如果使用 WAL,则应确保数据库已完全检查点 see - Write-Ahead Logging

是:-

public class DBAssetHandler {

    static final String[] tempfiles = new String[]{"-journal","-wal","-shm"}; // temporary files to rename
    public static final String backup = "-backup"; //value to be appended to file name when renaming (psuedo delete)
    public static final  int OUCH = -666666666;

    /**
     * Check if the database already exists. NOTE will create the databases folder is it doesn't exist
     * @return true if it exists, false if it doesn't
     */
    public static boolean checkDataBase(Context context, String dbname) {

        File db = new File(context.getDatabasePath(dbname).getPath()); //Get the file name of the database
        Log.d("DBPATH","DB Path is " + db.getPath()); //TODO remove if publish App
        if (db.exists()) return true; // If it exists then return doing nothing

        // Get the parent (directory in which the database file would be)
        File dbdir = db.getParentFile();
        // If the directory does not exits then make the directory (and higher level directories)
        if (!dbdir.exists()) {
            db.getParentFile().mkdirs();
            dbdir.mkdirs();
        }
        return false;
    }


    /**
     * Copy database file from the assets folder
     * (long version caters for asset file name being different to the database name)
     * @param context           Context is needed to get the applicable package
     * @param dbname            name of the database file
     * @param assetfilename     name of the asset file
     * @param deleteExistingDB  true if an existing database file should be deleted
     *                              note will delete journal and wal files
     *                              note doen't actually delete the files rater it renames
     *                              the files by appended -backup to the file name
     *                              SEE/USE clearForceBackups below to delete the renamed files
     */
    public static void copyDataBase(Context context, String dbname, String assetfilename, boolean deleteExistingDB) {

        final String TAG = "COPYDATABASE";
        int stage = 0, buffer_size = 4096, blocks_copied = 0, bytes_copied = 0;
        File f = new File(context.getDatabasePath(dbname).toString());
        InputStream is;
        OutputStream os;

        /**
         * If forcing then effectively delete (rename) current database files
         */
        if (deleteExistingDB) {
            //String[] tempfiles = new String[]{"-journal","-wal","-shm"};
            //String backup = "-backup";
            f.renameTo(context.getDatabasePath(dbname + "-backup"));
            for (String s: tempfiles) {
                File tmpf = new File(context.getDatabasePath(dbname + s).toString());
                if (tmpf.exists()) {
                    tmpf.renameTo(context.getDatabasePath(dbname + s + backup));
                }
            }
        }


        //Open your local db as the input stream
        Log.d(TAG,"Initiated Copy of the database file " + assetfilename + " from the assets folder."); //TODO remove if publishing
        try {
            is = context.getAssets().open(assetfilename); // Open the Asset file
            stage++;
            Log.d(TAG, "Asset file " + assetfilename + " found so attmepting to copy to " + f.getPath()); //TODO remove if publishing

            os = new FileOutputStream(f);
            stage++;
            //transfer bytes from the inputfile to the outputfile
            byte[] buffer = new byte[buffer_size];
            int length;
            while ((length = is.read(buffer)) > 0) {
                blocks_copied++;
                Log.d(TAG, "Attempting copy of block " + String.valueOf(blocks_copied) + " which has " + String.valueOf(length) + " bytes."); //TODO remove if publishing
                os.write(buffer, 0, length);
                bytes_copied += length;
            }
            stage++;
            Log.d(TAG,
                    "Finished copying Database " + dbname +
                            " from the assets folder, to  " + f.getPath() +
                            String.valueOf(bytes_copied) + "were copied, in " +
                            String.valueOf(blocks_copied) + " blocks of size " +
                            String.valueOf(buffer_size) + "."
            ); //TODO remove if publishing
            //Close the streams
            os.flush();
            stage++;
            os.close();
            stage++;
            is.close();
            Log.d(TAG, "All Streams have been flushed and closed.");
        } catch (IOException e) {
            String exception_message = "";
            e.printStackTrace();
            switch (stage) {
                case 0:
                    exception_message = "Error trying to open the asset " + dbname;
                    break;
                case 1:
                    exception_message = "Error opening Database file for output, path is " + f.getPath();
                    break;
                case 2:
                    exception_message = "Error flushing written database file " + f.getPath();
                    break;
                case 3:
                    exception_message = "Error closing written database file " + f.getPath();
                    break;
                case 4:
                    exception_message = "Error closing asset file " + f.getPath();

            }
            throw new RuntimeException("Unable to copy the database from the asset folder." + exception_message + " see starck-trace above.");
        }
    }

    /**
     * Copy the databsse from the assets folder where asset name and dbname are the same
     * @param context
     * @param dbname
     * @param deleteExistingDB
     */
    public static void copyDataBase(Context context, String dbname, boolean deleteExistingDB) {
        copyDataBase(context, dbname,dbname,deleteExistingDB);
    }

    /**
     * Get the SQLite_user_vesrion from the DB in the asset folder
     *
     * @param context           needed to get the appropriate package assets
     * @param assetfilename     the name of the asset file (assumes/requires name matches database)
     * @return                  the version number as stored in the asset DB
     */
    public static int getVersionFromDBInAssetFolder(Context context, String assetfilename) {
        InputStream is;
        try {
            is = context.getAssets().open(assetfilename);
        } catch (IOException e) {
            return OUCH;
        }
        return getDBVersionFromInputStream(is);
    }

    /**
     * Get the version from the database itself without opening the database as an SQliteDatabase
     * @param context   Needed to ascertain package
     * @param dbname    the name of the dataabase
     * @return          the version number extracted
     */
    public static int getVersionFromDBFile(Context context, String dbname) {
        InputStream is;
        try {
            is = new FileInputStream(new File(context.getDatabasePath(dbname).toString()));
        } catch (IOException e) {
            return OUCH;
        }
        return getDBVersionFromInputStream(is);
    }

    /**
     * Get the Database Version (user_version) from an inputstream
     *  Note the inputstream is closed
     * @param is    The Inputstream
     * @return      The extracted version number
     */
    private static int getDBVersionFromInputStream(InputStream is) {
        int rv = -1, dbversion_offset = 60, dbversion_length = 4 ;
        byte[] dbfileheader = new byte[64];
        byte[] dbversion = new byte[4];
        try {
            is.read(dbfileheader);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
            return rv;
        }

        for (int i = 0; i < dbversion_length; i++ ) {
            dbversion[i] = dbfileheader[dbversion_offset + i];
        }
        return ByteBuffer.wrap(dbversion).getInt();
    }

    /**
     * Check to see if the asset file exists
     *
     * @param context           needed to get the appropriate package
     * @param assetfilename     the name of the asset file to check
     * @return                  true if the asset file exists, else false
     */
    public static boolean ifAssetFileExists(Context context, String assetfilename) {
        try {
            context.getAssets().open(assetfilename);
        } catch (IOException e) {
            return false;
        }
        return true;
    }


    /**
     * Delete the backup
     * @param context
     * @param dbname
     */
    public static void clearForceBackups(Context context, String dbname) {
        String[] fulllist = new String[tempfiles.length + 1];

        for (int i = 0;i < tempfiles.length; i++) {
            fulllist[i] = tempfiles[i];
        }
        fulllist[tempfiles.length] = ""; // Add "" so database file backup is also deleted
        for (String s: fulllist) {
            File tmpf = new File(context.getDatabasePath(dbname + s + backup).toString());
            if (tmpf.exists()) {
                tmpf.delete();
            }
        }
    }
}
  • 希望方法名和注释能解释上面的代码。

assets文件,有两个:-

  • pev1.db - 如上所述的原始预先存在的数据库
  • pev1mod.db - 修改后的(额外列、UNIQUE 约束和额外行)。

数据库助手(SQLOpenHelper的子class)即PEV2DBHelper.java,需要注意的是数据库版本( DBVERSION) 用于控制并且与 APK 的版本不同(它可能比 DB 更频繁地更改)

  • 在尝试使用 onUpgrade 方法时发现问题,因此另一种方法是获取数据库的 user_version 从文件而不是通过 SQLiteDatabase。

这里是PEV2DBHelper.java:-

/**
 * MORE COMPLEX EXAMPLE RETAINING USER DATA
 */
public class PEV2DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "pev1.db";
    public static final String ASSETTOCOPY_DBV2 = "pev1mod.db"; //<<<<<<<<<< changed DB
    public static final int DBVERSION = 2; //<<<<<<<<<< increase and db file from assets will copied keeping existing data
    Context mContext;

    public PEV2DBHelper(Context context) {
        super(context, DBNAME, null, DBVERSION);


        int dbversion = DBAssetHandler.getVersionFromDBFile(context,DBNAME);
        Log.d("DBFILEVERSION","Database File Version = " + String.valueOf(dbversion));
        int af1version = DBAssetHandler.getVersionFromDBInAssetFolder(context,DBNAME);
        Log.d("DBFILEVERSION","Asset Database File Version = " + String.valueOf(af1version));
        int af2version = DBAssetHandler.getVersionFromDBInAssetFolder(context,ASSETTOCOPY_DBV2);
        Log.d("DBFILEVERSION","Asset Database File Version = " + String.valueOf(af2version));

        // cater for different DBVERSIONS (for testing )
        if (!DBAssetHandler.checkDataBase(context,DBNAME)) {

            //If new installation of the APP then copy the appropriate asset file for the DB
            switch (DBVERSION) {
                case 1:
                    DBAssetHandler.copyDataBase(context,DBNAME,DBNAME,false);
                    break;
                case 2:
                    DBAssetHandler.copyDataBase(context,DBNAME,ASSETTOCOPY_DBV2,false);
                    break;
            }
        }

        // If DBVERSION upgraded to 2 with modified DB but wanting to preserve used data
        if (DBAssetHandler.checkDataBase(context,DBNAME) & (DBVERSION > DBAssetHandler.getVersionFromDBFile(context, DBNAME)) & (DBVERSION == 2) ) {

            String[] oldcolumns = new String[]{"user","password"};

            // Copy in the new DB noting that delete option renames old (truue flag important)
            DBAssetHandler.copyDataBase(context,DBNAME,ASSETTOCOPY_DBV2,true);

            //Get the newly copied database
            SQLiteDatabase newdb = SQLiteDatabase.openDatabase(context.getDatabasePath(DBNAME).toString(),null,SQLiteDatabase.OPEN_READWRITE);
            //Get the old database (backup copy)
            SQLiteDatabase olddb =  SQLiteDatabase.openDatabase(context.getDatabasePath(DBNAME + DBAssetHandler.backup).toString(),null,SQLiteDatabase.OPEN_READWRITE);

            //Prepare to insert old rows (note user column is UNIQUE so pretty simple scenario just try inserting all and duplicates will be rejected)
            ContentValues cv = new ContentValues();
            Cursor oldcsr = olddb.query("user",null,null,null,null,null,null);
            newdb.beginTransaction();
            while (oldcsr.moveToNext()) {
                cv.clear();
                for (String columnname: oldcolumns) {
                    cv.put(columnname,oldcsr.getString(oldcsr.getColumnIndex(columnname)));
                }
                newdb.insert("user",null,cv);
            }
            newdb.setTransactionSuccessful();
            newdb.endTransaction();
            newdb.close();
            olddb.close();

            // Finally delete the renamed old database
            DBAssetHandler.clearForceBackups(context,DBNAME);
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }

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

    }
}
  • 请注意,添加、删除提取行的方法几乎没有膨胀。但是,它有点过于复杂,因为它处理版本之间的切换以方便演示。

最后是一个示例 activity,它调用了 PEV2DBHelper,将 table 中的架构和行写入日志。

使用的 activity 是 MainActivity.java 并且是 :-

public class MainActivity extends AppCompatActivity {

    PEV2DBHelper mDBHlpr2; //DBHelper for example that retains user data

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        doPEV2(); // Test simple more complex scenario
    }


    private void doPEV2() {

        mDBHlpr2 = new PEV2DBHelper(this);
        SQLiteDatabase db = mDBHlpr2.getWritableDatabase();
        Cursor csr = db.query("sqlite_master",null,null,null,null,null,null);
        DatabaseUtils.dumpCursor(csr);
        csr = db.query("user",null,null,null,null,null,null);
        DatabaseUtils.dumpCursor(csr);
        if (PEV2DBHelper.DBVERSION == 1) {
            addUserData(db);
            csr = db.query("user",null,null,null,null,null,null);
            DatabaseUtils.dumpCursor(csr);
        }
        csr.close();
        db.close();
    }

    /**
     * Add some user data for testing presevation of that data
     * @param db    the SQLitedatabase
     */
    private void addUserData(SQLiteDatabase db) {
        ContentValues cv = new ContentValues();
        cv.put("user","mr new user");
        cv.put("password","a password");
        db.insert("user",null,cv);
    }
}

结果

1。当第一个 运行 DBVERSION 为 1 时(全新 App)

在这种情况下,资产文件 pev1.db 是从资产文件夹复制的,输出为:-

2019-02-22 19:07:54.676 28670-28670/? D/DBFILEVERSION: Database File Version = -666666666
2019-02-22 19:07:54.677 28670-28670/? D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:07:54.677 28670-28670/? D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:07:54.677 28670-28670/? D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:07:54.677 28670-28670/? D/COPYDATABASE: Initiated Copy of the database file pev1.db from the assets folder.
2019-02-22 19:07:54.677 28670-28670/? D/COPYDATABASE: Asset file pev1.db found so attmepting to copy to /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:07:54.677 28670-28670/? D/COPYDATABASE: Attempting copy of block 1 which has 4096 bytes.
2019-02-22 19:07:54.677 28670-28670/? D/COPYDATABASE: Attempting copy of block 2 which has 4096 bytes.
2019-02-22 19:07:54.677 28670-28670/? D/COPYDATABASE: Finished copying Database pev1.db from the assets folder, to  /data/user/0/mjt.so54807516/databases/pev1.db8192were copied, in 2 blocks of size 4096.
2019-02-22 19:07:54.678 28670-28670/? D/COPYDATABASE: All Streams have been flushed and closed.
2019-02-22 19:07:54.678 28670-28670/? D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:07:54.701 28670-28670/? I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@71528f1
2019-02-22 19:07:54.701 28670-28670/? I/System.out: 0 {
2019-02-22 19:07:54.701 28670-28670/? I/System.out:    type=table
2019-02-22 19:07:54.701 28670-28670/? I/System.out:    name=user
2019-02-22 19:07:54.701 28670-28670/? I/System.out:    tbl_name=user
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    rootpage=2
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    sql=CREATE TABLE "user" (
2019-02-22 19:07:54.702 28670-28670/? I/System.out:   "_id" INTEGER NOT NULL,
2019-02-22 19:07:54.702 28670-28670/? I/System.out:   "user" TEXT,
2019-02-22 19:07:54.702 28670-28670/? I/System.out:   "password" TEXT,
2019-02-22 19:07:54.702 28670-28670/? I/System.out:   PRIMARY KEY ("_id")
2019-02-22 19:07:54.702 28670-28670/? I/System.out: )
2019-02-22 19:07:54.702 28670-28670/? I/System.out: }
2019-02-22 19:07:54.702 28670-28670/? I/System.out: 1 {
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    type=table
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    name=android_metadata
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    tbl_name=android_metadata
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    rootpage=3
2019-02-22 19:07:54.702 28670-28670/? I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
2019-02-22 19:07:54.702 28670-28670/? I/System.out: }
2019-02-22 19:07:54.702 28670-28670/? I/System.out: <<<<<
2019-02-22 19:07:54.703 28670-28670/? I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@1e20cd6
2019-02-22 19:07:54.703 28670-28670/? I/System.out: 0 {
2019-02-22 19:07:54.703 28670-28670/? I/System.out:    _id=1
2019-02-22 19:07:54.703 28670-28670/? I/System.out:    user=Fred
2019-02-22 19:07:54.703 28670-28670/? I/System.out:    password=fredpassword
2019-02-22 19:07:54.703 28670-28670/? I/System.out: }
2019-02-22 19:07:54.703 28670-28670/? I/System.out: 1 {
2019-02-22 19:07:54.703 28670-28670/? I/System.out:    _id=2
2019-02-22 19:07:54.703 28670-28670/? I/System.out:    user=Mary
2019-02-22 19:07:54.704 28670-28670/? I/System.out:    password=marypassword
2019-02-22 19:07:54.704 28670-28670/? I/System.out: }
2019-02-22 19:07:54.704 28670-28670/? I/System.out: <<<<<
2019-02-22 19:07:54.705 28670-28670/? I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@acdb57
2019-02-22 19:07:54.705 28670-28670/? I/System.out: 0 {
2019-02-22 19:07:54.705 28670-28670/? I/System.out:    _id=1
2019-02-22 19:07:54.705 28670-28670/? I/System.out:    user=Fred
2019-02-22 19:07:54.705 28670-28670/? I/System.out:    password=fredpassword
2019-02-22 19:07:54.706 28670-28670/? I/System.out: }
2019-02-22 19:07:54.706 28670-28670/? I/System.out: 1 {
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    _id=2
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    user=Mary
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    password=marypassword
2019-02-22 19:07:54.706 28670-28670/? I/System.out: }
2019-02-22 19:07:54.706 28670-28670/? I/System.out: 2 {
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    _id=3
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    user=mr new user
2019-02-22 19:07:54.706 28670-28670/? I/System.out:    password=a password
2019-02-22 19:07:54.706 28670-28670/? I/System.out: }
2019-02-22 19:07:54.706 28670-28670/? I/System.out: <<<<<
  • -666666666 是版本,因为不存在文件因此尝试从文件中获取版本返回默认值以指示无法获取版本。

2。第二个 运行 除了版本号是 1.

其他都一样
2019-02-22 19:09:43.724 28730-28730/mjt.so54807516 D/DBFILEVERSION: Database File Version = 1
2019-02-22 19:09:43.724 28730-28730/mjt.so54807516 D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:09:43.724 28730-28730/mjt.so54807516 D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:09:43.725 28730-28730/mjt.so54807516 D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:09:43.725 28730-28730/mjt.so54807516 D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:09:43.729 28730-28730/mjt.so54807516 I/System.out: >>>>> 
..... etc

3。 Next 运行 将 DBVERSION 更改为 2

2019-02-22 19:13:49.157 28866-28866/mjt.so54807516 D/DBFILEVERSION: Database File Version = 1
2019-02-22 19:13:49.158 28866-28866/mjt.so54807516 D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:13:49.158 28866-28866/mjt.so54807516 D/DBFILEVERSION: Asset Database File Version = 0
2019-02-22 19:13:49.158 28866-28866/mjt.so54807516 D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:13:49.158 28866-28866/mjt.so54807516 D/DBPATH: DB Path is /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:13:49.158 28866-28866/mjt.so54807516 D/COPYDATABASE: Initiated Copy of the database file pev1mod.db from the assets folder.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Asset file pev1mod.db found so attmepting to copy to /data/user/0/mjt.so54807516/databases/pev1.db
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Attempting copy of block 1 which has 4096 bytes.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Attempting copy of block 2 which has 4096 bytes.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Attempting copy of block 3 which has 4096 bytes.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Attempting copy of block 4 which has 4096 bytes.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: Finished copying Database pev1.db from the assets folder, to  /data/user/0/mjt.so54807516/databases/pev1.db16384were copied, in 4 blocks of size 4096.
2019-02-22 19:13:49.159 28866-28866/mjt.so54807516 D/COPYDATABASE: All Streams have been flushed and closed.
2019-02-22 19:13:49.186 28866-28866/mjt.so54807516 E/SQLiteDatabase: Error inserting password=fredpassword user=Fred
    android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: user.user (code 2067 SQLITE_CONSTRAINT_UNIQUE)
        at 
    .........
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2019-02-22 19:13:49.191 28866-28866/mjt.so54807516 E/SQLiteDatabase: Error inserting password=a password user=mr new user
    android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: user.user (code 2067 SQLITE_CONSTRAINT_UNIQUE)
        at 
   .............
2019-02-22 19:13:49.209 28866-28866/mjt.so54807516 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@34252b0
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out: 0 {
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:    type=table
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:    name=user
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:    tbl_name=user
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:    rootpage=2
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:    sql=CREATE TABLE "user" (
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   "_id" INTEGER NOT NULL,
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   "user" TEXT,
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   "password" TEXT,
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   "email" TEXT,
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   PRIMARY KEY ("_id"),
2019-02-22 19:13:49.210 28866-28866/mjt.so54807516 I/System.out:   CONSTRAINT "user" UNIQUE ("user")
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out: )
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out: }
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out: 1 {
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    type=index
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    name=sqlite_autoindex_user_1
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    tbl_name=user
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    rootpage=4
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    sql=null
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out: }
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out: 2 {
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    type=table
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    name=android_metadata
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    tbl_name=android_metadata
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    rootpage=3
2019-02-22 19:13:49.211 28866-28866/mjt.so54807516 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
2019-02-22 19:13:49.212 28866-28866/mjt.so54807516 I/System.out: }
2019-02-22 19:13:49.212 28866-28866/mjt.so54807516 I/System.out: <<<<<
2019-02-22 19:13:49.212 28866-28866/mjt.so54807516 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@c8f0529
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out: 0 {
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    _id=1
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    user=Fred
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    password=fredpassword
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    email=fred@email.com
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out: }
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out: 1 {
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    _id=2
2019-02-22 19:13:49.213 28866-28866/mjt.so54807516 I/System.out:    
...... etc

注意这个答案是对主要答案的补充

如果只是引入新的 data/schema 而无需保留用户数据,则可以使用更简单的数据库助手版本,例如 :-

PVE1DBHelper.java

/**
 * SIMPLE CASE EXAMPLE WHENEVER APP's DBVERSION is changed re-copy asset DB to db file
 */
public class PEV1DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "pev1.db";
    public static final int DBVERSION = 1; //<<<<<<<<<< increase and db file from assets will be re-copied
    Context mContext;

    public PEV1DBHelper(Context context) {

        super(context, DBNAME, null, DBVERSION);
        mContext = context;

        int dbversion = DBAssetHandler.getVersionFromDBFile(mContext,DBNAME);
        Log.d("DBFILEVERSION","Database File Version = " + String.valueOf(dbversion));

        // Alternative to onUpgrade
        // bypass issues with potential DB re-open already closed due to onUpgrade being passed SQLiteDatabase
        // i.e. done before any attempt to get open the database
        if (DBVERSION > dbversion & DBAssetHandler.checkDataBase(mContext,DBNAME)) {
            Log.d("UPGRADING","Re-copying database file from the assets file due to App DBVERSION change.");
            DBAssetHandler.copyDataBase(mContext,DBNAME,true);
            DBAssetHandler.clearForceBackups(mContext,DBNAME);
        }

        // Original copy from the assets folder
        if (!DBAssetHandler.checkDataBase(mContext,DBNAME)) {
            Log.d("INITIALDBCOPY","Copying database file from the assets file due to DB not existing.");
            DBAssetHandler.copyDataBase(mContext,DBNAME,true); // no need for true as existing should exist
            DBAssetHandler.clearForceBackups(mContext,DBNAME); // also no need for clearing backups as none
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}
  • 请注意 来自资产文件夹的原始副本 部分中的注释,即仅可以使用单行 DBAssetHandler.copyDataBase(mContext,DBNAME,false);