How to ensure uniqueness of certain fields in collection?

For my iOS app I’ve created custom user data collection for my users. I want to ensure that some of the fields in that collection are unique - like username. My first idea was to call a Realm Function which would first call another function as system user to check if username is taken and if not then write username to custom user data of user that called that function (users can only read and write on partition matching their generated user id).

The issue with this solution is that I assume that 2 users could call this function at the same time and it could pass for both of them. Maybe something like unique indexes could help? Is there a way to add unique index on Realm Object fields? Or maybe is there some other solution for ensuring uniqueness in MongoDB Realm?

A username property within a Realm Object is just a string. So why can’t you run a query for that string within the collection and if it doesn’t exist, create it?

Leveraging a Transaction would be useful:

A transaction is a list of read and write operations that Realm treats as a single indivisible operation.

Essentially it either all passes or all fails. That would help create uniqueness of the selected user name

So why can’t you run a query for that string within the collection and if it doesn’t exist, create it?

That was exactly my idea. But I cannot simply query it with local Realm, because I need to search for this username across all partitions and each user only have access to his own. This is why I’m calling Realm Function setUsername that checks if username is taken and if not then changes username. The only issue I see here is that I don’t know if 2 Realm Functions can run concurrently for 2 users which could cause racing conditions.

Hmm. Two questions.

  1. If each user has their own user space and user name and there’s no sharing of data, then why require unique user names?

  2. It’s very common to have basic user data, such as user names all stored within the same partition and accessible by all users. Have you considered adding that structure? It’s somewhat a denormalization of data but makes tasks like what you’re doing super simple.

  1. As you’ve pointed out right now there is no real reason for username to even exist, but I’m considering adding community aspect to my app later in development and I think it’s better for users to create username during register, then make them do it sometime later after introducing said community features.

  2. Denormalization aspect aside - would that mean that each user would locally keep Realm with some basic data on all the other users? If that would be let’s say username and some low resolution avatar picture - wouldn’t that Realm file be huge and generate lot’s of sync traffic?

Yes, and that’s very common practice. This would be useful when you want to, as mentioned, use other users name or avatars to identify messages or post. Or when you have collaborative app where mulltiple users are working on a project. How about a chat app? You need to know who you’re chatting with and can look it up via that data.

You’re mileage may vary but generally speaking, no.

If you’re adding and removing hundreds of users on a daily basis, maybe. Thinking about the actual numbers, if you store a user name, a small avatar and a user id, that’s a tiny amount of mostly static data; a couple hundred bytes each. Using non-technical round numbers, 10,000 users would be about 5Mb of data.

For completeness, you are able to enforce uniqueness of attributes in your collection by adding unique indexes in Atlas: https://docs.atlas.mongodb.com/data-explorer/indexes/

@Andrew_Morgan

Super great info! Thanks for that tip.

From a MongoDB Realm Swift perspective, if an index is added to a collection and unique is set to true, what’s the expected behavior from the API when an object is added that has the same index?

For example a User object that has the user_name property set as an index and unique in Atlas. What happens with this code when the user with that user_name already exists?

try! realm.write {
   realm.add(userWithDuplicateName)
}

perhaps it throws so could be captured do a do:catch?

do {
    try realm.write {
        realm.add(userWithDuplicateName)
    }
} catch let err as NSError {
    print(err.localizedDescription)
}

Someone can correct me if I’m wrong, but I believe that putting indexes on atlas schemas will only prevent the data from syncing, not from writing it to a local realm. So there is still a need to implement additional logic preventing local writes.

Hi @Michael_Macutkiewicz The synchronization of the (duplicate) write to the backend happens asynchronously, and so the write will succeed. When Realm Sync attempts to write the new document to Atlas, it will fail with a duplicate key error and the Object is removed from Realm (and that deletion is synced back to the mobile app(s)) :

In this example, I added a unique index on the body of the text messages (you can see that the addition is accepted and synced but a fraction of a second later, the duplicate key is detected and the chat message is removed:

1 Like

Ok. What about scenario where document is updated, not inserted? Will the whole document be deleted? Or will the previous value of the field be restored?

@Andrew_Morgan

By your image, there appears there’s a notification that goes along with this

I am asking as if the app attempts to add a user that has a username that already exists (per the original question) the new user should be notified of that event so using the observer the proper path?

What you’re seeing is that sync removes the new object when the Atlas translator realises that the unique index constraint has been broken. That then automatically updates the realm in the iOS app. Because the live Realm result set is bound to the UI, the message is removed from the UI automatically.

I realise that this isn’t a good fit for what you’re looking to do, but wanted to answer the query about unique indexes.

Duplicating a small amount of data into a partition that’s accessible to all is definitely an option – see the Chatster Object/document in this article. There’s still the risk that 2 new users pick the same username at about the same time.

A Realm function to set the username (combined with an Atlas unique index) should be a robust solution.

1 Like