更改对象模型 - Swift SDK
Overview
更新对象模式时,必须递增模式版本并执行迁移。
如果模式更新操作添加了可选属性或删除了属性,则 Realm 可自动执行迁移。您只需递增 schemaVersion
。
对于更复杂的架构更新,您还必须在 migrationBlock
中手动指定迁移逻辑。该操作可能包括如下更改:
添加必须使用默认值填充的必要属性
合并字段
重命名字段
更改字段的类型
从对象转换为嵌入式对象
提示
在开发期间绕过迁移
在开发或调试应用程序时,您可能更想删除而不是迁移域。因模式不匹配而需要迁移时,请使用 deleteRealmIfMigrationNeeded 标志自动删除数据库。
切勿将此标记设为 true
的应用发布到生产环境。
模式版本
模式版本可标识 Realm 模式在某个时间点的状态。Realm 会跟踪每个 Realm 的模式版本,并使用该信息将每个 Realm 中的对象映射到正确的模式。
模式版本是指打开 Realm 时可包含在 Realm 配置中的整数。如果客户端应用程序在打开 Realm 时未指定版本号,则该 Realm 默认采用 0
版本。
重要
单调递增版本
迁移必须将 Realm 更新到更高的模式版本。如果客户端应用程序打开的 Realm 的模式版本低于该 Realm 的当前版本,或是指定的模式版本与该 Realm 的当前版本相同但包含不同对象模式,Realm 则会引发错误。
迁移
本地迁移是针对不会自动与另一个 Realm同步的 Realm 的迁移。 本地迁移可以访问现有的 Realm 模式、版本和对象,并定义以增量方式将 Realm 更新到新模式版本的逻辑。 要执行本地迁移,您必须指定高于当前版本的新模式版本,并在打开过时 Realm 时提供迁移函数。
在 iOS 中,您可以使用手动迁移来更新底层数据,从而反映模式更改。此类手动迁移期间,当您在模式中添加或删除属性时可定义新属性和已删除属性。
自动更新模式
添加属性
Realm 可自动迁移添加的属性,但您在执行这些更改时必须指定更新后的模式版本。
注意
Realm 不会自动为新的必需属性设置值。您必须使用迁移区块为新的必需属性设置默认值。对于新的可选属性,现有记录可具有 null 值。这表示在添加可选属性时无需迁移区块。
例子
使用 1
模式版本的 Realm 具有 Person
对象类型,而该对象类型具有名字、姓氏和年龄属性:
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. @interface Person : RLMObject @property NSString *firstName; @property NSString *lastName; @property int age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"firstName", @"lastName", @"age"]; } @end
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. class Person: Object { var firstName = "" var lastName = "" var age = 0 }
开发者将确定 Person
类需要 email
字段并更新模式。
// In a new version, you add a property // on the Person model. @interface Person : RLMObject @property NSString *firstName; @property NSString *lastName; // Add a new "email" property. @property NSString *email; // New properties can be migrated // automatically, but must update the schema version. @property int age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"firstName", @"lastName", @"email", @"age"]; } @end
// In a new version, you add a property // on the Person model. class Person: Object { var firstName = "" var lastName = "" // Add a new "email" property. var email: String? // New properties can be migrated // automatically, but must update the schema version. var age = 0 }
Realm 会自动迁移该 Realm 以符合更新后的 Person
模式。但开发者必须将 Realm 的模式版本设为 2
。
// When you open the realm, specify that the schema // is now using a newer version. RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; // Set the new schema version config.schemaVersion = 2; // Use this configuration when opening realms [RLMRealmConfiguration setDefaultConfiguration:config]; RLMRealm *realm = [RLMRealm defaultRealm];
// When you open the realm, specify that the schema // is now using a newer version. let config = Realm.Configuration( schemaVersion: 2) // Use this configuration when opening realms Realm.Configuration.defaultConfiguration = config let realm = try! Realm()
删除属性
要从架构中删除一个属性,请从对象的类中删除该属性,并设置 Realm 配置对象的 schemaVersion
。删除属性不会影响现有的对象。
例子
使用 1
模式版本的 Realm 具有 Person
对象类型,而该对象类型具有名字、姓氏和年龄属性:
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. @interface Person : RLMObject @property NSString *firstName; @property NSString *lastName; @property int age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"firstName", @"lastName", @"age"]; } @end
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. class Person: Object { var firstName = "" var lastName = "" var age = 0 }
开发者确定 Person
不需要 age
字段并更新架构。
// In a new version, you remove a property // on the Person model. @interface Person : RLMObject @property NSString *firstName; @property NSString *lastName; // Remove the "age" property. // @property int age; // Removed properties can be migrated // automatically, but must update the schema version. @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"firstName", @"lastName"]; } @end
// In a new version, you remove a property // on the Person model. class Person: Object { var firstName = "" var lastName = "" // Remove the "age" property. // @Persisted var age = 0 // Removed properties can be migrated // automatically, but must update the schema version. }
Realm 会自动迁移该 Realm 以符合更新后的 Person
模式。但开发者必须将 Realm 的模式版本设为 2
。
// When you open the realm, specify that the schema // is now using a newer version. RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; // Set the new schema version config.schemaVersion = 2; // Use this configuration when opening realms [RLMRealmConfiguration setDefaultConfiguration:config]; RLMRealm *realm = [RLMRealm defaultRealm];
// When you open the realm, specify that the schema // is now using a newer version. let config = Realm.Configuration( schemaVersion: 2) // Use this configuration when opening realms Realm.Configuration.defaultConfiguration = config let realm = try! Realm()
提示
SwiftUI 开发者在添加或删除属性时可能会看到错误“需进行迁移”。此问题与 SwiftUI 中的生命周期有关。对视图进行布局,然后 .environment
修饰符会设置该配置。
要解决这些情况下出现的迁移错误,请将 Realm.Configuration(schemaVersion: <Your Incremented Version>)
传递给 ObservedResults
构造函数。
手动迁移模式
对于更复杂的模式更新,Realm 需将给定对象的旧实例手动迁移到新模式。
为属性重命名
要在迁移期间重命名属性,请使用Migration.renameProperty(onType:from:to:) 方法。
Realm 会在重命名操作期间应用所有新的为空性 (nullability) 或索引设置。
例子
在迁移块中将age
重命名为yearsSinceBirth
。
RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; config.schemaVersion = 2; config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) { if (oldSchemaVersion < 2) { // Rename the "age" property to "yearsSinceBirth". // The renaming operation should be done outside of calls to `enumerateObjects(ofType: _:)`. [migration renamePropertyForClass:[Person className] oldName:@"age" newName:@"yearsSinceBirth"]; } };
let config = Realm.Configuration( schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion < 2 { // Rename the "age" property to "yearsSinceBirth". // The renaming operation should be done outside of calls to `enumerateObjects(ofType: _:)`. migration.renameProperty(onType: Person.className(), from: "age", to: "yearsSinceBirth") } })
修改属性
提示
如果需要迁移,您可以使用 deleteRealmIfMigrationNeeded 方法删除该域。在需要快速迭代且不想执行迁移时,此功能在开发期间非常有用。
要定义自定义迁移逻辑,请在 打开 Realm 时设置 配置 的 迁移区块 属性。
迁移块会收到一个迁移对象,而您可使用它来执行迁移。您可以使用迁移对象的 enumerateObjects(ofType:_:) 方法来迭代和更新域中给定 Realm 类型的所有实例。
例子
使用 1
模式版本的 Realm 具有 Person
对象类型,而该对象类型具有针对名字和姓氏的单独字段:
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. @interface Person : RLMObject @property NSString *firstName; @property NSString *lastName; @property int age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"firstName", @"lastName", @"age"]; } @end
// In the first version of the app, the Person model // has separate fields for first and last names, // and an age property. class Person: Object { var firstName = "" var lastName = "" var age = 0 }
开发者将确定 Person
类应使用 fullName
组合字段而不是单独的 firstName
和 lastName
字段,并更新模式。
为了迁移 Realm 以符合更新的 Person
架构,开发者将 Realm 的架构版本设置为 2
,并定义一个迁移函数以根据现有的 firstName
和 lastName
属性设置 fullName
值。
// In version 2, the Person model has one // combined field for the full name and age as a Int. // A manual migration will be required to convert from // version 1 to this version. @interface Person : RLMObject @property NSString *fullName; @property int age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"fullName", @"age"]; } @end
RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; // Set the new schema version config.schemaVersion = 2; config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) { if (oldSchemaVersion < 2) { // Iterate over every 'Person' object stored in the Realm file to // apply the migration [migration enumerateObjects:[Person className] block:^(RLMObject * _Nullable oldObject, RLMObject * _Nullable newObject) { // Combine name fields into a single field newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@", oldObject[@"firstName"], oldObject[@"lastName"]]; }]; } }; // Tell Realm to use this new configuration object for the default Realm [RLMRealmConfiguration setDefaultConfiguration:config]; // Now that we've told Realm how to handle the schema change, opening the realm // will automatically perform the migration RLMRealm *realm = [RLMRealm defaultRealm];
// In version 2, the Person model has one // combined field for the full name and age as a Int. // A manual migration will be required to convert from // version 1 to this version. class Person: Object { var fullName = "" var age = 0 }
// In application(_:didFinishLaunchingWithOptions:) let config = Realm.Configuration( schemaVersion: 2, // Set the new schema version. migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion < 2 { // The enumerateObjects(ofType:_:) method iterates over // every Person object stored in the Realm file to apply the migration migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in // combine name fields into a single field let firstName = oldObject!["firstName"] as? String let lastName = oldObject!["lastName"] as? String newObject!["fullName"] = "\(firstName!) \(lastName!)" } } } ) // Tell Realm to use this new configuration object for the default Realm Realm.Configuration.defaultConfiguration = config // Now that we've told Realm how to handle the schema change, opening the file // will automatically perform the migration let realm = try! Realm()
随后,开发者将确定 age
字段的类型应为 String
而不是 Int
,并更新模式。
为迁移 Realm 以符合更新后的 Person
模式,开发者会将此 Realm 的模式版本设为 3
,并向迁移函数添加一个条件,以便该函数定义如何从任一先前版本迁移到新版本。
// In version 3, the Person model has one // combined field for the full name and age as a String. // A manual migration will be required to convert from // version 2 to this version. @interface Person : RLMObject @property NSString *fullName; @property NSString *age; @end @implementation Person + (NSArray<NSString *> *)requiredProperties { return @[@"fullName", @"age"]; } @end
RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; // Set the new schema version config.schemaVersion = 3; config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) { if (oldSchemaVersion < 2) { // Previous Migration. [migration enumerateObjects:[Person className] block:^(RLMObject * _Nullable oldObject, RLMObject * _Nullable newObject) { newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@", oldObject[@"firstName"], oldObject[@"lastName"]]; }]; } if (oldSchemaVersion < 3) { // New Migration [migration enumerateObjects:[Person className] block:^(RLMObject * _Nullable oldObject, RLMObject * _Nullable newObject) { // Make age a String instead of an Int newObject[@"age"] = [oldObject[@"age"] stringValue]; }]; } }; // Tell Realm to use this new configuration object for the default Realm [RLMRealmConfiguration setDefaultConfiguration:config]; // Now that we've told Realm how to handle the schema change, opening the realm // will automatically perform the migration RLMRealm *realm = [RLMRealm defaultRealm];
// In version 3, the Person model has one // combined field for the full name and age as a String. // A manual migration will be required to convert from // version 2 to this version. class Person: Object { var fullName = "" var age = "0" }
// In application(_:didFinishLaunchingWithOptions:) let config = Realm.Configuration( schemaVersion: 3, // Set the new schema version. migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion < 2 { // Previous Migration. migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in let firstName = oldObject!["firstName"] as? String let lastName = oldObject!["lastName"] as? String newObject!["fullName"] = "\(firstName!) \(lastName!)" } } if oldSchemaVersion < 3 { // New Migration. migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in // Make age a String instead of an Int newObject!["age"] = "\(oldObject!["age"] ?? 0)" } } } ) // Tell Realm to use this new configuration object for the default Realm Realm.Configuration.defaultConfiguration = config // Now that we've told Realm how to handle the schema change, opening the file // will automatically perform the migration let realm = try! Realm()
提示
线性迁移
避免在迁移区块中进行嵌套或以其他方式跳过 if (oldSchemaVersion < X)
语句。此举可确保无论客户端从哪个模式版本启动,所有更新操作均可按正确顺序进行应用。其目标是定义迁移逻辑,而该逻辑可将所有已过时模式版本中的数据进行转换,从而匹配当前模式。
从对象转换为嵌入式对象
嵌入式对象无法独立于父对象而存在。将某一对象更改为嵌入式对象时,迁移区必须确保每个嵌入式对象均有一个指向父对象的反向链接。缺少反向链接或有多个反向链接会导致以下异常:
At least one object does not have a backlink (data would get lost).
At least one object does have multiple backlinks.