Are Queries Permitted/Valid Inside a Write Transaction?

Context

Suppose I have two objects, like this:

final class Parent: Object
{
    @Persisted var children: List<Child>
    @Persisted var hasFlaggedChildren: Bool
}


final class Child: Object
{
    @Persisted var flags: MutableSet<String>
}

Question

Is it valid/safe to use a query inside a write transaction where I’m updating the property being queried, like this:

func update(children: [Child], newFlags: [String], removedFlags: [String])
{
    try someRealm.write {
    
        // Loop over `children` and, for each, insert all `newFlags` 
        // and remove all `removedFlags`.

        // To update the `hasFlaggedChildren` property on `Parent`, 
        // can I do this in the same write transaction?
        let parents: Results<Parent> = someRealm.objects(ofType: Parent.self)

        for parent: Parent in parents
        {
             let flaggedKids: Results<Child> = parent.children.where({ $0.flags.count > 0 })
             parent.hasFlaggedChildren = (flaggedKids.isEmpty) ? false : true
        }
    }
}

I’m worried that because the write transaction has not been committed when I query for Child objects with an empty flags set, the query will return “stale” results. Is that the case?

I have behavior in my app where the “hasFlaggedChildren” property is “out of sync” (it’s true even though all children have empty flags sets) and I believe the explanation might be this query-inside-the-write-transaction.

Thanks!

There are a couple of typo’s in the code .objects(ofType: Parent.self) should be .objects(Parent.self) for example, but other than that it’s works as is. note this : Results<Child> is not needed

In general the only task that must be within a write is when a managed object is modified so this is the only line that does that

parent.hasFlaggedChildren...

Technically you could encapsulate just that line within a write

try! realm.write {
   parent.hasFlaggedChildren
}

But that leads to the next question; do you have any other code attempting to modify those objects after the query (the read?)

Results objects reflect the current state of those objects - if they are modified elsewhere it will be reflected here so that could be a factor.

@jay Thanks. When does the Results collection reflect the new changes: IMMEDIATELY or when this write transaction is closed and committed?

Note: adding/removing strings to the “flags” set is a change that must occur in the write transaction, so it can’t be narrowed down as you propose. I simply omitted that part for brevity.

This may help - from the docs

The Swift SDK represents each transaction as a callback function that contains zero or more read and write operations. To run a transaction, define a transaction callback and pass it to the realm’s write method. Within this callback, you are free to create, read, update, and delete on the realm. If the code in the callback throws an exception when Realm runs it, Realm cancels the transaction. Otherwise, Realm commits the transaction immediately after the callback.

So that boils down to all or none. It either all passes or all fails as it’s “one thing”. Whatever happens in the transaction, stays in the transaction.

The process can be illustrated by some example code.

In this case, I have a PersonClass object and each person has a name and desc property. The code loads them in, queries for me (Jay) with the property desc set to Jays Desc and then updates my description to “Hello, World”.

The fetched Jay is printed before, and then after the update (you’ll see it’s updated) but then throws an exception so nothing was committed.

do {
    try realm.write {
        let people = realm.objects(PersonClass.self)
        let jayBefore = people.where { $0.name == "Jay" }.first!
        print(jayBefore.name, jayBefore.desc)
        jayBefore.desc = "Hello, World" //update the description
        let jayAfter = people.where { $0.name == "Jay" }.first!
        print(jayAfter.name, jayAfter.desc)
        throw "Throwing"
    }
} catch let err as NSError {
    print(err.localizedDescription)
}

and the output

Jay Jays desc   //this is before the update
Jay Hello, World  //this is after the update
The operation couldn’t be completed. (Swift.String error 1.)  //throw causing the transaction to cancel

If we then retrieve Jay again, it’s unchanged.

Jay Jays desc //back to it's original value

So - the data within the write block is scoped to that block and only changes within that block.

Does that clarify it?

@Jay Right. That’s all straightforward. But I’m not sure the same applies to collection properties on an Object (List, MutableSet, etc.).

If we change the contents of those collection properties during the write transaction, is an immediate query against the collection property while still within the open write transaction, such as:

.where({ $0.collectionProperty.count == 0 })

going to work with the collection property as it exists inside the open write transaction (with changes), or does it query against the “old” version of the collection property that hasn’t been updated in the database yet because the write hasn’t been committed?

I’m looking for a definitive answer because, in testing with a live app that’s using Realm Sync, the answer seems to be a race condition of sorts.

That was the point of my example. Making a change to an object in any fashion is scoped only within the transaction. I changed a property on the jay object

jayBefore.desc = "Hello, World" //update the description

but .desc property could be any property. For example suppose my PersonClass had a dogList property which is a List of DogClass objects. If this is done within the write

jay.dogList.removeAll() //delete all dogs

the dogList property after that call will contain 0 entries (as long as we are within the write). If the write fails the dogList will still contain the original dogs. If it completes, it will contain 0 dogs.

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.