From aa8c1fd0c4b1c057f3506d705f7957e0c0a2670c Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 5 Dec 2018 13:01:09 -0800 Subject: [PATCH] Add an option to only set the properties which actually change --- CHANGELOG.md | 8 +- Realm/ObjectStore | 2 +- Realm/RLMAccessor.mm | 18 +- Realm/RLMObject.h | 99 ++++++++- Realm/RLMObject.mm | 22 +- Realm/RLMObjectStore.h | 13 +- Realm/RLMObjectStore.mm | 21 +- Realm/RLMRealm.mm | 6 +- Realm/Tests/DynamicTests.m | 10 +- Realm/Tests/NotificationTests.m | 65 +++++- RealmSwift/Realm.swift | 207 ++++++++++++++++-- RealmSwift/Sync.swift | 2 +- RealmSwift/Tests/ObjectCreationTests.swift | 233 ++++++++++++++++++++- RealmSwift/Tests/RealmTests.swift | 10 +- 14 files changed, 644 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eadacea04..b8a94eaf23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements -* None. +* Add an option to only set the properties which have values different from the + existing ones when updating an existing object with + `Realm.create()`/`-[RLMObject createOrUpdateInRealm:withValue:]`. This makes + notifications report only the properties which have actually changed, and + improves Object Server performance by reducing the number of operations to + merge. (Issue: [#5970](https://github.com/realm/realm-cocoa/issues/5970), + PR: [#6149](https://github.com/realm/realm-cocoa/pulls/6149)). ### Fixed * ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?) diff --git a/Realm/ObjectStore b/Realm/ObjectStore index 36c41e2137..d5a4534b46 160000 --- a/Realm/ObjectStore +++ b/Realm/ObjectStore @@ -1 +1 @@ -Subproject commit 36c41e213789af016f3fef569ec0b4c614d5b6ca +Subproject commit d5a4534b4648d503552d6550fad55c7271874d93 diff --git a/Realm/RLMAccessor.mm b/Realm/RLMAccessor.mm index 818f85756f..3546605de7 100644 --- a/Realm/RLMAccessor.mm +++ b/Realm/RLMAccessor.mm @@ -114,7 +114,7 @@ void setValue(__unsafe_unretained RLMObjectBase *const obj, NSUInteger colIndex, return; } - RLMAddObjectToRealm(val, obj->_realm, false); + RLMAddObjectToRealm(val, obj->_realm, RLMUpdatePolicyError); // make sure it is the correct type if (val->_row.get_table() != obj->_row.get_table()->get_link_target(colIndex)) { @@ -724,12 +724,16 @@ static auto to_optional(__unsafe_unretained id const value, Fn&& fn) { } template<> -realm::RowExpr RLMAccessorContext::unbox(__unsafe_unretained id const v, bool create, bool update, bool, size_t) { +realm::RowExpr RLMAccessorContext::unbox(__unsafe_unretained id const v, bool create, bool update, bool diff, size_t) { + RLMUpdatePolicy policy = !update ? RLMUpdatePolicyError + : !diff ? RLMUpdatePolicyUpdateAll + : RLMUpdatePolicyUpdateChanged; + RLMObjectBase *link = RLMDynamicCast(v); if (!link) { if (!create) return realm::RowExpr(); - return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, v, update)->_row; + return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, v, policy)->_row; } if (link.isInvalidated) { @@ -743,7 +747,7 @@ static auto to_optional(__unsafe_unretained id const value, Fn&& fn) { if (![link->_objectSchema.className isEqualToString:_info.rlmObjectSchema.className]) { if (create && !_promote_existing) - return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, link, update)->_row; + return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, link, policy)->_row; return link->_row; } @@ -751,13 +755,13 @@ static auto to_optional(__unsafe_unretained id const value, Fn&& fn) { if (!create) return realm::RowExpr(); if (!_promote_existing) - return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, link, update)->_row; - RLMAddObjectToRealm(link, _realm, update); + return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, link, policy)->_row; + RLMAddObjectToRealm(link, _realm, policy); } else if (link->_realm != _realm) { if (_promote_existing) @throw RLMException(@"Object is already managed by another Realm. Use create instead to copy it into this Realm."); - return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, v, update)->_row; + return RLMCreateObjectInRealmWithValue(_realm, _info.rlmObjectSchema.className, v, policy)->_row; } return link->_row; } diff --git a/Realm/RLMObject.h b/Realm/RLMObject.h index 2b55a3e9e1..6a49eec169 100644 --- a/Realm/RLMObject.h +++ b/Realm/RLMObject.h @@ -194,10 +194,13 @@ NS_ASSUME_NONNULL_BEGIN argument's type is the same as the receiver, and the objects have identical values for their managed properties, this method does nothing. - If the object is being updated, all properties defined in its schema will be set by copying from + If the object is being updated, each property defined in its schema will be set by copying from `value` using key-value coding. If the `value` argument does not respond to `valueForKey:` for a given property name (or getter name, if defined), that value will remain untouched. Nullable properties on the object can be set to nil by using `NSNull` as the updated value. + Each property is set even if the existing value is the same as the new value being set, and + notifications will report them all being changed. See `createOrUpdateChangedInDefaultRealmWithValue:` + for a version of this function which only sets the values which have changed. If the `value` argument is an array, all properties must be present, valid and in the same order as the properties defined in the model. @@ -208,6 +211,50 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)createOrUpdateInDefaultRealmWithValue:(id)value; +/** + Creates or updates a Realm object within the default Realm. + + This method may only be called on Realm object types with a primary key defined. If there is already + an object with the same primary key value in the default Realm, its values are updated and the object + is returned. Otherwise, this method creates and populates a new instance of the object in the default Realm. + + If nested objects are included in the argument, `createOrUpdateChangedInDefaultRealmWithValue:` will be + recursively called on them if they have primary keys, `createInDefaultRealmWithValue:` if they do not. + + The `value` argument is used to populate the object. It can be a Realm object, a key-value coding + compliant object, an array or dictionary returned from the methods in `NSJSONSerialization`, or an + array containing one element for each managed property. + + If the object is being created, an exception will be thrown if any required properties + are not present and those properties were not defined with default values. + + If the `value` argument is a Realm object already managed by the default Realm, the + argument's type is the same as the receiver, and the objects have identical values for + their managed properties, this method does nothing. + + If the object is being updated, each property defined in its schema will be set by copying from + `value` using key-value coding. If the `value` argument does not respond to `valueForKey:` for a + given property name (or getter name, if defined), that value will remain untouched. + Nullable properties on the object can be set to nil by using `NSNull` as the updated value. + Unlike `createOrUpdateInDefaultRealmWithValue:`, only properties which have changed in value are + set, and any change notifications produced by this call will report only which properies have + actually changed. + + Checking which properties have changed imposes a small amount of overhead, and so this method + may be slower when all or nearly all of the properties being set have changed. If most or all + of the properties being set have not changed, this method will be much faster than unconditionally + setting all of them, and will also reduce how much data has to be written to the Realm, saving + both i/o time and disk space. + + If the `value` argument is an array, all properties must be present, valid and in the same + order as the properties defined in the model. + + @param value The value used to populate the object. + + @see `defaultPropertyValues`, `primaryKey` + */ ++ (instancetype)createOrUpdateChangedInDefaultRealmWithValue:(id)value; + /** Creates or updates an Realm object within a specified Realm. @@ -229,10 +276,13 @@ NS_ASSUME_NONNULL_BEGIN argument's type is the same as the receiver, and the objects have identical values for their managed properties, this method does nothing. - If the object is being updated, all properties defined in its schema will be set by copying from + If the object is being updated, each property defined in its schema will be set by copying from `value` using key-value coding. If the `value` argument does not respond to `valueForKey:` for a given property name (or getter name, if defined), that value will remain untouched. Nullable properties on the object can be set to nil by using `NSNull` as the updated value. + Each property is set even if the existing value is the same as the new value being set, and + notifications will report them all being changed. See `createOrUpdateChangedInRealm:withValue:` + for a version of this function which only sets the values which have changed. If the `value` argument is an array, all properties must be present, valid and in the same order as the properties defined in the model. @@ -244,6 +294,51 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)createOrUpdateInRealm:(RLMRealm *)realm withValue:(id)value; +/** + Creates or updates an Realm object within a specified Realm. + + This method may only be called on Realm object types with a primary key defined. If there is already + an object with the same primary key value in the given Realm, its values are updated and the object + is returned. Otherwise this method creates and populates a new instance of this object in the given Realm. + + If nested objects are included in the argument, `createOrUpdateInRealm:withValue:` will be + recursively called on them if they have primary keys, `createInRealm:withValue:` if they do not. + + The `value` argument is used to populate the object. It can be a Realm object, a key-value coding + compliant object, an array or dictionary returned from the methods in `NSJSONSerialization`, or an + array containing one element for each managed property. + + If the object is being created, an exception will be thrown if any required properties + are not present and those properties were not defined with default values. + + If the `value` argument is a Realm object already managed by the given Realm, the + argument's type is the same as the receiver, and the objects have identical values for + their managed properties, this method does nothing. + + If the object is being updated, each property defined in its schema will be set by copying from + `value` using key-value coding. If the `value` argument does not respond to `valueForKey:` for a + given property name (or getter name, if defined), that value will remain untouched. + Nullable properties on the object can be set to nil by using `NSNull` as the updated value. + Unlike `createOrUpdateInRealm:withValue:`, only properties which have changed in value are + set, and any change notifications produced by this call will report only which properies have + actually changed. + + Checking which properties have changed imposes a small amount of overhead, and so this method + may be slower when all or nearly all of the properties being set have changed. If most or all + of the properties being set have not changed, this method will be much faster than unconditionally + setting all of them, and will also reduce how much data has to be written to the Realm, saving + both i/o time and disk space. + + If the `value` argument is an array, all properties must be present, valid and in the same + order as the properties defined in the model. + + @param realm The Realm which should own the object. + @param value The value used to populate the object. + + @see `defaultPropertyValues`, `primaryKey` + */ ++ (instancetype)createOrUpdateChangedInRealm:(RLMRealm *)realm withValue:(id)value; + #pragma mark - Properties /** diff --git a/Realm/RLMObject.mm b/Realm/RLMObject.mm index 1ace411f46..eef3e8711e 100644 --- a/Realm/RLMObject.mm +++ b/Realm/RLMObject.mm @@ -72,25 +72,29 @@ - (instancetype)initWithValue:(id)value { #pragma mark - Class-based Object Creation + (instancetype)createInDefaultRealmWithValue:(id)value { - return (RLMObject *)RLMCreateObjectInRealmWithValue([RLMRealm defaultRealm], [self className], value, false); + return (RLMObject *)RLMCreateObjectInRealmWithValue([RLMRealm defaultRealm], [self className], value, RLMUpdatePolicyError); } + (instancetype)createInRealm:(RLMRealm *)realm withValue:(id)value { - return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, false); + return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyError); } + (instancetype)createOrUpdateInDefaultRealmWithValue:(id)value { return [self createOrUpdateInRealm:[RLMRealm defaultRealm] withValue:value]; } ++ (instancetype)createOrUpdateChangedInDefaultRealmWithValue:(id)value { + return [self createOrUpdateChangedInRealm:[RLMRealm defaultRealm] withValue:value]; +} + + (instancetype)createOrUpdateInRealm:(RLMRealm *)realm withValue:(id)value { - // verify primary key - RLMObjectSchema *schema = [self sharedSchema]; - if (!schema.primaryKeyProperty) { - NSString *reason = [NSString stringWithFormat:@"'%@' does not have a primary key and can not be updated", schema.className]; - @throw [NSException exceptionWithName:@"RLMExecption" reason:reason userInfo:nil]; - } - return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, true); + RLMVerifyHasPrimaryKey(self); + return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyUpdateAll); +} + ++ (instancetype)createOrUpdateChangedInRealm:(RLMRealm *)realm withValue:(id)value { + RLMVerifyHasPrimaryKey(self); + return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyUpdateChanged); } #pragma mark - Subscripting diff --git a/Realm/RLMObjectStore.h b/Realm/RLMObjectStore.h index b44e7e68c4..a21c0098ab 100644 --- a/Realm/RLMObjectStore.h +++ b/Realm/RLMObjectStore.h @@ -24,8 +24,16 @@ extern "C" { @class RLMRealm, RLMSchema, RLMObjectBase, RLMResults, RLMProperty; +typedef NS_ENUM(NSUInteger, RLMUpdatePolicy) { + RLMUpdatePolicyError = 0, + RLMUpdatePolicyUpdateChanged = 1, + RLMUpdatePolicyUpdateAll = 2, +}; + NS_ASSUME_NONNULL_BEGIN +void RLMVerifyHasPrimaryKey(Class cls); + // // Accessor Creation // @@ -39,7 +47,7 @@ void RLMRealmCreateAccessors(RLMSchema *schema); // // add an object to the given realm -void RLMAddObjectToRealm(RLMObjectBase *object, RLMRealm *realm, bool createOrUpdate); +void RLMAddObjectToRealm(RLMObjectBase *object, RLMRealm *realm, RLMUpdatePolicy); // delete an object from its realm void RLMDeleteObjectFromRealm(RLMObjectBase *object, RLMRealm *realm); @@ -56,10 +64,9 @@ id _Nullable RLMGetObject(RLMRealm *realm, NSString *objectClassName, id _Nullab // create object from array or dictionary RLMObjectBase *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, - id _Nullable value, bool createOrUpdate) + id _Nullable value, RLMUpdatePolicy updatePolicy) NS_RETURNS_RETAINED; - // // Accessor Creation // diff --git a/Realm/RLMObjectStore.mm b/Realm/RLMObjectStore.mm index 232a29be81..a642b334ed 100644 --- a/Realm/RLMObjectStore.mm +++ b/Realm/RLMObjectStore.mm @@ -112,9 +112,17 @@ void RLMInitializeSwiftAccessorGenerics(__unsafe_unretained RLMObjectBase *const } } +void RLMVerifyHasPrimaryKey(Class cls) { + RLMObjectSchema *schema = [cls sharedSchema]; + if (!schema.primaryKeyProperty) { + NSString *reason = [NSString stringWithFormat:@"'%@' does not have a primary key and can not be updated", schema.className]; + @throw [NSException exceptionWithName:@"RLMExecption" reason:reason userInfo:nil]; + } +} + void RLMAddObjectToRealm(__unsafe_unretained RLMObjectBase *const object, __unsafe_unretained RLMRealm *const realm, - bool createOrUpdate) { + RLMUpdatePolicy updatePolicy) { RLMVerifyInWriteTransaction(realm); // verify that object is unmanaged @@ -140,7 +148,9 @@ void RLMAddObjectToRealm(__unsafe_unretained RLMObjectBase *const object, object->_objectSchema = info.rlmObjectSchema; try { realm::Object::create(c, realm->_realm, *info.objectSchema, (id)object, - createOrUpdate, /* diff */ false, -1, &object->_row); + updatePolicy != RLMUpdatePolicyError, + updatePolicy == RLMUpdatePolicyUpdateChanged, + -1, &object->_row); } catch (std::exception const& e) { @throw RLMException(e); @@ -150,10 +160,10 @@ void RLMAddObjectToRealm(__unsafe_unretained RLMObjectBase *const object, } RLMObjectBase *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, - id value, bool createOrUpdate = false) { + id value, RLMUpdatePolicy updatePolicy) { RLMVerifyInWriteTransaction(realm); - if (createOrUpdate && RLMIsObjectSubclass([value class])) { + if (updatePolicy != RLMUpdatePolicyError && RLMIsObjectSubclass([value class])) { RLMObjectBase *obj = value; if (obj->_realm == realm && [obj->_objectSchema.className isEqualToString:className]) { // This is a no-op if value is an RLMObject of the same type already backed by the target realm. @@ -176,7 +186,8 @@ void RLMAddObjectToRealm(__unsafe_unretained RLMObjectBase *const object, RLMObjectBase *object = RLMCreateManagedAccessor(info.rlmObjectSchema.accessorClass, realm, &info); try { object->_row = realm::Object::create(c, realm->_realm, *info.objectSchema, - (id)value, createOrUpdate).row(); + (id)value, updatePolicy != RLMUpdatePolicyError, + updatePolicy == RLMUpdatePolicyUpdateChanged).row(); } catch (std::exception const& e) { @throw RLMException(e); diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 8521039cda..f86f8caa5e 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -712,7 +712,7 @@ - (BOOL)refresh { } - (void)addObject:(__unsafe_unretained RLMObject *const)object { - RLMAddObjectToRealm(object, self, false); + RLMAddObjectToRealm(object, self, RLMUpdatePolicyError); } - (void)addObjects:(id)objects { @@ -731,7 +731,7 @@ - (void)addOrUpdateObject:(RLMObject *)object { @throw RLMException(@"'%@' does not have a primary key and can not be updated", object.objectSchema.className); } - RLMAddObjectToRealm(object, self, true); + RLMAddObjectToRealm(object, self, RLMUpdatePolicyUpdateAll); } - (void)addOrUpdateObjects:(id)objects { @@ -837,7 +837,7 @@ + (BOOL)performMigrationForConfiguration:(RLMRealmConfiguration *)configuration } - (RLMObject *)createObject:(NSString *)className withValue:(id)value { - return (RLMObject *)RLMCreateObjectInRealmWithValue(self, className, value, false); + return (RLMObject *)RLMCreateObjectInRealmWithValue(self, className, value, RLMUpdatePolicyError); } - (BOOL)writeCopyToURL:(NSURL *)fileURL encryptionKey:(NSData *)key error:(NSError **)error { diff --git a/Realm/Tests/DynamicTests.m b/Realm/Tests/DynamicTests.m index 9c638eef38..843d9dc617 100644 --- a/Realm/Tests/DynamicTests.m +++ b/Realm/Tests/DynamicTests.m @@ -78,10 +78,8 @@ - (void)testDynamicObjectRetrieval { - (void)testDynamicSchemaMatchesRegularSchema { RLMSchema *expectedSchema = nil; - // Force create and close realm @autoreleasepool { - RLMRealm *realm = self.realmWithTestPath; - expectedSchema = realm.schema; + expectedSchema = self.realmWithTestPath.schema; } XCTAssertNotNil(expectedSchema); @@ -98,6 +96,12 @@ - (void)testDynamicSchemaMatchesRegularSchema { // Class overrides names, so the dynamic schema for it shoudn't match continue; } + if (expectedObjectSchema.primaryKeyProperty.index != 0) { + // The dynamic schema will always put the primary key first, so it + // won't match if the static schema doesn't have it there + continue; + } + RLMObjectSchema *dynamicObjectSchema = dynamicSchema[expectedObjectSchema.className]; XCTAssertEqual(dynamicObjectSchema.properties.count, expectedObjectSchema.properties.count); for (NSUInteger propertyIndex = 0; propertyIndex < expectedObjectSchema.properties.count; propertyIndex++) { diff --git a/Realm/Tests/NotificationTests.m b/Realm/Tests/NotificationTests.m index 05e62ff862..fdbbc7290b 100644 --- a/Realm/Tests/NotificationTests.m +++ b/Realm/Tests/NotificationTests.m @@ -867,7 +867,25 @@ - (void)testModifyParent { @end -// clang things the tests below have retain cycles because `_obj` could retain +@interface AllTypesWithPrimaryKey : RLMObject +@property BOOL boolCol; +@property int intCol; +@property float floatCol; +@property double doubleCol; +@property NSString *stringCol; +@property NSData *binaryCol; +@property NSDate *dateCol; +@property bool cBoolCol; +@property int64_t longCol; +@property StringObject *objectCol; + +@property (nonatomic) int pk; +@end +@implementation AllTypesWithPrimaryKey ++ (NSString *)primaryKey { return @"pk"; } +@end + +// clang thinks the tests below have retain cycles because `_obj` could retain // the block passed to addNotificationBlock (but it doesn't) #pragma clang diagnostic ignored "-Warc-retain-cycles" @@ -1087,4 +1105,49 @@ - (void)testArrayPropertiesMerelyReportModification { [token invalidate]; } +- (void)testDiffedUpdatesOnlyNotifyForPropertiesWhichActuallyChanged { + NSMutableArray *values = [_initialValues mutableCopy]; + [values addObject:@1]; + + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + [realm addObject:_values.lastObject]; + AllTypesWithPrimaryKey *obj = [AllTypesWithPrimaryKey createInRealm:realm withValue:values]; + [realm commitWriteTransaction]; + + __block NSUInteger i = 0; + __block XCTestExpectation *expectation = nil; + RLMNotificationToken *token = [obj addNotificationBlock:^(BOOL deleted, NSArray *changes, NSError *error) { + XCTAssertFalse(deleted); + XCTAssertNil(error); + XCTAssertEqual(changes.count, 1U); + RLMPropertyChange *prop = changes[0]; + XCTAssertEqualObjects(prop.name, _propertyNames[i]); + XCTAssertNil(prop.previousValue); + if ([prop.name isEqualToString:@"objectCol"]) { + XCTAssertTrue([prop.value isEqualToObject:_values[i]], + @"%d: %@ %@", (int)i, prop.value, _values[i]); + } + else { + XCTAssertEqualObjects(prop.value, _values[i]); + } + + [expectation fulfill]; + }]; + + + for (i = 0; i < _values.count; ++i) { + expectation = [self expectationWithDescription:@""]; + + [realm beginWriteTransaction]; + values[i] = _values[i]; + [AllTypesWithPrimaryKey createOrUpdateChangedInRealm:realm withValue:values]; + [realm commitWriteTransaction]; + + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + } + [token invalidate]; + +} + @end diff --git a/RealmSwift/Realm.swift b/RealmSwift/Realm.swift index def1432389..5c246daf11 100644 --- a/RealmSwift/Realm.swift +++ b/RealmSwift/Realm.swift @@ -264,31 +264,94 @@ public final class Realm { // MARK: Adding and Creating objects /** - Adds or updates an existing object into the Realm. + What to do when an object being added to or created in a Realm has a primary key that already exists. + */ + 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 = 0 + /** + 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 changed = 1 + /** + 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 + } + + /** + Adds an unmanaged object to this Realm. - Only pass `true` to `update` if the object has a primary key. If no object exists in the Realm with the same - primary key value, the object is inserted. Otherwise, the existing object is updated with any changed values. + If `update` is `true` and an object with the same primary key already exists in this Realm, the existing object + will be overwritten by the newly added one. If `update` is `false` then it is instead a non-recoverable error + to add an object with a primary key that is already in use. `update` must be `false` for object types which + do not have a primary key. - When added, all child relationships referenced by this object will also be added to the Realm if they are not - already in it. If the object or any related objects are already being managed by a different Realm an error will be - thrown. Instead, use one of the `create` functions to insert a copy of a managed object into a different Realm. + Adding an object to a Realm will also add all child relationships referenced by that object (via `Object` and + `List` properties). Those objects must also be valid objects to add to this Realm, and the value of + the `update:` parameter is propagated to those adds. - The object to be added must be valid and cannot have been previously deleted from a Realm (i.e. `isInvalidated` - must be `false`). + The object to be added must either be an unmanaged object or a valid object which is already managed by this + Realm. Adding an object already managed by this Realm is a no-op, while adding an object which is managed by + another Realm or which has been deleted from any Realm (i.e. one where `isInvalidated` is `true`) is an error. + + To copy a managed object from one Realm to another, use `create()` instead. + + - warning: This method may only be called during a write transaction. - parameter object: The object to be added to this Realm. - parameter update: If `true`, the Realm will try to find an existing copy of the object (with the same primary key), and update it. Otherwise, the object will be added. */ - public func add(_ object: Object, update: Bool = false) { - if update && object.objectSchema.primaryKeyProperty == nil { + @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + public func add(_ object: Object, update: Bool) { + add(object, update: update ? .all : .error) + } + + /** + Adds an unmanaged object to this Realm. + + If an object with the same primary key already exists in this Realm, it is updated with the property values from + this object as specified by the `UpdatePolicy` selected. The update policy must be `.error` for objects with no + primary key. + + Adding an object to a Realm will also add all child relationships referenced by that object (via `Object` and + `List` properties). Those objects must also be valid objects to add to this Realm, and the value of + the `update:` parameter is propagated to those adds. + + The object to be added must either be an unmanaged object or a valid object which is already managed by this + Realm. Adding an object already managed by this Realm is a no-op, while adding an object which is managed by + another Realm or which has been deleted from any Realm (i.e. one where `isInvalidated` is `true`) is an error. + + To copy a managed object from one Realm to another, use `create()` instead. + + - warning: This method may only be called during a write transaction. + + - parameter object: The object to be added to this Realm. + - parameter update: What to do if an object with the same primary key alredy exists. Must be `.error` for objects + without a primary key. + */ + public func add(_ object: Object, update: UpdatePolicy = .error) { + if update != .error && object.objectSchema.primaryKeyProperty == nil { throwRealmException("'\(object.objectSchema.className)' does not have a primary key and can not be updated") } - RLMAddObjectToRealm(object, rlmRealm, update) + RLMAddObjectToRealm(object, rlmRealm, RLMUpdatePolicy(rawValue: UInt(update.rawValue))!) } /** - Adds or updates all the objects in a collection into the Realm. + Adds all the objects in a collection into the Realm. - see: `add(_:update:)` @@ -297,7 +360,27 @@ public final class Realm { - parameter objects: A sequence which contains objects to be added to the Realm. - parameter update: If `true`, objects that are already in the Realm will be updated instead of added anew. */ - public func add(_ objects: S, update: Bool = false) where S.Iterator.Element: Object { + @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + public func add(_ objects: S, update: Bool) where S.Iterator.Element: Object { + for obj in objects { + add(obj, update: update) + } + } + + /** + Adds all the objects in a collection into the Realm. + + - see: `add(_:update:)` + + - warning: This method may only be called during a write transaction. + + - parameter objects: A sequence which contains objects to be added to the Realm. + - parameter update: How to handle + without a primary key. + - parameter update: How to handle objects in the collection with a primary key that alredy exists in this + Realm. Must be `.error` for object types without a primary key. + */ + public func add(_ objects: S, update: UpdatePolicy = .error) where S.Iterator.Element: Object { for obj in objects { add(obj, update: update) } @@ -339,12 +422,49 @@ public final class Realm { - returns: The newly created object. */ @discardableResult - public func create(_ type: T.Type, value: Any = [:], update: Bool = false) -> T { - let typeName = (type as Object.Type).className() - if update && schema[typeName]?.primaryKeyProperty == nil { - throwRealmException("'\(typeName)' does not have a primary key and can not be updated") + @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + public func create(_ type: T.Type, value: Any = [:], update: Bool) -> T { + return create(type, value: value, update: update ? .all : .error) + } + + /** + Creates a Realm object with a given value, adding it to the Realm and returning it. + + The `value` argument can be a Realm object, a key-value coding compliant object, an array + or dictionary returned from the methods in `NSJSONSerialization`, or an `Array` containing + one element for each managed property. Do not pass in a `LinkingObjects` instance, either + by itself or as a member of a collection. If the `value` argument is an array, all properties + must be present, valid and in the same order as the properties defined in the model. + + If the object type does not have a primary key or no object with the specified primary key + already exists, a new object is created in the Realm. If an object already exists in the Realm + with the specified primary key and the update policy is `.changed` or `.all`, the existing + object will be updated and a reference to that object will be returned. + + If the object is being updated, all properties defined in its schema will be set by copying + from `value` using key-value coding. If the `value` argument does not respond to `value(forKey:)` + for a given property name (or getter name, if defined), that value will remain untouched. + Nullable properties on the object can be set to nil by using `NSNull` as the updated value, + or (if you are passing in an instance of an `Object` subclass) setting the corresponding + property on `value` to nil. + + - warning: This method may only be called during a write transaction. + + - parameter type: The type of the object to create. + - parameter value: The value used to populate the object. + - parameter update: What to do if an object with the same primary key alredy exists. Must be `.error` for object + types without a primary key. + + - returns: The newly created object. + */ + @discardableResult + public func create(_ type: T.Type, value: Any = [:], update: UpdatePolicy = .error) -> T { + if update != .error { + RLMVerifyHasPrimaryKey(T.self) } - return unsafeDowncast(RLMCreateObjectInRealmWithValue(rlmRealm, typeName, value, update), to: T.self) + let typeName = (type as Object.Type).className() + return unsafeDowncast(RLMCreateObjectInRealmWithValue(rlmRealm, typeName, value, + RLMUpdatePolicy(rawValue: UInt(update.rawValue))!), to: T.self) } /** @@ -387,11 +507,56 @@ public final class Realm { :nodoc: */ @discardableResult - public func dynamicCreate(_ typeName: String, value: Any = [:], update: Bool = false) -> DynamicObject { - if update && schema[typeName]?.primaryKeyProperty == nil { + @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + public func dynamicCreate(_ typeName: String, value: Any = [:], update: Bool) -> DynamicObject { + return dynamicCreate(typeName, value: value, update: update ? .all : .error) + } + + /** + This method is useful only in specialized circumstances, for example, when building + components that integrate with Realm. If you are simply building an app on Realm, it is + recommended to use the typed method `create(_:value:update:)`. + + Creates or updates an object with the given class name and adds it to the `Realm`, populating + the object with the given value. + + The `value` argument can be a Realm object, a key-value coding compliant object, an array + or dictionary returned from the methods in `NSJSONSerialization`, or an `Array` containing + one element for each managed property. Do not pass in a `LinkingObjects` instance, either + by itself or as a member of a collection. If the `value` argument is an array, all properties + must be present, valid and in the same order as the properties defined in the model. + + If the object type does not have a primary key or no object with the specified primary key + already exists, a new object is created in the Realm. If an object already exists in the Realm + with the specified primary key and the update policy is `.changed` or `.all`, the existing + object will be updated and a reference to that object will be returned. + + If the object is being updated, all properties defined in its schema will be set by copying + from `value` using key-value coding. If the `value` argument does not respond to `value(forKey:)` + for a given property name (or getter name, if defined), that value will remain untouched. + Nullable properties on the object can be set to nil by using `NSNull` as the updated value, + or (if you are passing in an instance of an `Object` subclass) setting the corresponding + property on `value` to nil. + + + - warning: This method can only be called during a write transaction. + + - parameter className: The class name of the object to create. + - parameter value: The value used to populate the object. + - parameter update: What to do if an object with the same primary key alredy exists. + Must be `.error` for object types without a primary key. + + - returns: The created object. + + :nodoc: + */ + @discardableResult + public func dynamicCreate(_ typeName: String, value: Any = [:], update: UpdatePolicy = .error) -> DynamicObject { + if update != .error && schema[typeName]?.primaryKeyProperty == nil { throwRealmException("'\(typeName)' does not have a primary key and can not be updated") } - return noWarnUnsafeBitCast(RLMCreateObjectInRealmWithValue(rlmRealm, typeName, value, update), + return noWarnUnsafeBitCast(RLMCreateObjectInRealmWithValue(rlmRealm, typeName, value, + RLMUpdatePolicy(rawValue: UInt(update.rawValue))!), to: DynamicObject.self) } diff --git a/RealmSwift/Sync.swift b/RealmSwift/Sync.swift index 033d490854..92724d79f4 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -1677,7 +1677,7 @@ extension List where Element == Permission { */ public func findOrCreate(forRoleNamed roleName: String) -> Permission { precondition(realm != nil, "Cannot be called on an unmanaged object") - return RLMPermissionForRole(_rlmArray, realm!.create(PermissionRole.self, value: [roleName], update: true)) as! Permission + return RLMPermissionForRole(_rlmArray, realm!.create(PermissionRole.self, value: [roleName], update: .changed)) as! Permission } /** diff --git a/RealmSwift/Tests/ObjectCreationTests.swift b/RealmSwift/Tests/ObjectCreationTests.swift index 6d77320df3..3c0bbcbfb8 100644 --- a/RealmSwift/Tests/ObjectCreationTests.swift +++ b/RealmSwift/Tests/ObjectCreationTests.swift @@ -353,7 +353,24 @@ class ObjectCreationTests: TestCase { let standalone = SwiftPrimaryStringObject(value: ["primary", 11]) try! Realm().beginWrite() let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: ["otherPrimary", ["primary", 12], - [["primary", 12]]], update: true) + [["primary", 12]]], update: .all) + try! Realm().commitWrite() + + let stringObjects = try! Realm().objects(SwiftPrimaryStringObject.self) + XCTAssertEqual(stringObjects.count, 1) + let persistedObject = object.object! + + XCTAssertEqual(persistedObject.intCol, 12) + XCTAssertNil(standalone.realm) // the standalone object should be copied, rather than added, to the realm + XCTAssertEqual(object.object!, persistedObject) + XCTAssertEqual(object.objects.first!, persistedObject) + } + + func testUpdateChangedWithNestedObjects() { + let standalone = SwiftPrimaryStringObject(value: ["primary", 11]) + try! Realm().beginWrite() + let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: ["otherPrimary", ["primary", 12], + [["primary", 12]]], update: .changed) try! Realm().commitWrite() let stringObjects = try! Realm().objects(SwiftPrimaryStringObject.self) @@ -435,7 +452,23 @@ class ObjectCreationTests: TestCase { try! Realm().beginWrite() try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: ["primary", ["10", 10], [["11", 11]]]) - let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: otherRealmObject, update: true) + let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: otherRealmObject, update: .all) + try! Realm().commitWrite() + + XCTAssertNotEqual(otherRealmObject, object) // the object from the other realm should be copied into this realm + XCTAssertEqual(try! Realm().objects(SwiftLinkToPrimaryStringObject.self).count, 1) + XCTAssertEqual(try! Realm().objects(SwiftPrimaryStringObject.self).count, 4) + } + + func testUpdateChangedWithObjectsFromAnotherRealm() { + realmWithTestPath().beginWrite() + let otherRealmObject = realmWithTestPath().create(SwiftLinkToPrimaryStringObject.self, + value: ["primary", NSNull(), [["2", 2], ["4", 4]]]) + try! realmWithTestPath().commitWrite() + + try! Realm().beginWrite() + try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: ["primary", ["10", 10], [["11", 11]]]) + let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: otherRealmObject, update: .changed) try! Realm().commitWrite() XCTAssertNotEqual(otherRealmObject, object) // the object from the other realm should be copied into this realm @@ -499,7 +532,7 @@ class ObjectCreationTests: TestCase { realm.beginWrite() // Create with all fields nil - let object = realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: true) + let object = realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .all) XCTAssertNil(object.id.value) XCTAssertNil(object.optIntCol.value) @@ -531,7 +564,7 @@ class ObjectCreationTests: TestCase { object2.optNSStringCol = "" object2.optBinaryCol = Data() object2.optObjectCol = SwiftBoolObject() - realm.create(SwiftOptionalPrimaryObject.self, value: object2, update: true) + realm.create(SwiftOptionalPrimaryObject.self, value: object2, update: .all) XCTAssertNil(object.id.value) XCTAssertNotNil(object.optIntCol.value) @@ -549,7 +582,83 @@ class ObjectCreationTests: TestCase { XCTAssertNotNil(object.optObjectCol) // Try to switch back to nil - realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: true) + realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .all) + + XCTAssertNil(object.id.value) + + XCTAssertNil(object.optIntCol.value) + XCTAssertNil(object.optInt8Col.value) + XCTAssertNil(object.optInt16Col.value) + XCTAssertNil(object.optInt32Col.value) + XCTAssertNil(object.optInt64Col.value) + XCTAssertNil(object.optBoolCol.value) + XCTAssertNil(object.optFloatCol.value) + XCTAssertNil(object.optDoubleCol.value) + XCTAssertNil(object.optDateCol) + XCTAssertNil(object.optStringCol) + XCTAssertNil(object.optNSStringCol) + XCTAssertNil(object.optBinaryCol) + XCTAssertNil(object.optObjectCol) + + realm.cancelWrite() + } + + func testCreateOrUpdateChangedNil() { + let realm = try! Realm() + realm.beginWrite() + + // Create with all fields nil + let object = realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .changed) + + XCTAssertNil(object.id.value) + XCTAssertNil(object.optIntCol.value) + XCTAssertNil(object.optInt8Col.value) + XCTAssertNil(object.optInt16Col.value) + XCTAssertNil(object.optInt32Col.value) + XCTAssertNil(object.optInt64Col.value) + XCTAssertNil(object.optBoolCol.value) + XCTAssertNil(object.optFloatCol.value) + XCTAssertNil(object.optDoubleCol.value) + XCTAssertNil(object.optDateCol) + XCTAssertNil(object.optStringCol) + XCTAssertNil(object.optNSStringCol) + XCTAssertNil(object.optBinaryCol) + XCTAssertNil(object.optObjectCol) + + // Try to switch to non-nil + let object2 = SwiftOptionalPrimaryObject() + object2.optIntCol.value = 1 + object2.optInt8Col.value = 1 + object2.optInt16Col.value = 1 + object2.optInt32Col.value = 1 + object2.optInt64Col.value = 1 + object2.optFloatCol.value = 1 + object2.optDoubleCol.value = 1 + object2.optBoolCol.value = true + object2.optDateCol = Date() + object2.optStringCol = "" + object2.optNSStringCol = "" + object2.optBinaryCol = Data() + object2.optObjectCol = SwiftBoolObject() + realm.create(SwiftOptionalPrimaryObject.self, value: object2, update: .changed) + + XCTAssertNil(object.id.value) + XCTAssertNotNil(object.optIntCol.value) + XCTAssertNotNil(object.optInt8Col.value) + XCTAssertNotNil(object.optInt16Col.value) + XCTAssertNotNil(object.optInt32Col.value) + XCTAssertNotNil(object.optInt64Col.value) + XCTAssertNotNil(object.optBoolCol.value) + XCTAssertNotNil(object.optFloatCol.value) + XCTAssertNotNil(object.optDoubleCol.value) + XCTAssertNotNil(object.optDateCol) + XCTAssertNotNil(object.optStringCol) + XCTAssertNotNil(object.optNSStringCol) + XCTAssertNotNil(object.optBinaryCol) + XCTAssertNotNil(object.optObjectCol) + + // Try to switch back to nil + realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .changed) XCTAssertNil(object.id.value) @@ -575,7 +684,7 @@ class ObjectCreationTests: TestCase { let unmanagedValue = SwiftOptionalPrimaryObject() // Shouldn't throw. realm.beginWrite() - _ = realm.create(type(of: unmanagedValue), value: unmanagedValue, update: true) + _ = realm.create(type(of: unmanagedValue), value: unmanagedValue, update: .changed) realm.cancelWrite() } @@ -587,7 +696,19 @@ class ObjectCreationTests: TestCase { } // Shouldn't throw. realm.beginWrite() - _ = realm.create(type(of: managedValue), value: managedValue, update: true) + _ = realm.create(type(of: managedValue), value: managedValue, update: .all) + realm.cancelWrite() + } + + func testCreateOrUpdateChangedDynamicManagedType() { + let realm = try! Realm() + let managedValue = SwiftOptionalPrimaryObject() + try! realm.write { + realm.add(managedValue) + } + // Shouldn't throw. + realm.beginWrite() + _ = realm.create(type(of: managedValue), value: managedValue, update: .changed) realm.cancelWrite() } @@ -617,7 +738,22 @@ class ObjectCreationTests: TestCase { try! Realm().beginWrite() let object = SwiftLinkToPrimaryStringObject(value: ["primary", ["primary", 2], []]) - try! Realm().add(object, update: true) + try! Realm().add(object, update: .all) + try! Realm().commitWrite() + + XCTAssertNotNil(object.realm) + XCTAssertEqual(object.object!, existingObject) // the existing object should be updated + XCTAssertEqual(existingObject.intCol, 2) + } + + func testAddAndUpdateChangedWithExisingNestedObjects() { + try! Realm().beginWrite() + let existingObject = try! Realm().create(SwiftPrimaryStringObject.self, value: ["primary", 1]) + try! Realm().commitWrite() + + try! Realm().beginWrite() + let object = SwiftLinkToPrimaryStringObject(value: ["primary", ["primary", 2], []]) + try! Realm().add(object, update: .changed) try! Realm().commitWrite() XCTAssertNotNil(object.realm) @@ -688,7 +824,84 @@ class ObjectCreationTests: TestCase { object2.optNSStringCol = "" object2.optBinaryCol = Data() object2.optObjectCol = SwiftBoolObject() - realm.add(object2, update: true) + realm.add(object2, update: .all) + + XCTAssertNil(object.id.value) + XCTAssertNotNil(object.optIntCol.value) + XCTAssertNotNil(object.optInt8Col.value) + XCTAssertNotNil(object.optInt16Col.value) + XCTAssertNotNil(object.optInt32Col.value) + XCTAssertNotNil(object.optInt64Col.value) + XCTAssertNotNil(object.optBoolCol.value) + XCTAssertNotNil(object.optFloatCol.value) + XCTAssertNotNil(object.optDoubleCol.value) + XCTAssertNotNil(object.optDateCol) + XCTAssertNotNil(object.optStringCol) + XCTAssertNotNil(object.optNSStringCol) + XCTAssertNotNil(object.optBinaryCol) + XCTAssertNotNil(object.optObjectCol) + + // Try to switch back to nil + let object3 = SwiftOptionalPrimaryObject() + realm.add(object3, update: .all) + + XCTAssertNil(object.id.value) + XCTAssertNil(object.optIntCol.value) + XCTAssertNil(object.optInt8Col.value) + XCTAssertNil(object.optInt16Col.value) + XCTAssertNil(object.optInt32Col.value) + XCTAssertNil(object.optInt64Col.value) + XCTAssertNil(object.optBoolCol.value) + XCTAssertNil(object.optFloatCol.value) + XCTAssertNil(object.optDoubleCol.value) + XCTAssertNil(object.optDateCol) + XCTAssertNil(object.optStringCol) + XCTAssertNil(object.optNSStringCol) + XCTAssertNil(object.optBinaryCol) + XCTAssertNil(object.optObjectCol) + + realm.cancelWrite() + } + + func testAddOrUpdateChangedNil() { + let realm = try! Realm() + realm.beginWrite() + + // Create with all fields nil + let object = SwiftOptionalPrimaryObject() + realm.add(object) + + XCTAssertNil(object.id.value) + XCTAssertNil(object.optIntCol.value) + XCTAssertNil(object.optInt8Col.value) + XCTAssertNil(object.optInt16Col.value) + XCTAssertNil(object.optInt32Col.value) + XCTAssertNil(object.optInt64Col.value) + XCTAssertNil(object.optBoolCol.value) + XCTAssertNil(object.optFloatCol.value) + XCTAssertNil(object.optDoubleCol.value) + XCTAssertNil(object.optDateCol) + XCTAssertNil(object.optStringCol) + XCTAssertNil(object.optNSStringCol) + XCTAssertNil(object.optBinaryCol) + XCTAssertNil(object.optObjectCol) + + // Try to switch to non-nil + let object2 = SwiftOptionalPrimaryObject() + object2.optIntCol.value = 1 + object2.optInt8Col.value = 1 + object2.optInt16Col.value = 1 + object2.optInt32Col.value = 1 + object2.optInt64Col.value = 1 + object2.optFloatCol.value = 1 + object2.optDoubleCol.value = 1 + object2.optBoolCol.value = true + object2.optDateCol = Date() + object2.optStringCol = "" + object2.optNSStringCol = "" + object2.optBinaryCol = Data() + object2.optObjectCol = SwiftBoolObject() + realm.add(object2, update: .changed) XCTAssertNil(object.id.value) XCTAssertNotNil(object.optIntCol.value) @@ -707,7 +920,7 @@ class ObjectCreationTests: TestCase { // Try to switch back to nil let object3 = SwiftOptionalPrimaryObject() - realm.add(object3, update: true) + realm.add(object3, update: .changed) XCTAssertNil(object.id.value) XCTAssertNil(object.optIntCol.value) diff --git a/RealmSwift/Tests/RealmTests.swift b/RealmSwift/Tests/RealmTests.swift index 3fc8d36b7f..ef628f4b45 100644 --- a/RealmSwift/Tests/RealmTests.swift +++ b/RealmSwift/Tests/RealmTests.swift @@ -317,16 +317,16 @@ class RealmTests: TestCase { var defaultRealmObject: SwiftPrimaryStringObject! try! realm.write { defaultRealmObject = SwiftPrimaryStringObject() - realm.add(defaultRealmObject, update: true) + realm.add(defaultRealmObject, update: .all) XCTAssertEqual(1, realm.objects(SwiftPrimaryStringObject.self).count) - realm.add(SwiftPrimaryStringObject(), update: true) + realm.add(SwiftPrimaryStringObject(), update: .all) XCTAssertEqual(1, realm.objects(SwiftPrimaryStringObject.self).count) } XCTAssertEqual(1, realm.objects(SwiftPrimaryStringObject.self).count) let testRealm = realmWithTestPath() try! testRealm.write { - self.assertThrows(_ = testRealm.add(defaultRealmObject, update: true)) + self.assertThrows(_ = testRealm.add(defaultRealmObject, update: .all)) } } @@ -352,14 +352,14 @@ class RealmTests: TestCase { XCTAssertEqual(0, realm.objects(SwiftPrimaryStringObject.self).count) try! realm.write { let objs = [SwiftPrimaryStringObject(), SwiftPrimaryStringObject()] - realm.add(objs, update: true) + realm.add(objs, update: .all) XCTAssertEqual(1, realm.objects(SwiftPrimaryStringObject.self).count) } XCTAssertEqual(1, realm.objects(SwiftPrimaryStringObject.self).count) let testRealm = realmWithTestPath() try! testRealm.write { - self.assertThrows(_ = testRealm.add(realm.objects(SwiftPrimaryStringObject.self), update: true)) + self.assertThrows(_ = testRealm.add(realm.objects(SwiftPrimaryStringObject.self), update: .all)) } }