Realm primary key migration - ios

I want to migrate my realm schema to a new version. Therefor the removal of my primary key is needed.
Old schema:
class StudyState : Object
{
dynamic var name = ""
dynamic var x = ""
dynamic var y = ""
override static func primaryKey() -> String? {
return "name"
}
}
New schema:
class StudyState : Object
{
dynamic var name = ""
dynamic var x = ""
dynamic var y = ""
}
Without migration, realm will fail with
'RLMException', reason: 'Migration is required for object type 'StudyState' due to the following errors:
- Property 'name' is no longer a primary key.'
I tried this migration block, which failed too:
migration.enumerate(StudyState.className()) { oldObject, newObject in
newObject?["deleted"] = false
newObject?["primaryKeyProperty"] = ""
}
'RLMException', reason: 'Invalid property name'
Is there a way to remove the primary key when migrating realm to a new schema version?

You do not need to do anything in migration block if you only remove the primary key annotation.
But there is a need to increase the schema version because schema definitions changed.
Like below:
// You have to migrate Realm BEFORE open Realm if you changed schema definitions
setSchemaVersion(1, Realm.defaultPath) { (migration, oldSchemaVersion) -> Void in
if oldSchemaVersion < 1 {
// Nothing to do!
// Realm will automatically detect new properties and removed properties
// And will update the schema on disk automatically
}
}
let realm = Realm()
...

Related

Is Realm smart when updating values or check for new values should be perfomed manually?

I wonder if I should create my own additional layer when updating Realm objects to avoid redundant database writing operations or is it done automatically on a lower level?
Let's take an example:
class SomeEntity: Object {
#Persisted(primaryKey: true) var id = 0
#Persisted var aaa: String?
#Persisted var bbb: Float?
#Persisted var ccc: Int?
}
when doing some batch update:
newDownloadedData.forEach { entry in
guard let id = entry["id"].int else {
return
}
try? localRealm.write {
let entity = existingLocalEntities.first { $0.id == id } ?? SomeEntity(id: id)
localRealm.add(entity, update: .modified) //this makes an 'upsertion' which is automatically an update or insert
if entity.aaa != entry["aaa"].string {
entity.aaa = movieInfo["aaa"].string
}
if entity.bbb != entry["bbb"].float {
entity.bbb = movieInfo["bbb"].float
}
if entity.ccc != entry["ccc"].int {
entity.ccc = movieInfo["ccc"].int
}
}
}
I wonder if these checks necessary or can I just go with:
entity.aaa = movieInfo["aaa"].string
entity.bbb = movieInfo["bbb"].float
entity.ccc = movieInfo["ccc"].int
and not worry that values will be updated and written even if downloaded values are the same as existing local ones?
Your observers will be notified if you update a property on a realm object with the same value. Realm does not care if you use a different value or not.
I'm not sure what your use case is, but it may be a pain in the butt to check every value manually.
You can do something like this though:
protocol UniqueUpdating { }
extension UniqueUpdating where Self: AnyObject {
#discardableResult
func update<Value: Equatable>(
_ keyPath: ReferenceWritableKeyPath<Self, Value>,
to value: Value
) -> Bool {
guard self[keyPath: keyPath] != value else { return false }
self[keyPath: keyPath] = value
return true
}
}
extension Object: UniqueUpdating {}
class Person: Object {
#Persisted(primaryKey: true) var id: Int = 0
#Persisted var name: String = ""
}
Usage would be like this:
let realm = try! Realm()
try! realm.write {
person.update(\.name, to: "BOB")
}
EDIT: things described below do not work like expected.
eg. if name = "Tom" and later the same value is assigned ( self.name = "Tom") it will be treated as modification.
It turns out that YES, Realm can be smart about it!
the clue sits in update parameter in .add function.
using .modified will result in smart data write.
Excerpt from documentation:
/**
What to do when an object being added to or created in a Realm has a primary key that already exists.
*/
#frozen public enum UpdatePolicy: Int {
/**
Throw an exception. This is the default when no policy is specified for `add()` or `create()`.
This behavior is the same as passing `update: false` to `add()` or `create()`.
*/
case error = 1
/**
Overwrite only properties in the existing object which are different from the new values. This results
in change notifications reporting only the properties which changed, and influences the sync merge logic.
If few or no of the properties are changing this will be faster than .all and reduce how much data has
to be written to the Realm file. If all of the properties are changing, it may be slower than .all (but
will never result in *more* data being written).
*/
case modified = 3
/**
Overwrite all properties in the existing object with the new values, even if they have not changed. This
results in change notifications reporting all properties as changed, and influences the sync merge logic.
This behavior is the same as passing `update: true` to `add()` or `create()`.
*/
case all = 2
}

Why do my linking objects not have a Realm?

In my iOS app, I get the following exception:
'Linking objects notifications are only supported on managed objects.'
when I try to add an observer block:
y.xxx.observe { ... }
to a property that is defined as such:
class Y: Object {
...
let xxx = LinkingObjects(fromType: X.self, property: "y")
...
}
I believe this means that y.xxx does not have a Realm, and indeed I can see in the debugger that y.xxx.realm is nil. However, y.realm is NOT nil.
How can the linking objects not have a Realm if the object I am linking to does have one?
For completeness, this is how Class X is defined:
class X: Object {
...
#Persisted var y: Y?
...
}
Realm version 10.11.0, RealmDatabase version 11.1.1.
Context: I am in the last phase of migrating an app that was originally written in ObjC to be purely in Swift. This means switching to the Swift version of Realm. I have not encountered this problem in the previous version of the app that is largely the same code base except that it uses a very old version of the Realm framework and the Realm objects are defined in ObjC.
You can add an observer to a linking objects property as long as the objects are managed. Let me set this up starting with the PersonClass who has a List property of DogClass objects
class PersonClass: Object {
#objc dynamic var _id = ObjectId.generate()
#objc dynamic var name = "Jay"
let dogList = List<DogClass>()
override static func primaryKey() -> String? {
return "_id"
}
}
and then our DogClass has an inverse relationship to the PersonClass objects
class DogClass: Object {
#objc dynamic var _id = ObjectId.generate()
#objc dynamic var name = ""
let linkingOwners = LinkingObjects(fromType: PersonClass.self, property: "dogList")
override static func primaryKey() -> String? {
return "_id"
}
}
Then suppose we want to observe Spot's linkingOwners - both Jay and Cindy had Spot added to their dogList previously
let spot = realm.objects(DogClass.self).filter("name == 'Spot'").first!
self.peopleToken = spot.linkingOwners.observe { changes in
switch changes {
case .initial(let dogs):
print(" spots owners have been loaded")
case .update(_, let deletions, let insertions, let modifications ):
print(" something changed in spots owners")
case .error(let error):
print(error.localizedDescription)
}
}
Running this section of code outputs this to console and adds the observer the linkingObjects
spots owners have been loaded
Then, lets make a change to one of the owners properties
let jay = realm.objects(PersonClass.self).filter("name == 'Jay'").first!
try! realm.write {
jay.name = "Jay, Spots owner"
}
that last piece of code will output this to the console
something changed in spots owners
The above code creates a PersonClass object and a DogClass object object with a inverse relationship. The code then adds an observer the linkingObjects (PersonClass) and fires when one of them changes.
Turns out the linking objects now have to be declared like so:
class Y: Object {
...
#Persisted(originProperty: "y") var xxx: LinkingObjects<X>
...
}
I am not sure if the declaration style I used in my question is still supposed to be valid and if this is a bug, but using the new style gets rid of the exception.

Changing primary key property name on Realm Swift object model

I have a Realm (Swift) object that's existed in my app for about a year now. The model for this object looks like this:
class ChatMessage: Object {
#objc dynamic var messageId: Int = generateId()
// other properties omitted
override static func primaryKey() -> String? {
return "messageId"
}
}
Now, I find myself needing to effectively rename the "messageId" property to "id" and make "id" the primary key. I need to do this because I need this object to conform to a particular protocol (from a framework that I don't control) and that protocol specifies that I need a "messageId" property of type String. Renaming seems to be the most straight forward explanation for what I'm trying to do but I realize that this might also be explained by saying that I want to:
create a new "id" property (of type Int)
copy the existing value of "messageId" to the "id" property (during migration)
change the primary key to use "id" instead of "messageId"
In effect, what I want the object to look like after the change is:
class ChatMessage: Object {
#objc dynamic var id: Int = generateId()
// other properties omitted
override static func primaryKey() -> String? {
return "id"
}
}
I have a migration block defined and I've tried a few different ways of accomplishing this but so far haven't had any luck. Without the migration block, I get the following in the Xcode debug console:
ERROR - Failed to open Realm! Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'ChatMessage.messageId' has been removed.
- Property 'ChatMessage.id' has been added.
- Primary Key for class 'ChatMessage' has changed from 'messageId' to 'id'.
For what it's worth, I'm using Realm 5.50 and I've read through the docs at https://realm.io/docs/swift/latest/ and I see this statement:
Once an object with a primary key is added to a Realm, the primary key cannot be changed.
That makes it sound like this isn't possible to do with Realm anymore but I'm wondering if anyone can confirm that or if there is, in fact, a way to achieve this? Thanks!
You can actually change the primary key within a migration block. Suppose you have a object that looks like this, with 'localID' being the primary key
class TestClass: Object {
#objc dynamic var localID = UUID().uuidString
override static func primaryKey() -> String? {
return "localID"
}
}
and we want to change the primary key to _id. The migration block would look something like this
let vers = UInt64(3)
let config = Realm.Configuration( schemaVersion: vers, migrationBlock: { migration, oldSchemaVersion in
print("oldSchemaVersion: \(oldSchemaVersion)")
if (oldSchemaVersion < vers) {
print(" performing migration")
migration.enumerateObjects(ofType: TestClass.className()) { oldItem, newItem in
let oldId = oldItem!["localID"]
newItem!["_id"] = oldId
}
}
})
I would suggest using _id instead of id so it will be compatible with MongoDB Realm in the future. See Define a Realm Schema
One day someone will come looking for this in flutter:
The simple answer is to use the annotation #MapTo("_id")
class _Car {
#PrimaryKey()
#MapTo("_id")
late ObjectId id;
...
}

Realm Migration Error: 'RLMException': 'Property 'RecipeStep.recipeId' does not exist'

I'm running into issues during a Realm migration. The error messages says that the property doesn't exist, but this is what my new object looks like:
class RecipeStep: Object {
#objc dynamic var recipeId: Int = 0
let stepNumber = RealmOptional<Int>()
#objc dynamic var stepText: String? = nil
}
And here's what the old object schema looks like:
class RecipeStep: Object {
let recipeId = RealmOptional<Int>()
let stepNumber = RealmOptional<Int>()
#objc dynamic var stepText: String? = nil
}
As you can see, the single change is the type of the recipeId: converting RealmOptional to an Int. And here's the migration block I'm using to do it:
migration.enumerateObjects(ofType: RecipeStep.className()) { oldObject, newObject in
if let recipeStepRecipeId = oldObject?["recipeId"] as? Int {
newObject?["recipeId"] = recipeStepRecipeId
}
}
What am I doing wrong?
This turns out to have been a bug in an older version of Realm (v5.4.5). I updated to version 10.0.0 and this issue was resolved. The migration successfully completes. I changed nothing in my code.
I'm on Xcode 12, building for iOS 13.
Found discussion on a similar sounding bug on this SO thread.

Realm: Can't update a object if one of its properties is another object that already exists

I have this schema:
class A: Object {
var idA = ""
var b: B?
override class func primaryKey() -> String {
return "idA"
}
}
class B: Object {
var idB = ""
var name = ""
override class func primaryKey() -> String {
return "idB"
}
}
So if i want to save an A object:
func updateA(a: A, b: B) {
do {
try realm.write {
a.b = b //Here i get the excepcion
realm.add(a, update: true)
}
} catch { error
}
}
When i call update i can't assign b to a.b, i get:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Can't create object with existing primary key value "bKey".'
If the b object already exists in the database with that primary key i get that, but if its a new object it works. Im pretty sure long time ago this worked as expected
Notice i want to save A updating the B object if one of its properties has changed, like name. And i dont wanto to create another B object, just use the b object that already passed.
It's as expected isnt it, you are adding the b that already created to create a similar one, that's not possible since you are already having that b with that primary key already
What you should do is just create a with another new b, not create b first then assign to a
If an Object doesn't already belong to a Realm, setting it as a child object will also try and add it to the Realm. In this case, since this appears to be a copy of b and not a direct reference to the record in the database, it's being treated as a new record with a conflicting primary key attempting to be added.
If you already have a copy of b in the database, you can grab an explicit reference to that one by using realm.object(ofType:forPrimaryKey:):
func updateA(a: A, b: B) {
do {
let bInDatabase = realm.object(ofType: B.self, forPrimaryKey: b.idB)
try realm.write {
a.b = bInDatabase
realm.add(a, update: true)
}
} catch { error }
}

Resources