首页 > 解决方案 > 如何仅在插入一些新记录后删除 Android SQLite 数据库上的先前记录

问题描述

我正在构建一个 Android 应用程序,它通过 HTML 页面的内容进行抓取(学校的网站没有给我们官方应用程序,也没有公共 API)。

我无法确定数据是否绝对是新的,所以目前所有获取数据并将其插入数据库中都是在 AsyncTask 中完成的,该任务基本上是这样的:

然后在 AsyncTask 结果中:

问题是:如果用户进入应用程序中的某个区域,该区域在 recreateDatabase() 和 insertSubjectList() 之间从数据库中读取数据,则数据库中可能只有很少或根本没有数据,因为它当前正在重新填充。

它不会使应用程序崩溃左右,但这是一种不良行为。我的问题是:是否可以告诉 SQLite 仅在新数据可用于查询后清理旧数据?旧的和新的不能同时读取,因为它们的信息可能会崩溃。

希望有人能帮助我度过这个难关!

标签: androidandroid-sqlite

解决方案


听起来不是:-

  • recreateDatabase() // 删除所有表并重新创建它们。
  • insertSubjectList() //遍历列表中的所有项目并将它们添加到数据库中

你要

  • insertSubjectList() 到过渡表中(与对应的名称不同的表,例如新创建的表)
  • 将原始表重命名为另一个表名,例如使用ALTER TABLE original TO original_renamed
  • 将过渡表重命名为对应的(原始)表名。
  • 删除旧表。

因此,在加载数据时,原始表仍然存在,然后仅在相对较短的时间内没有数据可用。

这也可以在事务中完成,这可能会提高性能。

因此,这将回答

是否可以告诉 SQLite 仅在新数据可用于查询后清理旧数据?

至于

旧的和新的不能同时读取,因为它们的信息可能会崩溃。

他们不会被同时阅读。

例如考虑以下演示:-

DROP TABLE IF EXISTS master;
CREATE TABLE IF NOT EXISTS master (mydata TEXT);

-- Load the original table with some data 
WITH RECURSIVE cte1(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM cte1 where x < 1000)
INSERT INTO master SELECT * FROM cte1;
SELECT * FROM master LIMIT 10;

BEGIN TRANSACTION;
-- Load the new data (original table still available)
CREATE TABLE IF NOT EXISTS transitional (mydata TEXT);
WITH RECURSIVE cte1(x) AS (SELECT 2000 UNION ALL SELECT x+1 FROM cte1 where x < 3000)
INSERT INTO transitional SELECT * FROM cte1;
SELECT * FROM transitional LIMIT 10;

-- Switch the tables - original not available
ALTER TABLE master RENAME TO master_old;
ALTER TABLE transitional RENAME TO master;
-- original now available

-- Clean-up old
DROP TABLE master_old;
END TRANSACTION;
SELECT * FROM master LIMIT 10;

安卓演示

以下是与上述类似的工作 Android 示例的代码。

调用活动和数据库助手是两段代码

MainActivity.java

public class MainActivity extends AppCompatActivity {

    DBHelper mDBHlpr;


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

        // Instantiate the helper noting that data will be loaded
        mDBHlpr = new DBHelper(this); 
        // Get the current data
        Cursor csr = mDBHlpr.getFirst10Rows();
        // Write the current data to the log
        DatabaseUtils.dumpCursor(csr);

        //<<<<<<<<<< do the switch >>>>>>>>>>
        mDBHlpr.switchData(DBHelper.TABLE_MASTER);

        //Again get the data and write to the log
        csr = mDBHlpr.getFirst10Rows();
        DatabaseUtils.dumpCursor(csr);

        //Clean up
        csr.close();
    }
}

DBHelper.java

public class DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "mydb";
    private static final int DBVERSION = 1;

    public static final String TABLE_MASTER = "master";
    public static final String COl_MASTER_ID = BaseColumns._ID;
    public static final String COL_MASTER_MYDATA = "mydata";

    SQLiteDatabase mDB;

    //Construct the helper NOTE will create the db if needed
    public DBHelper(Context context) {
        super(context, DBNAME,null, DBVERSION);
        mDB = this.getWritableDatabase(); // force db open
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(create_table_sql(TABLE_MASTER));
        mDB = db;
        addLotsOfData(TABLE_MASTER,1000,1); // Load some data
    }

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

    }

    // Generate the table SQL according to the table name passed
    private String create_table_sql(String table) {
        return "CREATE TABLE IF NOT EXISTS " + table + "(" +
                COl_MASTER_ID + " INTEGER PRIMARY KEY," +
                COL_MASTER_MYDATA + " TEXT" +
                ")";
    }

    // Switch the original table for a newly created version
    public void switchData(String table) {

        String transitional_table = "transitional";
        String renamed_master = table + "_old";
        boolean already_in_transaction = mDB.inTransaction();

        if (!already_in_transaction) {
            mDB.beginTransaction();
        }

        //<<<<<<<<<< create and load of new data could be done elsewhere
        mDB.execSQL(create_table_sql(transitional_table));
        addLotsOfData(transitional_table,1000,3001); //<<<<<<<< load new data here
        //>>>>>>>>>>

        mDB.execSQL("ALTER TABLE " + TABLE_MASTER + " RENAME TO " + renamed_master);
        mDB.execSQL("ALTER TABLE " + transitional_table + " RENAME TO " + TABLE_MASTER);
        mDB.execSQL("DROP TABLE IF EXISTS " + renamed_master);

        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }

    // Add some data
    private void addLotsOfData(String table, int rows, int value_offset) {
        boolean already_in_transaction = mDB.inTransaction();
        if (!already_in_transaction) {
            mDB.beginTransaction();
        }
        for (int i = 0; i < rows; i++) {
            addRow(table,String.valueOf(value_offset + i));
        }
        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }

    // Insert a single row
    public long addRow(String table, String value) {
        ContentValues cv = new ContentValues();
        cv.put(COL_MASTER_MYDATA,value);
        return mDB.insert(table,null,cv);
    }

    public Cursor getFirst10Rows() {
        return mDB.query(TABLE_MASTER,null,null,null,null,null,null,"10");
    }
}
  • 请注意,上面的设计只运行一次,对于后续运行,之前和之后的数据是相同的。

结果

日志显示(如预期): -

04-26 08:42:43.556I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@11697ce
04-26 08:42:43.557I/System.out: 0 {
04-26 08:42:43.557I/System.out:    _id=1
04-26 08:42:43.557I/System.out:    mydata=1
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 1 {
04-26 08:42:43.557I/System.out:    _id=2
04-26 08:42:43.557I/System.out:    mydata=2
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 2 {
04-26 08:42:43.557I/System.out:    _id=3
04-26 08:42:43.557I/System.out:    mydata=3
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 3 {
04-26 08:42:43.557I/System.out:    _id=4
04-26 08:42:43.557I/System.out:    mydata=4
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 4 {
04-26 08:42:43.557I/System.out:    _id=5
04-26 08:42:43.557I/System.out:    mydata=5
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 5 {
04-26 08:42:43.557I/System.out:    _id=6
04-26 08:42:43.557I/System.out:    mydata=6
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 6 {
04-26 08:42:43.557I/System.out:    _id=7
04-26 08:42:43.557I/System.out:    mydata=7
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 7 {
04-26 08:42:43.557I/System.out:    _id=8
04-26 08:42:43.557I/System.out:    mydata=8
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 8 {
04-26 08:42:43.557I/System.out:    _id=9
04-26 08:42:43.557I/System.out:    mydata=9
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: 9 {
04-26 08:42:43.557I/System.out:    _id=10
04-26 08:42:43.557I/System.out:    mydata=10
04-26 08:42:43.557I/System.out: }
04-26 08:42:43.557I/System.out: <<<<<
04-26 08:42:43.652I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@3396ef
04-26 08:42:43.652I/System.out: 0 {
04-26 08:42:43.652I/System.out:    _id=1
04-26 08:42:43.652I/System.out:    mydata=3001
04-26 08:42:43.652I/System.out: }
04-26 08:42:43.652I/System.out: 1 {
04-26 08:42:43.652I/System.out:    _id=2
04-26 08:42:43.652I/System.out:    mydata=3002
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 2 {
04-26 08:42:43.653I/System.out:    _id=3
04-26 08:42:43.653I/System.out:    mydata=3003
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 3 {
04-26 08:42:43.653I/System.out:    _id=4
04-26 08:42:43.653I/System.out:    mydata=3004
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 4 {
04-26 08:42:43.653I/System.out:    _id=5
04-26 08:42:43.653I/System.out:    mydata=3005
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 5 {
04-26 08:42:43.653I/System.out:    _id=6
04-26 08:42:43.653I/System.out:    mydata=3006
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 6 {
04-26 08:42:43.653I/System.out:    _id=7
04-26 08:42:43.653I/System.out:    mydata=3007
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 7 {
04-26 08:42:43.653I/System.out:    _id=8
04-26 08:42:43.653I/System.out:    mydata=3008
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.653I/System.out: 8 {
04-26 08:42:43.653I/System.out:    _id=9
04-26 08:42:43.653I/System.out:    mydata=3009
04-26 08:42:43.653I/System.out: }
04-26 08:42:43.654I/System.out: 9 {
04-26 08:42:43.654I/System.out:    _id=10
04-26 08:42:43.654I/System.out:    mydata=3010
04-26 08:42:43.654I/System.out: }
04-26 08:42:43.654I/System.out: <<<<<

额外的

这是一种更通用的方法,可以处理多个表,禁止将新数据明显加载到转换表中的应用程序/表特定的加载。

添加了日志记录,这可以实现计时。

  • 例如运行这个并加载 10,000 行(使用mDBHlpr.addLotsOfData(DBHelper.TABLE_MASTER + DBHelper.TRANSITION_SUFFIX,100000,10000);。可以看出:-

  • 尽管表需要 0.659 秒来加载数据(10,000 行),但表实际上仅在 0.007 秒内不可用(如果 DROP 在切换后完成,则为 0.001 秒(删除表在时间方面可能很昂贵,同时重命名一张桌子需要很少的时间))。

修改后的活动是:-

public class MainActivity extends AppCompatActivity {

    DBHelper mDBHlpr;


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

        // Instantiate the helper noting that data will be loaded
        mDBHlpr = new DBHelper(this);
        // Get the current data
        Cursor csr = mDBHlpr.getFirst10Rows();
        // Write the current data to the log
        DatabaseUtils.dumpCursor(csr);

        //<<<<<<<<<< do the switch >>>>>>>>>>
        mDBHlpr.switchData(DBHelper.TABLE_MASTER);

        //Again get the data and write to the log
        csr = mDBHlpr.getFirst10Rows();
        DatabaseUtils.dumpCursor(csr);

        //<<<<<<<<<<<<<<< AutoSwitch example >>>>>>>>>>
        //Create the transition tables
        mDBHlpr.prepareSwitchAllAppTables();

        Log.d("NEWDATALOADSTART","Loading of new data has started");
        // Prepare the new data by loading the data into the transition table
        // (only the 1 table for demo)
        // but perhaps 1 load per table according to requirements
        mDBHlpr.addLotsOfData(DBHelper.TABLE_MASTER + DBHelper.TRANSITION_SUFFIX,100000,10000);

        Log.d("TABLESNOTAVAILABLE","Tables will now be unavailable whil switching");

        // Switch all of the tables
        mDBHlpr.doSwitchAllAppTables();
        Log.d("TABLESAVAILABLE","Switch completed, Tables are now available with new data");

        //Again get the data and write to the log
        csr = mDBHlpr.getFirst10Rows();
        DatabaseUtils.dumpCursor(csr);

        //Clean up
        csr.close();
    }
}
  • 即 from line//<<<<<<<<<<<<<<< AutoSwitch example >>>>>>>>>>已添加,但csr.close()仍是最后一行。
  • 看评论
  • 切换分为两个阶段准备(创建转换表)和实际切换(重命名原始表,然后将转换表重命名为相应的原始表)。尽管该切换包括删除重命名的表,但这可以移至第三阶段,进一步减少表不可用的时间。

和修改后的DBHelpr.java

public class DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "mydb";
    private static final int DBVERSION = 1;

    public static final String TABLE_MASTER = "master";
    public static final String COl_MASTER_ID = BaseColumns._ID;
    public static final String COL_MASTER_MYDATA = "mydata";

    public static final String TRANSITION_SUFFIX = "_trn";
    public static final String RENAME_SUFFIX = "rename";

    private static final String SQLITE_MASTER = "sqlite_master";
    private static final String SQLITE_MASTER_TYPECOLUMN = "type";
    private static final String SQLITE_MASTER_NAMECOLUMN = "name";
    private static final String SQLITE_MASTER_SQLCOLUMN = "sql";
    private static final String[] SQLITE_MATSER_COLUMNS = new String[]{SQLITE_MASTER_NAMECOLUMN,SQLITE_MASTER_SQLCOLUMN};
    private static final String APPTABLES_WHERECLAUSE =
            "(" +
                    SQLITE_MASTER_NAMECOLUMN + " NOT LIKE 'sqlite%' " +
                    " AND " +  SQLITE_MASTER_NAMECOLUMN + " NOT LIKE 'android%' " +
                    " AND " + SQLITE_MASTER_NAMECOLUMN + " NOT LIKE '%" + TRANSITION_SUFFIX + "'" +
                    ")" +
                    " AND " + SQLITE_MASTER_TYPECOLUMN + "= '"  + "table" + "'";

    SQLiteDatabase mDB;

    //Construct the helper NOTE will create the db if needed
    public DBHelper(Context context) {
        super(context, DBNAME,null, DBVERSION);
        mDB = this.getWritableDatabase(); // force db open
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(create_table_sql(TABLE_MASTER));
        mDB = db;
        addLotsOfData(TABLE_MASTER,1000,1); // Load some data
    }

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

    }

    // Generate the table SQL according to the table name passed
    private String create_table_sql(String table) {
        return "CREATE TABLE IF NOT EXISTS " + table + "(" +
                COl_MASTER_ID + " INTEGER PRIMARY KEY," +
                COL_MASTER_MYDATA + " TEXT" +
                ")";
    }

    // Switch the original table for a newly created version
    public void switchData(String table) {

        String transitional_table = "transitional";
        String renamed_master = table + "_old";
        boolean already_in_transaction = mDB.inTransaction();

        if (!already_in_transaction) {
            mDB.beginTransaction();
        }

        //<<<<<<<<<< create and load of new data could be done elsewhere
        mDB.execSQL(create_table_sql(transitional_table));
        addLotsOfData(transitional_table,1000,3001); //<<<<<<<< load new data here
        //>>>>>>>>>>

        mDB.execSQL("ALTER TABLE " + TABLE_MASTER + " RENAME TO " + renamed_master);
        mDB.execSQL("ALTER TABLE " + transitional_table + " RENAME TO " + TABLE_MASTER);
        mDB.execSQL("DROP TABLE IF EXISTS " + renamed_master);

        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }

    // Add some data
    public void addLotsOfData(String table, int rows, int value_offset) {
        boolean already_in_transaction = mDB.inTransaction();
        if (!already_in_transaction) {
            mDB.beginTransaction();
        }
        for (int i = 0; i < rows; i++) {
            addRow(table,String.valueOf(value_offset + i));
        }
        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }

    // Insert a single row
    public long addRow(String table, String value) {
        ContentValues cv = new ContentValues();
        cv.put(COL_MASTER_MYDATA,value);
        return mDB.insert(table,null,cv);
    }

    public Cursor getFirst10Rows() {
        return mDB.query(TABLE_MASTER,null,null,null,null,null,null,"10");
    }

    /**
     * Create transition copy of all App tables (not sqlite..... tables or android.... tables)
     */
    public void prepareSwitchAllAppTables() {
        boolean already_in_transaction = mDB.inTransaction();
        if (!already_in_transaction) {
            mDB.beginTransaction();
        }

        Cursor csr = mDB.query(SQLITE_MASTER,SQLITE_MATSER_COLUMNS,APPTABLES_WHERECLAUSE,null,null, null,null);
        while (csr.moveToNext()) {
            String original_tablename = csr.getString(csr.getColumnIndex(SQLITE_MASTER_NAMECOLUMN));
            String original_sql = csr.getString(csr.getColumnIndex(SQLITE_MASTER_SQLCOLUMN));
            String transition_tablename = original_tablename + TRANSITION_SUFFIX;
            String transition_sql = original_sql.replace(original_tablename,transition_tablename).replace("CREATE TABLE","CREATE TABLE IF NOT EXISTS");
            Log.d("PREAPRE4SWITCH","Executing the SQL (create transition table for table " + original_sql +
                    ") \n\t" + transition_sql);
            mDB.execSQL(transition_sql);
            mDB.delete(transition_tablename,null,null); // just to make sure that all transition tables are empty
        }
        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }

    public void doSwitchAllAppTables() {

        boolean already_in_transaction = mDB.inTransaction();
        if (!already_in_transaction) {
            mDB.beginTransaction();
        }
        Cursor csr = mDB.query(SQLITE_MASTER,SQLITE_MATSER_COLUMNS,APPTABLES_WHERECLAUSE,null,null, null,null);
        ArrayList<String> tables_to_delete = new ArrayList<>();
        while (csr.moveToNext()) {

            String original_name = csr.getString(csr.getColumnIndex(SQLITE_MASTER_NAMECOLUMN));
            String transition_name = original_name + TRANSITION_SUFFIX;
            String rename_name = RENAME_SUFFIX;
            tables_to_delete.add(rename_name);

            Log.d("SWITCHRENAMEORIG","Executing the SQL to rename(original) " + original_name + " table to " + rename_name);
            mDB.execSQL("ALTER TABLE " + original_name + " RENAME TO " + rename_name);
            Log.d("SWITCHRENAMETRNS","Executing the SQL to rename(transition) from " + transition_name +
                    " to (original)" + original_name);
            mDB.execSQL("ALTER TABLE " + transition_name + " RENAME TO " + original_name);
        }
        csr.close();
        for (String table_to_delete: tables_to_delete) {
            Log.d("SWITCHDROPRENAMED","Dropping renamed original table " + table_to_delete);
            mDB.execSQL("DROP TABLE If EXISTS " + table_to_delete);
        }

        if (!already_in_transaction) {
            mDB.setTransactionSuccessful();
            mDB.endTransaction();
        }
    }
}
  • 这假设您从不创建以androidsqlite开头的 App 表

    • (sqlite_ 受保护

      以“sqlite_”开头的表名保留供内部使用。尝试创建名称以“sqlite_”开头的表是错误的。)。

    • 所以真的SQLITE_MASTER_NAMECOLUMN + " NOT LIKE 'sqlite%' "应该如此SQLITE_MASTER_NAMECOLUMN + " NOT LIKE 'sqlite_%' "

  • sqlite_master 用于驱动流程,它是模式并包含表列表(以及其他实体,如索引、视图和触发器)。

  • 请注意,如果使用 FOREIGN KEYS 并且未使用 ON CASCADE DELETE,则不能依赖上述内容。因为子表必须在父母之前被删除。

  • 触发器见ALTER TABLE - ALTER TABLE RENAME

结果

在日志中运行上述结果,包括:-

...........
04-26 13:41:36.000 I/System.out:    mydata=3010
04-26 13:41:36.000 I/System.out: }
04-26 13:41:36.000 I/System.out: <<<<<



04-26 13:41:36.000 D/PREAPRE4SWITCH: Executing the SQL (create transition table for table CREATE TABLE "master"(_id INTEGER PRIMARY KEY,mydata TEXT)) 
        CREATE TABLE IF NOT EXISTS "master_trn"(_id INTEGER PRIMARY KEY,mydata TEXT)
04-26 13:41:36.007 D/NEWDATALOADSTART: Loading of new data has started
04-26 13:41:43.666 D/TABLESNOTAVAILABLE: Tables will now be unavailable whil switching
04-26 13:41:43.666 D/SWITCHRENAMEORIG: Executing the SQL to rename(original) master table to rename
04-26 13:41:43.667 D/SWITCHRENAMETRNS: Executing the SQL to rename(transition) from master_trn to (original)master
04-26 13:41:43.667 D/SWITCHDROPRENAMED: Dropping renamed original table rename
04-26 13:41:43.673 D/TABLESAVAILABLE: Switch completed, Tables are now available with new data
04-26 13:41:43.673 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@9ce45fc
04-26 13:41:43.673 I/System.out: 0 {
04-26 13:41:43.673 I/System.out:    _id=1
04-26 13:41:43.673 I/System.out:    mydata=10000
04-26 13:41:43.673 I/System.out: }
04-26 13:41:43.673 I/System.out: 1 {
04-26 13:41:43.673 I/System.out:    _id=2
04-26 13:41:43.673 I/System.out:    mydata=10001
04-26 13:41:43.673 I/System.out: 
..........

最后

您可能对以下内容感兴趣:-

WAL 提供了更多的并发性,因为读取器不会阻塞写入器,写入器不会阻塞读取器。读和写可以同时进行。

在这种情况下,您可以使用enableWriteAheadLogging(覆盖onConfigure()方法以使用它)打开 WAL(Android 9+ 默认情况下打开)。


推荐阅读