realm / realm-kotlin

Kotlin Multiplatform and Android SDK for the Realm Mobile Database: Build Better Apps Faster.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot modify managed objects outside of a write transaction

SanEffect opened this issue · comments

Your documentation says the following - "Collections are mutable and are backed by their corresponding built-in Kotlin classes. You can add and remove elements in a collection within a write transaction."
But that's not true.

When I run the following code I get "llegalStateException: Cannot modify managed objects outside of a write transaction":

class Board : RealmObject {
    @PrimaryKey
    var id: String = ""
    var name: String = ""
    var flashCards: RealmList<FlashCard> = realmListOf()
}
class FlashCard : RealmObject {
    var id: String = ""
    var frontText: String = ""
    var backText: String = ""
}

val board = realm.query<Board>("id == $0", boardId).first().find()
realm.write {
    board.flashCards.add(flashCard)
}

Maybe I'm doing something wrong?

Hi @SanEffect. I see that the error message is a bit unclear. The issue is that you are trying to update a frozen object. When querying the realm you get objects tied to the version at the point when the query is executed. The write-block executes on the most recent version of the realm, and in this version your board could potentially have been deleted. To update board you must obtain a reference of the object in the context of the write transaction with findLatest(board). If board has been deleted this will return null.

realm.write {
    findLatest(board)?.flashCards.add(flashCard)   
}

Alternatively you can also query for live objects inside the write-block

realm.write { // this : MutableRealm
    val board = this.query<Board>("id == $0", boardId).first().find()
    board.flashCards.add(flashCard)
}

Hi @rorbech. Thanks to your explanation I realized what I was doing wrong. Previously I tried this approach:

realm.write { // this : MutableRealm
    val board = this.query<Board>("id == $0", boardId).first().find()
    board.flashCards.add(flashCard)
}

But my mistake was that I specified not "this.query()", but "realm.query()" (inside a write transaction).
Thank you very much for the detailed explanation! Now adding new elements to the RealmList works perfectly!

@rorbech I think it might be worth keeping this open to track updating the error message to be something that guides the user to a fix.

I'm also getting this same issue in my Compose Multiplatform project, but can't figure out how to fix it. It only happens while I'm updating a RealmList field, with all other fields it works fine.

override suspend fun addQuizToHistory(
        completedQuiz: Quiz,
        earnedPoints: Int
    ): RequestState<Boolean> {
        return if (mongoUser != null) {
            try {
                realm.write {
                    val currentUser = this.query<QuizUser>(
                        query = "ownerId == $0", mongoUser!!.id
                    ).first().find()
                    if (currentUser != null) {
                        currentUser.history.add(
                            QuizHistory().apply {
                                quiz = completedQuiz
                                points = earnedPoints
                                timestamp = Clock.System.now().toEpochMilliseconds()
                            }
                        )
                        RequestState.Success(data = true)
                    } else {
                        RequestState.Error(message = USER_NOT_FOUND)
                    }
                }
            } catch (e: Exception) {
                RequestState.Error(message = e.message.toString())
            }
        } else {
            RequestState.Error(MONGO_USER_NULL_MESSAGE)
        }
    }

Error: Cannot import an outdated object. Use findLatest(object) to find an

@rorbech I've even tried this approach, but still the same issue:

override suspend fun addQuizToHistory(
        completedQuiz: Quiz,
        earnedPoints: Int
    ): RequestState<Boolean> {
        return if (mongoUser != null) {
            try {
                realm.write {
                    val currentUser = this.query<QuizUser>(
                        query = "ownerId == $0", mongoUser!!.id
                    ).first().find()
                    findLatest(currentUser!!)?.history?.add(
                        QuizHistory().apply {
                            quiz = completedQuiz
                            points = earnedPoints
                            timestamp = Clock.System.now().toEpochMilliseconds()
                        }
                    )
                    RequestState.Success(data = true)
                }
            } catch (e: Exception) {
                RequestState.Error(message = e.message.toString())
            }
        } else {
            RequestState.Error(MONGO_USER_NULL_MESSAGE)
        }
    }

Hi @stevdza-san. I am not really certain what is triggering your issue, but if the completedQuiz is an object that is already in the realm, then you need to obtain a reference to the most recent version of it with findLatest(completeQuiz).

Hi @rorbech. I assume you meant to refer to stevdza-san :)

Hi @rorbech. I assume you meant to refer to stevdza-san :)

Yep, sorry.

@rorbech Hey, thanks for the response. I've managed to fix it somehow. Here's my new function implementation, just in case someone might find it helpful:

override suspend fun addQuizToHistory(
        id: ObjectId,
        earnedPoints: Int,
        successRate: Int,
        total: Int,
        correct: Int
    ): RequestState<Unit> {
        return if (mongoUser != null) {
            try {
                realm.write {
                    val currentUser = this.query<QuizUser>(
                        query = "ownerId == $0", mongoUser!!.id
                    ).first().find()
                    if (currentUser != null) {
                        if (currentUser.history.find { it.quizId == id } == null) {
                            currentUser.apply {
                                history.add(
                                    QuizHistory().apply {
                                        quizId = id
                                        points = earnedPoints
                                        success = successRate
                                        totalQuestions = total
                                        correctAnswers = correct
                                        timestamp = Clock.System.now().toEpochMilliseconds()
                                    }
                                )
                            }
                            RequestState.Success(data = Unit)
                        } else {
                            RequestState.Error(QUIZ_ALREADY_COMPLETED)
                        }
                    } else {
                        RequestState.Error(USER_NOT_FOUND)
                    }
                }
            } catch (e: Exception) {
                RequestState.Error(message = e.message.toString())
            }
        } else {
            RequestState.Error(MONGO_USER_NULL)
        }
    }