0

In our Realm Swift SDK we are using GraphQL. The query is an aggregate with two lookups, it takes quite long (3s+) until 10 results (of a collection with only 200 entries) are displayed.

Query code:

let aggregatePipeline: [Document] = [
                "$match": [
                    "$and": [
                        [ "example": AnyBSON(stringLiteral: "test")           ],
                        [ "_id": ["$ne": AnyBSON("123") ] ]
                    ]
                ]
            ]
// realm database object loaded elsewhere
let collection = database.collection(withName: "Example")
do {
            let resultObjectsRaw = try await collection.aggregate(pipeline: aggregatePipeline)
} catch {
            print("error")
        }

The query only took around 0.5s or less. The rest of the time is converting the result to our swift objects (Subclassed from RealmSwift's Object, see implementation below) (sometimes up to 1s per object). Is there a better way to convert the result of the query to swift objects?

This is what the result resultObjectsRaw looks like:

array(
  [
      Optional(RealmSwift.AnyBSON.document([
           "_id": Optional(RealmSwift.AnyBSON.string("xyz")), 
           "example": Optional(RealmSwift.AnyBSON.string("yzx"))
       ])),
      Optional(RealmSwift.AnyBSON.document([
           "_id": Optional(RealmSwift.AnyBSON.string("142")), 
           "example": Optional(RealmSwift.AnyBSON.string("647"))
       ]))
])

and we need it as an array of Swift objects. i.e:

class ExampleObject: Identifiable {
    var id = UUID().uuidString
    var example: String?
    
    init(id: String? = nil, example: String? = nil) {
        self.init()
        if let id = id {
            self.id = id
        }
        self.example = example
    }
}

This is what we currently use to convert:

guard let documentId = document["_id"]??.stringValue else { return nil }
let property = document["example"]??.stringValue
// more properties ...
let object = ExampleObject(id: documentId, example: example) // creating the swift object

It should be noted that our actual objects have around 10 properties, where one property is a linked document (thus the lookup number 1) and this linked document once again has a linked document (thus lookup number 2). But all properties are either strings, Date objects or similarly "small" data.

P.S: There is this similar question on the MongoDB Community Forum, however it has no solution and is around a year old..

D. Kee
  • 169
  • 14
  • 1
    I don't quite follow the code - A Realm Object is (must be) subclassed; like this `class PersonClass: Object {...}` so this `let object = Object(_id: documentId` is a little puzzling. What's the result of this `document["property"]??.stringValue` as my XCode says "Cannot use optional chaining on non-optional value of type 'String'". You mention - *converting the result to our swift objects* - do you mean Realm objects or Swift objects?. Last thing is about *a better way* of converting the results of a query - as in Realm Results? Can you clarify the question a bit and address the comments? – Jay Jan 19 '23 at 20:32
  • @Jay thank you for the response and sorry for the confusion. I edited the question, adding all relevant code snippets and making them more coherent. The reason it might be confusing is because I might be overlooking some clear and easy way to convert to *RealmSwift* `Object`s easily.. The `document["property"]??.stringValue` returns a string which is then used in a conveniece init as can be seen in the new code snippet. – D. Kee Jan 20 '23 at 07:33
  • @Jay What confuses me is that this seems so relevant and basic, yet I find nothing and no one mentioning how this is supposed to be done correctly. Thus I suspect I might be missing something obvious. Before, I was using Realm sync with which this is not a problem since one can just directly access the objects of the synced realm.. – D. Kee Jan 20 '23 at 13:45
  • Update: after searching some more I stumbled across `Decodable` and also, [this question on SO](https://stackoverflow.com/q/39951647/9739903) not sure if any of this is relevant in my case... – D. Kee Jan 20 '23 at 13:54
  • Ok. I see the issue(s). First this `class ExampleObject: Object, ObjectKeyIdentifiable` is not a Swift object, it's a Realm Object. Second thing is you're not using Realm. You're querying the the MongoDB server directly using the [Swift SDK](https://www.mongodb.com/docs/realm/sdk/swift/app-services/mongodb-remote-access/). Realm is part of the SDK but provides a lot of other features (sync, local-first storage etc). On a personal note, I know what this `document["property"].stringValue` does.... what does this `document["property"]??.stringValue` (with the ??)? – Jay Jan 20 '23 at 18:52
  • @Jay Yes, that's entirely correct. However we are still using Realm (sync features and more) elsewhere in our app, only for these particular objects we need to get them using queries since it is an *infinite feed* (which does not work very well with sync...). I edit the question again since unfortunately I used code from the wrong commit; we actually **are** using Swift objects here, though we could also use Realm `Object`s if that would make more sense. In conclusion, how would you recommend to deserialise (if that's the correct term) the data? – D. Kee Jan 20 '23 at 20:02
  • Is the issue mapping the documents returned from Atlas to Swift classes/structs? You should be able to map the keys in the documents to the properties of your swift class - I can add that as an answer if it would help. – Jay Jan 20 '23 at 21:06
  • @Jay yes I suppose that is the issue. Regarding the `??` it is simply what is necessary since each property (as seen in my code snippet) is `Optional`. The second `?` is added because Xcode "told me to". I assume it's because while unwrapping a key like `["keyName"]` is not necessarily present, thus the result is `Optional` again.. – D. Kee Jan 20 '23 at 21:45

1 Answers1

1

I believe the objective here is to retrieve documents from an Atlas collection, those are then used to populate Swift objects (non-Realm) and stored in an array.

For clarity, this question/answer does not use the Realm database - it uses the Swift SDK to communicate directly with MongoDB to retrieve the collections. There are no realm objects involved - but the UnmanagedTaskClass (see below) could easily be converted to a Realm object if local/synced persistence is needed.

In this example, we are loading all of the Tasks from a given _partitionKey into an array. The code was tested with 10,000 objects - ignoring the retrieval time from MongoDB, the 'conversion' took about a second to complete.

Starting with an array to hold the Swift objects

var myTasks = [UnmanagedTaskClass]()

and then the code to retrieve the documents from a MongoDB collection

collection.find(filter: ["_partitionKey": AnyBSON(whichPartition)], { (result) in
    // Note: this completion handler may be called on a background thread.
    //       If you intend to operate on the UI, dispatch back to the main
    //       thread with `DispatchQueue.main.async {}`.
    switch result {
    case .failure(let error):
        print("Call to MongoDB failed: \(error.localizedDescription)")
        return
    case .success(let documents):
        documents.forEach({(document) in
            let task = UnmanagedTaskClass(withDoc: document)
            //print(task._id, task.partitionkey, task.name, task.status)
            self.myTasks.append(task)
        })
    }
})

then the document is used to instantiate an UnmanagedTaskClass via the convenience init

class UnmanagedTaskClass {
    var partitionkey = ""
    var status = ""
    var name = ""
    var _id: ObjectId!
    
    convenience init(withDoc: [String: AnyBSON?] ) {
        self.init()
        
        withDoc.forEach { dict in
            switch dict.key {
            case "_partitionKey":
                self.partitionkey = dict.value?.stringValue ?? "No partition"
                
            case "status":
                self.status = dict.value?.stringValue ?? "No status"
                
            case "name":
                self.name = dict.value?.stringValue ?? "No name"
                
            case "_id":
                self._id = dict.value?.objectIdValue ?? ObjectId()
                
            default:
                break
            }
        }
    }
}

The above could be improved and shortened using Codable protocols but that also comes with its own set of issues. I prefer the above for this use case.

If you're uploading data to MongoDB, there's some options using codable in this answer.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Thanks @Jay for the detailed answer. It certainly makes more sense to create an init and convert the values there. But I feel like this would result in the same performance issues as my code now, since it is the same way of converting essentially. I assume the lacking efficiency is that in my case there are embedded documents, even two inside each other. That's why I would really like to see how it can be done with `Codable`. I have been researching a bit but am stuck at converter the `Document`(under the hood: `Dictionary`) to some value that a `JSONDecoder` can work with.. – D. Kee Jan 22 '23 at 10:57
  • I opened a [new question](https://stackoverflow.com/q/75199983/9739903) since I realise my question has shifted quite a bit thanks to your input. I will later try to implement it the way you describe here and accept this answer if simply moving it to the init directly will give performance improvements. – D. Kee Jan 22 '23 at 11:21