同时更新两个表,但如果更新失败则回滚

Updating two Tables at same time but rollback if update fails

假设我们有上图。我们有两个表,我想 update.In 如果一个更新失败,我想回滚两个更新。

我想要最好的性能,所以我意识到我能想到的解决方案只有一个:

viewModel.updateTable1(
    updatedId1 ->{ 
        viewModel.updateTable2(
            updatedId2 ->{ 
                Toast.makeText(this,"great, both succes!!!! :)",Toast.LENGTH_LONG);
            }, 
            er->{
                //somehow a rollback
            }
    }, 
    er->{
        Log.e("error",er.getMessage())
    }
);

然而,到目前为止,我并不是我的解决方案的忠实拥护者,因为我需要等到 table1 得到更新,然后我才能更新 table2。所以我的问题是,是否可以更新 2 个或更多表,但在一次更新失败时回滚两个事务?

您最好的选择是 运行 在单个事务中进行两个更新。

一个示例,尽管在 Java 中,但故意设计为失败(在第二次更新时),从而回滚第一次成功的更新。

所以首先我们有一个更新方法(Kotlin 中的函数),它利用和更新 AllDao 接口中的 Dao @Update int update(Bookmark bookmark); :-

public static void updateTwo(TheDatabase db, Bookmark bookmark1, Bookmark bookmark2) {
    boolean updateOk = true;
    AllDao dao = db.getAllDao();
    SupportSQLiteDatabase sdb = db.getOpenHelper().getWritableDatabase();
    sdb.beginTransaction();
    if (dao.update(bookmark1) > 0) {
        if (dao.update(bookmark2) < 1) {
            Log.d("UPDATETWO","Second Update failed");
            updateOk = false;
        }
    } else {
        Log.d("UPDATETWO","First Update failed to update anything.");
        updateOk = false;
    }
    if (updateOk) {
        Log.d("UPDATETWO","Both updates OK");
        sdb.setTransactionSuccessful(); //<<<<< ONLY SET TRANSACTION OK if both updates (aka ROLLBACK if not both OK)
    }
    sdb.endTransaction();
}
  • 注意使用了 SupportSQliteDatabase 但实际上 runInTransaction 应该使用(正在考虑使用它,所以我可能会更新答案)
  • 包含用于显示结果的日志记录。
  • P.S。我允许 运行ning 在主线程上用于 convenience/brevity

并且在 Activity 中我有设计为不更新的代码(即更新失败(尽管它不只是不更新​​,我认为这就是你所说的失败而不是比抛出异常)):-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDao dao;


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

        db = TheDatabase.getInstance(this);
        dao = db.getAllDao();
        dao.deleteAll();
        Bookmark b1 = new Bookmark(), b2 = new Bookmark();

        // Insert Row
        b1.setPostTitle("P1");
        b1.setPostUrl("U1");
        b1.setId(dao.insert(b1)); // sets the id so row can be updated

        // Prepare for update
        b1.setPostUrl("URL1"); // change so there is an update to be performed i.e. change U1 to URL1
        // Prepare a second (no-existent so doomed to fail) Row for update
        b2.setPostTitle("P2");
        b2.setPostUrl("U2");
        // Do the update
        Bookmark.updateTwo(db,b1,b2);
        for (Bookmark b: dao.getAll()) {
            Log.d("BOOKMARKINFO","ID = " + b.getId() + " PostTitle = " + b.getPostTitle() + " PostURL =" + b.getPostUrl());
        }
    }
}

结果

日志:-

D/UPDATETWO: Second Update failed
D/BOOKMARKINFO: ID = 1 PostTitle = P1 PostURL =U1

即该行返回到 U1 而不是 URL1(又名 ROLLED BACK)

如果在更新之前添加行 b2.setId(dao.insert(b2));(因此第二行存在并因此将被更新(即使实际上没有数据发生变化))那么日志是:-

D/UPDATETWO: Both updates OK
D/BOOKMARKINFO: ID = 1 PostTitle = P1 PostURL =URL1
D/BOOKMARKINFO: ID = 2 PostTitle = P2 PostURL =U2

即第一行是 URL1(又名 NOT ROLLED BACK)

  • 以上仅适用于单行更新,因为它只检查是否进行了任何更新,而不是更新匹配的数量。

更新

这是使用 运行InTransaction 的等效项。似乎“除非在 Runnable 中抛出异常,否则事务将被标记为成功”。仅当抛出 SQLite 异常时。因此添加了一个不稳定的 dao 来强制异常,即

@Insert(onConflict = OnConflictStrategy.ABORT)
long abort(Bookmark bookmark);
  • 不稳定,因为它假设书签存在,因此无法插入,因为违反了 UNIQUE 约束(id 列)。

替代代码为:-

public static void update2(TheDatabase db,Bookmark b1, Bookmark b2) {
    try {
        db.runInTransaction(new Runnable() {
            @Override
            public void run() {
                try {
                    upd(db, b1, b2);
                } catch (Exception e) {
                }
            }
        });
    } catch (Exception e) {}
}

private static void upd(TheDatabase db, Bookmark b1, Bookmark b2) throws Exception {
    boolean updateOk = true;
    AllDao dao = db.getAllDao();
    if (dao.update(b1) > 0) {
        if(dao.update(b2) < 1) {
            updateOk = false;
            Log.d("UPDATETWO","Second Update failed");
        }
    } else {
        Log.d("UPDATETWO","First Update failed to update anything.");
    }
    if (!updateOk) {
        dao.abort(b1); // Force SQLite Exception to ROLLBACK
    } else {
        Log.d("UPDATETWO","Both updates OK");
    }
} 

activity 中的以下代码用于测试:-

    dao.deleteAll();
    Bookmark b1 = new Bookmark(), b2 = new Bookmark(), b3 = new Bookmark();

    // Insert Row
    b1.setPostTitle("P1");
    b1.setPostUrl("U1");
    b1.setId(dao.insert(b1)); // sets the id so row can be updated

    // Prepare for update
    b1.setPostUrl("URL1"); // change so there is an update to be performed i.e. change U1 to URL1
    // Prepare a second (no-existent so doomed to fail) Row for update
    b2.setPostTitle("P2");
    b2.setPostUrl("U2");
    // Do the update
    Bookmark.updateTwo(db,b1,b2);
    logInfo("-A1");
    b2.setId(dao.insert(b2));
    Bookmark.updateTwo(db,b1,b2);
    logInfo("-A2");

    b1.setPostUrl("U R L 1");
    b3.setPostTitle("P3");
    b3.setPostUrl("U3");
    Bookmark.update2(db,b1,b3);
    logInfo("-B1");
    b3.setId(dao.insert(b3));
    Bookmark.update2(db,b1,b3);
    logInfo("-B2");
}

private void logInfo(String extra) {
    for (Bookmark b: dao.getAll()) {
        Log.d("BMINFO" + extra,"ID = " + b.getId() + " PostTitle = " + b.getPostTitle() + " PostURL =" + b.getPostUrl());
    }
}

这使用两种方法 -A(根据日志)使用 SupportSQliteDatabase -B 使用 runInStransaction.日志:-

2021-07-01 13:19:50.944 D/UPDATETWO: Second Update failed
2021-07-01 13:19:50.946 D/BMINFO-A1: ID = 1 PostTitle = P1 PostURL =U1
2021-07-01 13:19:50.949 D/UPDATETWO: Both updates OK
2021-07-01 13:19:50.951 D/BMINFO-A2: ID = 1 PostTitle = P1 PostURL =URL1
2021-07-01 13:19:50.951 D/BMINFO-A2: ID = 2 PostTitle = P2 PostURL =U2
2021-07-01 13:19:50.953 D/UPDATETWO: Second Update failed
2021-07-01 13:19:50.963 D/BMINFO-B1: ID = 1 PostTitle = P1 PostURL =URL1
2021-07-01 13:19:50.963 D/BMINFO-B1: ID = 2 PostTitle = P2 PostURL =U2
2021-07-01 13:19:50.966 D/UPDATETWO: Both updates OK
2021-07-01 13:19:50.969 D/BMINFO-B2: ID = 1 PostTitle = P1 PostURL =U R L 1
2021-07-01 13:19:50.969 D/BMINFO-B2: ID = 2 PostTitle = P2 PostURL =U2
2021-07-01 13:19:50.969 D/BMINFO-B2: ID = 3 PostTitle = P3 PostURL =U3

编辑 重新评论:-

Wouldnt't the anotation Transaction safe me the "boilercode"

尝试使用:-

@Transaction
public static boolean update3(AllDao dao,Bookmark b1, Bookmark b2) {
    boolean updateOk = true;
    if (dao.update(b1) > 0) {
        if (dao.update(b2) < 1) {
            updateOk = false;
        }
    }
    if (!updateOk) {
        dao.abort(b1);
    }
    return updateOk;
}

但是,中止会导致崩溃,例如:-

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so67958704javaroomconvertexistingdb, PID: 6136
    java.lang.RuntimeException: Unable to start activity ComponentInfo{blah....}: android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: mylist_data.ID (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)
....

所以我认为 @TransactionrunInTransaction 不同,它不是为回滚而设计的。