Android Room - 如何在每个应用程序运行时重置自动生成的表主键

Posted

技术标签:

【中文标题】Android Room - 如何在每个应用程序运行时重置自动生成的表主键【英文标题】:Android Room - How to reset auto generated table primary key on each app run 【发布时间】:2018-11-25 11:39:11 【问题描述】:

我正在使用 Room 来保存数据。 我有一个实体,它具有模拟票证系统的自动生成(autoGenerate)主键。 在每次运行应用程序时,我都需要此密钥以从 0 开始

实体:

@Entity
public class SequenceAction 

    @PrimaryKey(autoGenerate = true)
    private Integer sequenceId;
    private String actionType;
    private String extraInfo;
    //getters & setters

初始化:

// init sequenceAction object
// run with executor(sequenceId is automatically set on insert to table):
AppDatabase.getInstance(getContext()).sequenceActionDao().save(sequenceAction);

我尝试过的事情:

我使用AppDatabase.getInstance(getApplicationContext()).clearAllTables(); 在退出时清除表,但这不会重置键起始索引,而是从上次运行时停止的位置开始。

我还没有找到使用 Room 执行此操作的方法,因此我正在尝试将 SimpleSQLiteQuery 传递给我的 Dao 中的 RawQuery 方法:

//Dao
@RawQuery()
Integer init(SimpleSQLiteQuery query);

//Passed query
new SimpleSQLiteQuery("...query...");

我已经尝试了下一个查询:

    "ALTER TABLE SequenceAction AUTO_INCREMENT = 0"

我得到一个错误(我用'AUTOINCREMENT'尝试过,同样的错误):

android.database.sqlite.SQLiteException:靠近“AUTO_INCREMENT”:语法错误(代码 1):,编译时:ALTER TABLE SequenceAction AUTO_INCREMENT = 0

可能是因为,正如this question/answer 所说,SQLite 中没有自动增量关键字,而是声明为 INTEGER PRIMARY KEY 的列会自动自动增量。

    "delete from sqlite_sequence where name='SequenceAction'"

没有错误,但是索引也没有重置。

    如建议here:

    "UPDATE SQLITE_SEQUENCE SET seq = -1 WHERE name = 'SequenceAction'"

没有错误但是没有效果。

    "TRUNCATE TABLE 'SequenceAction';"

错误(可能是因为SQLite doesn't support the TRUNCATE command):

android.database.sqlite.SQLiteException: near "TRUNCATE": syntax error (code 1): , while compile: TRUNCATE TABLE 'SequenceAction';

    所以...最后一次尝试:DELETE FROM SequenceAction

没有错误,没有效果。

【问题讨论】:

你想每次都从一个全新的数据库开始,还是只清除这一个表和键? @TKK 我的问题是每次都有一个全新的数据库,但是,如果您两者都知道,这可能是我想使用的未来方法,所以知道这一点也很棒。跨度> 【参考方案1】:

尝试了很多方法。最后这对我有用!

public static void truncateTable(Context context, SupportSQLiteOpenHelper openHelper, String tableName) 
            SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
                    context.getDatabasePath(openHelper.getDatabaseName()),
                    null
            );

            if (database != null) 
                database.execSQL(String.format("DELETE FROM %s;", tableName));
                database.execSQL("UPDATE sqlite_sequence SET seq = 0 WHERE name = ?;", new String[]tableName);
            
        

实施:

truncateTable(getContext(), yourRoomDatabase.getOpenHelper(), "your_table_name");

【讨论】:

【参考方案2】:

以MikeT 所说的为模型。

我认为这可行:

        fun clearAndResetAllTables(): Boolean 
        if (db == null) return false

        // reset all auto-incrementalValues
        val query = SimpleSQLiteQuery("DELETE FROM sqlite_sequence")

        db!!.beginTransaction()
        return try 
            db!!.clearAllTables()
            db!!.query(query)
            db!!.setTransactionSuccessful()
            true
         catch (e: Exception)
            false
         finally 
            db!!.endTransaction()
        
    

【讨论】:

你确定在完成时真的需要 setTransactionSuccessful 吗?似乎在这种情况下您可以获得异常(developer.android.com/reference/android/arch/persistence/room/…)【参考方案3】:

为了在退出时清除表,但这不会重置密钥 起始索引,而是从上次运行时停止的位置开始。

....

“从 sqlite_sequence 中删除 name='Sequence Action'”没有错误 但是,索引也没有重置。

您必须同时删除 SequenceAction 表中的所有行并从 sqlite_sequence 中删除相应的行。

也就是说,当使用 AUTOINCREMENT 关键字时,会使用不同的算法。这大致是:-

找出两者中的最大值 - a) sqlite_sequence 编号中的表的值存储和 - b) 最高的rowid值

另一种方法是不使用AUTOINCREMENT 关键字,而只使用?? INTEGER PRIMARY KEY(其中?? 表示列名)。

您仍然会有一个唯一的 ID,它是 rowid 库尔姆的别名,但不能保证它会一直增加。 AUTOINCREMENT 确实保证唯一 id 递增,但不保证唯一 rowid 单调递增。

在每次运行应用程序时,我都需要此密钥从 0 开始。

但是,SQLite 会将第一个值设置为 1 而不是 0。

以下确实有效,正如您在 AUTOINCREMENT 中看到的那样(虽然有点小技巧):-

DROP TABLE IF EXISTS SequenceAction;
DROP TRIGGER IF EXISTS use_zero_as_first_sequence;
CREATE TABLE IF NOT EXISTS SequenceAction (id INTEGER PRIMARY KEY AUTOINCREMENT, otherdata TEXT);
CREATE TRIGGER IF NOT EXISTS use_zero_as_first_sequence AFTER INSERT ON SequenceAction
    BEGIN 
        UPDATE SequenceAction SET id = id - 1 WHERE id = new.id;
    END
;
INSERT INTO SequenceAction VALUES(null,'TEST1'),(null,'TEST2'),(null,'TEST3');
SELECT * FROM SequenceAction;
-- RESET and RESTART FROM 0
DELETE FROM SequenceAction;
DELETE FROM sqlite_sequence WHERE name = 'SequenceAction';
INSERT INTO SequenceAction VALUES(null,'TEST4'),(null,'TEST5'),(null,'TEST6');
SELECT * FROM SequenceAction
2 个 DROP 语句仅用于测试删除和重新定义。

这导致:-

第一个查询返回:-

第二次返回:-

所以本质上你想要:-

DELETE FROM SequenceAction;
DELETE FROM sqlite_sequence WHERE name = 'SequenceAction';

如果您希望编号从 0 而不是 1 开始,还有触发器。

或者,如果您取消了 AUTOINCREMENT,那么您可以使用稍微更改的触发器:-

CREATE TRIGGER IF NOT EXISTS use_zero_as_first_sequence 
    AFTER INSERT ON SequenceAction 
    WHEN (SELECT count() FROM SequenceAction) = 1
    BEGIN 
        UPDATE SequenceAction SET id = 0;
    END
;
这只是对第一个插入的行重新编号(算法然后为后续插入添加 1)

然后只需从 SequenceAction 表中删除所有行,即可重置编号。


使用房间的例子:-

根据您的代码以及上面的示例,以下方法似乎有效:-

private void resetSequenceAction() 
    SQLiteDatabase dbx;
    String sqlite_sequence_table = "sqlite_sequence";
    long initial_sacount;
    long post_sacount;
    long initial_ssn =0;
    long post_ssn = 0;
    Cursor csr;

    /*
        Need to Create Database and table if it doesn't exist
     */
    File f = this.getDatabasePath(TestDatabase.DBNAME);
    if (!f.exists()) 
        File d = new File(this.getDatabasePath(TestDatabase.DBNAME).getParent());
        d.mkdirs();
        dbx = SQLiteDatabase.openOrCreateDatabase(f,null);
        String crtsql = "CREATE TABLE IF NOT EXISTS " + SequenceAction.tablename + "(" +
                SequenceAction.id_column + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                SequenceAction.actionType_column + " TEXT," +
                SequenceAction.extraInfo_column + " TEXT" +
                ")";
        dbx.execSQL(crtsql);
        /*
           Might as well create the Trigger as well
         */
        String triggerSql = "CREATE TRIGGER IF NOT EXISTS user_zero_as_first_rowid AFTER INSERT ON " +
                SequenceAction.tablename +
                " BEGIN " +
                " UPDATE " + SequenceAction.tablename +
                " SET " +
                SequenceAction.id_column + " = " + SequenceAction.id_column + " - 1 " +
                " WHERE " + SequenceAction.id_column + " = new." + SequenceAction.id_column + ";" +
                " END ";
        dbx.execSQL(triggerSql);

     else 
        dbx = SQLiteDatabase.openDatabase(this.getDatabasePath(TestDatabase.DBNAME).getPath(),null, Context.MODE_PRIVATE);
    

    /*
        Add trigger to set id's to 1 less than they were set to
     */
    initial_sacount = DatabaseUtils.queryNumEntries(dbx,SequenceAction.tablename);
    /*
        Delete all the rows at startup
     */
    String deleteAllSequenceIdRowsSql = "DELETE FROM " + SequenceAction.tablename;
    dbx.execSQL(deleteAllSequenceIdRowsSql);
    post_sacount = DatabaseUtils.queryNumEntries(dbx,SequenceAction.tablename);
    /*
        delete the sequence row from the sqlite_sequence table
     */
    csr = dbx.query(sqlite_sequence_table,
            new String[]"seq","name=?",
            new String[]SequenceAction.tablename,
            null,null,null
    );
    if (csr.moveToFirst()) 
        initial_ssn = csr.getLong(csr.getColumnIndex("seq"));
    
    String deleteSqlLiteSequenceRow = "DELETE FROM " +
            sqlite_sequence_table +
            " WHERE name = '" + SequenceAction.tablename + "'";
    dbx.execSQL(deleteSqlLiteSequenceRow);
    csr = dbx.query(
            sqlite_sequence_table,
            new String[]"seq",
            "name=?",
            new String[]SequenceAction.tablename,
            null,null,null
    );
    if (csr.moveToFirst()) 
        post_ssn = csr.getLong(csr.getColumnIndex("seq"));
    
    csr.close();
    Log.d("SEQACTSTATS",
            "Initial Rowcount=" + String.valueOf(initial_sacount) +
                    " Initial Seq#=" + String.valueOf(initial_ssn) +
                    " Post Delete Rowcount =" + String.valueOf(post_sacount) +
                    " Post Delete Seq#=" + String.valueOf(post_ssn)
    );
    dbx.close();

初始运行的结果(即不存在 DB):-

D/SEQACTSTATS: Initial Rowcount=0 Initial Seq#=0 Post Delete Rowcount =0 Post Delete Seq#=0

从后续运行(添加 40 行之后):-

D/SEQACTSTATS: Initial Rowcount=40 Initial Seq#=40 Post Delete Rowcount =0 Post Delete Seq#=0

添加一个方法来列出所有的行,按照:-

private void listAllRows() 
    new Thread(new Runnable() 
        @Override
        public void run() 
            salist = mTestDB.SequenceActionDaoAccess().getAll();
            getSequenceActionList(salist);
        
    ).start();

还有:-

@Override
public void getSequenceActionList(List<SequenceAction> sequenceActionList) 
    for (SequenceAction sa: sequenceActionList) 
        Log.d("SA","ID=" + String.valueOf(sa.getSequenceId()) + " AT=" + sa.getActionType() + " EI=" + sa.getExtraInfo());
    

结果(第一行是ID=0 AT=X0 EI=Y0,即第一行的ID列是0):-

06-17 02:56:47.867 5526-5554/rt_mjt.roomtest D/SA: ID=0 AT=X0 EI=Y0
    ID=1 AT=X0 EI=Y0
    ID=2 AT=X0 EI=Y0
    ID=3 AT=X0 EI=Y0
    ID=4 AT=X1 EI=Y1
    ID=5 AT=X1 EI=Y1
    ID=6 AT=X1 EI=Y1
    ID=7 AT=X1 EI=Y1
06-17 02:56:47.868 5526-5554/rt_mjt.roomtest D/SA: ID=8 AT=X2 EI=Y2
    ID=9 AT=X2 EI=Y2
    ID=10 AT=X2 EI=Y2
    ID=11 AT=X2 EI=Y2
    ID=12 AT=X3 EI=Y3
    ID=13 AT=X3 EI=Y3
    ID=14 AT=X3 EI=Y3
    ID=15 AT=X3 EI=Y3
    ID=16 AT=X4 EI=Y4
06-17 02:56:47.869 5526-5554/rt_mjt.roomtest D/SA: ID=17 AT=X4 EI=Y4
    ID=18 AT=X4 EI=Y4
    ID=19 AT=X4 EI=Y4
    ID=20 AT=X5 EI=Y5
    ID=21 AT=X5 EI=Y5
    ID=22 AT=X5 EI=Y5
    ID=23 AT=X5 EI=Y5
    ID=24 AT=X6 EI=Y6
    ID=25 AT=X6 EI=Y6
    ID=26 AT=X6 EI=Y6
    ID=27 AT=X6 EI=Y6
06-17 02:56:47.870 5526-5554/rt_mjt.roomtest D/SA: ID=28 AT=X7 EI=Y7
    ID=29 AT=X7 EI=Y7
    ID=30 AT=X7 EI=Y7
    ID=31 AT=X7 EI=Y7
    ID=32 AT=X8 EI=Y8
    ID=33 AT=X8 EI=Y8
    ID=34 AT=X8 EI=Y8
    ID=35 AT=X8 EI=Y8
    ID=36 AT=X9 EI=Y9
    ID=37 AT=X9 EI=Y9
    ID=38 AT=X9 EI=Y9
    ID=39 AT=X9 EI=Y9
请注意,由于多个线程在没有控制/排序的情况下运行,结果可能会很奇怪。

addSomeData 使用的方法是:-

private void addSomeData() 
    new Thread(new Runnable() 
        @Override
        public void run() 
            SequenceAction sa = new SequenceAction();
            for (int i=0; i < 10; i++) 
                sa.setSequenceId(0);
                sa.setActionType("X" + String.valueOf(i));
                sa.setExtraInfo("Y" + String.valueOf(i));
                mTestDB.SequenceActionDaoAccess().insertSingleRow(sa);
            
        
    ) .start();

补充说明:-

“我相信你必须在房间之前进入......” - 你的意思是执行 在实例化 Room 之前清除运行索引的 SQL 数据库? - 天哪

不一定,但在 Room 之前 在您尝试对其进行任何操作之前打开数据库。 已添加调用代码(在 Overidden 活动 onStart() 方法中) 之后立即调用对 addSomeData 的某些 Room Db 访问。 – 迈克T

这是一个在 RoomDatabase 被实例化之后,但在它用于访问/打开数据库之前调用 resetSequenceAction 方法的示例(addSomeData 打开已经实例化的数据库并插入 10 行):-

@Override
protected void onStart() 
    super.onStart();
    mTestDB = Room.databaseBuilder(this,TestDatabase.class,TestDatabase.DBNAME).build(); //<<<< Room DB instantiated
    resetSequenceAction(); //<<<< reset the sequence (adding trigger if needed)
    addSomeData(); // This will be the first access open
    addSomeData();
    addSomeData();
    addSomeData();
    listAllRows();

【讨论】:

"从 SequenceAction 中删除;从 sqlite_sequence 中删除,其中名称 = 'SequenceAction';" - 这不会使我的自动递增键从 1 开始。我添加了初始化代码,也许问题出在其他地方? @ghosh 根据修改后的答案,我相信您必须在 Room 之前进入,或者可能不使用 Room 的 execSQL 方法(我认为后者需要前者) “我相信你必须在 Room 之前进入......” - 你的意思是在实例化 Room 数据库之前执行清除运行索引的 SQL 吗? @ghosh 不一定,但在 Room 打开数据库之前,即在您尝试对其进行任何操作之前。添加了调用代码(在 Overidden 活动 onStart() 方法中),并在之后立即调用了对 addSomeData 的一些 Room Db 访问。 resetSequenceAction() 已经为我完成了。感谢您详细的回答!【参考方案4】:

您可以创建一个in memory database,而不是在磁盘上创建它。然后,您每次都会从一张白纸开始。您可能希望根据BuildConfig.DEBUG 的值创建基于内存或磁盘的数据库。

【讨论】:

我希望数据库持续存在,据我所知 - “存储在内存数据库中的信息在进程被终止时消失。” - Android 文档。 @ghosh 那么我不明白你对我对这个问题的评论的回答。您希望数据持久保存,还是希望从一个全新的数据库开始?这些东西是矛盾的。 是的,内存数据库确实可以做到这一点,但是,文档 - “当移动到后台时,进程可能会被系统杀死以释放内存”。我有一个需要持久化的表,另一个可以像你提到的那样重新创建。我要创建 2 个数据库对象吗?每个人拿着一张桌子似乎很浪费。

以上是关于Android Room - 如何在每个应用程序运行时重置自动生成的表主键的主要内容,如果未能解决你的问题,请参考以下文章

Android Room Database:如何嵌入多个实体

Android Room:查询中的每个绑定变量都必须有一个匹配的方法

如何在android中使用Room Library fts4查询整个表

如何在 ROOM android 中修复“不确定如何将光标转换为此方法的返回类型”

如何在首次运行时填充 Android Room 数据库表?

如何将带有 Android Room 的应用添加为使用 makefile 编译的系统应用?