From 8d55465658a30e4ef843ea4c0cab8691cabd0bd0 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 5 Dec 2018 13:01:09 -0800 Subject: [PATCH 1/2] 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 7694f31b4b..d4898d02ff 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -1683,7 +1683,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)) } } From 104f4d7aa88eeadd3a105fe002f02b1bfa21831c Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Fri, 24 May 2019 14:05:41 -0700 Subject: [PATCH 2/2] Renamed "changed" to "modified" to align with JS --- Realm/RLMObject.h | 10 ++++----- Realm/RLMObject.mm | 6 ++--- Realm/Tests/NotificationTests.m | 2 +- RealmSwift/Realm.swift | 14 ++++++------ RealmSwift/Sync.swift | 2 +- RealmSwift/Tests/ObjectCreationTests.swift | 26 +++++++++++----------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Realm/RLMObject.h b/Realm/RLMObject.h index 6a49eec169..753fe03735 100644 --- a/Realm/RLMObject.h +++ b/Realm/RLMObject.h @@ -199,7 +199,7 @@ NS_ASSUME_NONNULL_BEGIN 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:` + notifications will report them all being changed. See `createOrUpdateModifiedInDefaultRealmWithValue:` 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 @@ -218,7 +218,7 @@ NS_ASSUME_NONNULL_BEGIN 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 + If nested objects are included in the argument, `createOrUpdateModifiedInDefaultRealmWithValue:` 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 @@ -253,7 +253,7 @@ NS_ASSUME_NONNULL_BEGIN @see `defaultPropertyValues`, `primaryKey` */ -+ (instancetype)createOrUpdateChangedInDefaultRealmWithValue:(id)value; ++ (instancetype)createOrUpdateModifiedInDefaultRealmWithValue:(id)value; /** Creates or updates an Realm object within a specified Realm. @@ -281,7 +281,7 @@ NS_ASSUME_NONNULL_BEGIN 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:` + notifications will report them all being changed. See `createOrUpdateModifiedInRealm: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 @@ -337,7 +337,7 @@ NS_ASSUME_NONNULL_BEGIN @see `defaultPropertyValues`, `primaryKey` */ -+ (instancetype)createOrUpdateChangedInRealm:(RLMRealm *)realm withValue:(id)value; ++ (instancetype)createOrUpdateModifiedInRealm:(RLMRealm *)realm withValue:(id)value; #pragma mark - Properties diff --git a/Realm/RLMObject.mm b/Realm/RLMObject.mm index eef3e8711e..1ae01eb326 100644 --- a/Realm/RLMObject.mm +++ b/Realm/RLMObject.mm @@ -83,8 +83,8 @@ + (instancetype)createOrUpdateInDefaultRealmWithValue:(id)value { return [self createOrUpdateInRealm:[RLMRealm defaultRealm] withValue:value]; } -+ (instancetype)createOrUpdateChangedInDefaultRealmWithValue:(id)value { - return [self createOrUpdateChangedInRealm:[RLMRealm defaultRealm] withValue:value]; ++ (instancetype)createOrUpdateModifiedInDefaultRealmWithValue:(id)value { + return [self createOrUpdateModifiedInRealm:[RLMRealm defaultRealm] withValue:value]; } + (instancetype)createOrUpdateInRealm:(RLMRealm *)realm withValue:(id)value { @@ -92,7 +92,7 @@ + (instancetype)createOrUpdateInRealm:(RLMRealm *)realm withValue:(id)value { return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyUpdateAll); } -+ (instancetype)createOrUpdateChangedInRealm:(RLMRealm *)realm withValue:(id)value { ++ (instancetype)createOrUpdateModifiedInRealm:(RLMRealm *)realm withValue:(id)value { RLMVerifyHasPrimaryKey(self); return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyUpdateChanged); } diff --git a/Realm/Tests/NotificationTests.m b/Realm/Tests/NotificationTests.m index fdbbc7290b..228c694305 100644 --- a/Realm/Tests/NotificationTests.m +++ b/Realm/Tests/NotificationTests.m @@ -1141,7 +1141,7 @@ - (void)testDiffedUpdatesOnlyNotifyForPropertiesWhichActuallyChanged { [realm beginWriteTransaction]; values[i] = _values[i]; - [AllTypesWithPrimaryKey createOrUpdateChangedInRealm:realm withValue:values]; + [AllTypesWithPrimaryKey createOrUpdateModifiedInRealm:realm withValue:values]; [realm commitWriteTransaction]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; diff --git a/RealmSwift/Realm.swift b/RealmSwift/Realm.swift index 5c246daf11..aa6fc275ff 100644 --- a/RealmSwift/Realm.swift +++ b/RealmSwift/Realm.swift @@ -281,7 +281,7 @@ public final class Realm { 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 + case modified = 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. @@ -315,7 +315,7 @@ public final class 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. */ - @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + @available(*, deprecated, message: "Pass .error, .modified 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) } @@ -360,7 +360,7 @@ 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. */ - @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + @available(*, deprecated, message: "Pass .error, .modified 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) @@ -422,7 +422,7 @@ public final class Realm { - returns: The newly created object. */ @discardableResult - @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + @available(*, deprecated, message: "Pass .error, .modified 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) } @@ -438,7 +438,7 @@ public final class Realm { 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 + with the specified primary key and the update policy is `.modified` 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 @@ -507,7 +507,7 @@ public final class Realm { :nodoc: */ @discardableResult - @available(*, deprecated, message: "Pass .error, .changed or .all rather than a boolean. .error is equivalent to false and .all is equivalent to true.") + @available(*, deprecated, message: "Pass .error, .modified 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) } @@ -528,7 +528,7 @@ public final class Realm { 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 + with the specified primary key and the update policy is `.modified` 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 diff --git a/RealmSwift/Sync.swift b/RealmSwift/Sync.swift index d4898d02ff..4785021fca 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -1683,7 +1683,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: .changed)) as! Permission + return RLMPermissionForRole(_rlmArray, realm!.create(PermissionRole.self, value: [roleName], update: .modified)) as! Permission } /** diff --git a/RealmSwift/Tests/ObjectCreationTests.swift b/RealmSwift/Tests/ObjectCreationTests.swift index 3c0bbcbfb8..19d459d27f 100644 --- a/RealmSwift/Tests/ObjectCreationTests.swift +++ b/RealmSwift/Tests/ObjectCreationTests.swift @@ -370,7 +370,7 @@ 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: .changed) + [["primary", 12]]], update: .modified) try! Realm().commitWrite() let stringObjects = try! Realm().objects(SwiftPrimaryStringObject.self) @@ -468,7 +468,7 @@ 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: .changed) + let object = try! Realm().create(SwiftLinkToPrimaryStringObject.self, value: otherRealmObject, update: .modified) try! Realm().commitWrite() XCTAssertNotEqual(otherRealmObject, object) // the object from the other realm should be copied into this realm @@ -603,12 +603,12 @@ class ObjectCreationTests: TestCase { realm.cancelWrite() } - func testCreateOrUpdateChangedNil() { + func testCreateOrUpdateModifiedNil() { let realm = try! Realm() realm.beginWrite() // Create with all fields nil - let object = realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .changed) + let object = realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .modified) XCTAssertNil(object.id.value) XCTAssertNil(object.optIntCol.value) @@ -640,7 +640,7 @@ class ObjectCreationTests: TestCase { object2.optNSStringCol = "" object2.optBinaryCol = Data() object2.optObjectCol = SwiftBoolObject() - realm.create(SwiftOptionalPrimaryObject.self, value: object2, update: .changed) + realm.create(SwiftOptionalPrimaryObject.self, value: object2, update: .modified) XCTAssertNil(object.id.value) XCTAssertNotNil(object.optIntCol.value) @@ -658,7 +658,7 @@ class ObjectCreationTests: TestCase { XCTAssertNotNil(object.optObjectCol) // Try to switch back to nil - realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .changed) + realm.create(SwiftOptionalPrimaryObject.self, value: SwiftOptionalPrimaryObject(), update: .modified) XCTAssertNil(object.id.value) @@ -684,7 +684,7 @@ class ObjectCreationTests: TestCase { let unmanagedValue = SwiftOptionalPrimaryObject() // Shouldn't throw. realm.beginWrite() - _ = realm.create(type(of: unmanagedValue), value: unmanagedValue, update: .changed) + _ = realm.create(type(of: unmanagedValue), value: unmanagedValue, update: .modified) realm.cancelWrite() } @@ -700,7 +700,7 @@ class ObjectCreationTests: TestCase { realm.cancelWrite() } - func testCreateOrUpdateChangedDynamicManagedType() { + func testCreateOrUpdateModifiedDynamicManagedType() { let realm = try! Realm() let managedValue = SwiftOptionalPrimaryObject() try! realm.write { @@ -708,7 +708,7 @@ class ObjectCreationTests: TestCase { } // Shouldn't throw. realm.beginWrite() - _ = realm.create(type(of: managedValue), value: managedValue, update: .changed) + _ = realm.create(type(of: managedValue), value: managedValue, update: .modified) realm.cancelWrite() } @@ -753,7 +753,7 @@ class ObjectCreationTests: TestCase { try! Realm().beginWrite() let object = SwiftLinkToPrimaryStringObject(value: ["primary", ["primary", 2], []]) - try! Realm().add(object, update: .changed) + try! Realm().add(object, update: .modified) try! Realm().commitWrite() XCTAssertNotNil(object.realm) @@ -863,7 +863,7 @@ class ObjectCreationTests: TestCase { realm.cancelWrite() } - func testAddOrUpdateChangedNil() { + func testAddOrUpdateModifiedNil() { let realm = try! Realm() realm.beginWrite() @@ -901,7 +901,7 @@ class ObjectCreationTests: TestCase { object2.optNSStringCol = "" object2.optBinaryCol = Data() object2.optObjectCol = SwiftBoolObject() - realm.add(object2, update: .changed) + realm.add(object2, update: .modified) XCTAssertNil(object.id.value) XCTAssertNotNil(object.optIntCol.value) @@ -920,7 +920,7 @@ class ObjectCreationTests: TestCase { // Try to switch back to nil let object3 = SwiftOptionalPrimaryObject() - realm.add(object3, update: .changed) + realm.add(object3, update: .modified) XCTAssertNil(object.id.value) XCTAssertNil(object.optIntCol.value)