【问题标题】:Room database: getting SELECT MAX() twice after creating new RecyclerView item房间数据库:创建新的 RecyclerView 项目后两次获取 SELECT MAX()
【发布时间】:2019-11-10 16:45:36
【问题描述】:

我有一个正常工作的 CardView 项目的 RecyclerView 列表。在创建插入数据库的新 CardView 后,我想触发一个 Toast,通知用户 CardView 已成功添加并显示 CardView 编号。 CardView 编号是插入数据库的 CardView 项的 Id。当用户单击触发 onClickSave() 的保存按钮时,数据将保存到数据库中。

我在 Dao 中设置了一个@Query 来获取 MAX(cardId):

Dao
...
@Query("SELECT MAX(cardId) FROM cards")
LiveData<Integer> getMax();

@Insert
void insertCard(Card card);

问题是两个 Toast 正在触发。第一个 Toast 正在返回之前创建的 CardView 编号,然后第二个 Toast 正在触发并显示刚刚添加的最新 CardView 编号。例如,Toast 将显示 CardView 编号 33,然后触发第二个 Toast,显示刚刚创建的预期 CardView 编号 34(我确认 CardViews 33 和 34 都在数据库中和两个最高的项目中,使用 DB Browser for SQLite 软件)。

AddorUpdateCardActivity
...
private int newMax = -1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mViewModel = new ViewModelProvider(this).get(cardViewModel.class);        
}

public void onClickSave(View v) {

    // set card data
    // then insert data in database
    mViewModel.insertCard(card1);     

    mViewModel.getMax().observe(this, value -> { newMax = value; Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();});
}

ViewModel
...

public cardViewModel(Application application) {
    super(application);
    repository = new cardRepository(application);
    getMax = repository.getMax();
}

public LiveData<Integer> getMax() {
    return getMax;
}

public void insertCard(Card card) {
    repository.insertCard(card);
}

cardRepository

private CardDao cardDao;
private LiveData<Integer> getMax;


public cardRepository(Application application) {
    RoomDatabase db = RoomDatabase.getDatabase(application);
    cardDao = db.cardDao();
}

public LiveData<Integer> getMax() {
    return cardDao.getMax;  
}

public void insertCard(Quickcard newcard) {
    AsyncTask.execute(() -> cardDao.insertCard(newcard));

} 

我在这里缺少什么?如果卡片正确插入数据库,那么为什么 ViewModel 观察者不只返回这个新的 CardView 编号而不是两个 Toast?

作为参考,我展示了在 Room 和 ViewModel 之前使用的先前代码,它使用光标来获取最新和最高插入的 Id:

public class SQLiteDB extends SQLiteOpenHelper {

    ...
    public int getLastInsertId() {

    int index = 0;
    SQLiteDatabase sdb = getReadableDatabase();
    Cursor cursor = sdb.query(
            "sqlite_sequence",
            new String[]{"seq"},
            "name = ?",
            new String[]{TABLE_NAME},
            null,
            null,
            null,
            null
    );

    sdb.beginTransaction();
    try {
        if (cursor !=null) { 
            if (cursor.moveToLast()) {                    
                index = cursor.getInt(cursor.getColumnIndex("seq"));
            }
        }
    ...
    }         
    return index;
}      

【问题讨论】:

  • 没有`mViewModel.insertCard(card1); `ViewModel 中的方法。分享那部分代码。
  • @GensaGames 会在美国东部时间下午 6 点之前完成。
  • @GensaGames ViewModel 的 insertCard() 已更新。 Repository 和 Dao 也是如此。
  • 你能把Toast移到mViewModel.getMax().observe()里面吗?您的 Toast 可能会在数据保存和 newMax 值更新之前触发。 @AJW
  • @Prokash Sarkar 我明白你的意思,但我不知道如何将 Toast 移到里面。你能举个例子吗?

标签: android android-room android-viewmodel


【解决方案1】:

您在onClickSave 中调用的视图模型操作是异步的:

public void onClickSave(View v) {
    mViewModel.insertCard(card1);
    mViewModel.getMax().observe(this, value -> { newMax = value; makeText(AddorUpdateCardActivity.this, "TEXT", .LENGTH_LONG).show();});
}

LiveData 的实现记录了数据版本以及观察者看到的最后一个版本。

因此,insertCard 开始在工作线程上运行,而您开始使用新创建的观察者从主线程观察 getMax。因此,您将收到当前值以及数据库更新后的新值。

相反,您只能在onCreate() 中观察一次,然后等待数据库触发的更新:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mViewModel = new ViewModelProvider(this).get(cardViewModel.class);
    mViewModel.getMax().observe(this, value -> { newMax = value; makeText(AddorUpdateCardActivity.this, "TEXT", .LENGTH_LONG).show();});
}

public void onClickSave(View v) {
    mViewModel.insertCard(card1);
}

【讨论】:

  • 啊,好的。那么如何将 Toast 限制为仅由 insert 方法触发(将 CardView 添加到数据库时),以便在发生其他 CRUD 操作(如删除或更新方法)时不会触发它?
  • 我从数据库中捕获了现有的 MAX,然后在 insertCard() 之后将其与 MAX 进行比较,以确定添加了新卡。答案被接受、投票和赏金。为你干杯。
  • 后续问题:如果我将getMax()观察者放在onCreate()中,如何判断更新已经被数据库触发了?所以我可以确保我获得的是最新版本而不是数据版本。
  • 我觉得getMax不应该用来判断insertCard的正确性。您应该相信 Room 总是从数据库返回正确的状态。您应该使用数据库插入调用的结果来验证插入是否成功。
  • 好的,那么如何确保验证插入成功的代码在插入后立即运行?单击“保存”按钮运行 insertCard 代码。插入后,我试图在卡上设置 PendingIntents 以便稍后发出通知警报。我正在尝试从新插入的卡(例如 card.getId())中获取数据以设置 PendingIntents,但我不确定如何在“onClickSave”的同一代码块中获取该数据。如有任何关于如何纠正的见解或想法,我将不胜感激。
【解决方案2】:

AsyncTask 中的 Room Insert 操作需要一段时间才能更新 maxCount 变量。由于您在按钮单击中显示Toast,因此该消息会立即显示,而不会从LiveData 接收到更新的值。

Toast 消息移动到obverve() 方法中,使其仅在LiveData 更改后触发。

mViewModel.getMax().observe(this, value -> {
        newMax = value;
        Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();
    });

此时,代码应该可以运行,但您会为单个 Insert 获得多个 LiveData 事件。发生这种情况是因为您为 InsertQuery 操作使用了 2 个单独的 Dao 实例。

public cardRepository(Application application) {
    RoomDatabase db = RoomDatabase.getDatabase(application);
    cardDao = db.cardDao(); // <---------- Instance #1
    getMax = cardDao.getMax();
}

public LiveData<Integer> getMax() {
    return getMax;  
}

 public void insertCard(Card newcard) {
    new InsertAsyncTask(quickcardDao).execute(newcard);
}

private static class InsertAsyncTask extends AsyncTask<Card, Void, Integer> {

    private CardDao asyncTaskDao;

    InsertAsyncTask(CardDao dao) {
        asyncTaskDao = dao; // <---------- Instance #2
    }

    @Override
    protected Integer doInBackground(final Card... params) {

        asyncTaskDao.insertCard(params[0]);
        return null;
    }
}

要解决它,请在任何地方使用相同的 Dao 实例:

public cardRepository(Application application) {
        RoomDatabase db = RoomDatabase.getDatabase(application);
        cardDao = db.cardDao();
    }

    public LiveData<Integer> getMax() {
        return cardDao.getMax();  
    }

     public void insertCard(Card newcard) {
        AsyncTask.execute(() -> cardDao.insertCard(newcard));
    }

【讨论】:

  • 啊,好的,试试你的修复。我应该为所有 CRUD 操作使用相同的 Dao 实例,对吗?那么 deleteCard() 和 updateCard() 也应该使用“cardDao...”而不是他们自己独特的 Dao 实例?
  • Toast 触发了,但它再次触发了两次,即使在为 insert() 和 getMax() 创建了一个 Dao 实例之后也是如此。根据您的回答查看上面的修改后的代码。首先 Toast 开火并说之前的卡# 已保存。然后它再次开火并说最后一张牌#被保存了。因此,第二个 Toast 正确触发并显示为刚刚插入数据库的最后一张卡片保存的正确卡片 #。但是第一个 Toast 不应该被触发,因为 CardView 之前已保存。关于如何解决这个问题的任何想法?
【解决方案3】:

由于使用 AsyncTask 将卡片插入数据库,该功能需要一些时间才能完成,并且您会立即展示您的吐司!将您的活动更改为:

AddorUpdateCardActivity
...
private int newMax = -1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mViewModel = new ViewModelProvider(this).get(cardViewModel.class);

    mViewModel.getMax().observe(this, integer2 -> {
        newMax = integer2;
        Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();
        hideProgressBar();
    });
}

public void onClickSave(View v) {

    //set card data
    // then insert data in database
    mViewModel.insertCard(card1);

    showProgressBar();
}

【讨论】:

  • 我希望在用户单击将新的 CardView 数据插入数据库后触发 Toast。那么我是否不必将 mViewModel.getMax()... 的观察者代码移出 onCreate() 并将其放在 onClickSave() 中的 mViewModel.insertCard() 之后?如果不是,如果 Toast 位于 onCreate() 中,它是如何触发的?
  • 此外,当列表为空时应用程序崩溃,我尝试使用 AddorUpdateCardActivity 添加第一张卡片。 Logcat 说整数是空对象引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多