Skip to content

Commit

Permalink
Merge pull request #6149 from realm/tg/diffed-updates
Browse files Browse the repository at this point in the history
Add an option to only set the properties which actually change
  • Loading branch information
tgoyne authored May 28, 2019
2 parents ce94f4b + 104f4d7 commit 99c6e4c
Show file tree
Hide file tree
Showing 14 changed files with 644 additions and 72 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
Expand Down
18 changes: 11 additions & 7 deletions Realm/RLMAccessor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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<RLMObjectBase>(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) {
Expand All @@ -743,21 +747,21 @@ 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;
}

if (!link->_realm) {
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;
}
Expand Down
99 changes: 97 additions & 2 deletions Realm/RLMObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 `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
order as the properties defined in the model.
Expand All @@ -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, `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
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)createOrUpdateModifiedInDefaultRealmWithValue:(id)value;

/**
Creates or updates an Realm object within a specified Realm.
Expand All @@ -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 `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
order as the properties defined in the model.
Expand All @@ -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)createOrUpdateModifiedInRealm:(RLMRealm *)realm withValue:(id)value;

#pragma mark - Properties

/**
Expand Down
22 changes: 13 additions & 9 deletions Realm/RLMObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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)createOrUpdateModifiedInDefaultRealmWithValue:(id)value {
return [self createOrUpdateModifiedInRealm:[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)createOrUpdateModifiedInRealm:(RLMRealm *)realm withValue:(id)value {
RLMVerifyHasPrimaryKey(self);
return (RLMObject *)RLMCreateObjectInRealmWithValue(realm, [self className], value, RLMUpdatePolicyUpdateChanged);
}

#pragma mark - Subscripting
Expand Down
13 changes: 10 additions & 3 deletions Realm/RLMObjectStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand All @@ -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);
Expand All @@ -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
//
Expand Down
21 changes: 16 additions & 5 deletions Realm/RLMObjectStore.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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.
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions Realm/RLMRealm.mm
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ - (BOOL)refresh {
}

- (void)addObject:(__unsafe_unretained RLMObject *const)object {
RLMAddObjectToRealm(object, self, false);
RLMAddObjectToRealm(object, self, RLMUpdatePolicyError);
}

- (void)addObjects:(id<NSFastEnumeration>)objects {
Expand All @@ -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<NSFastEnumeration>)objects {
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions Realm/Tests/DynamicTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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++) {
Expand Down
Loading

0 comments on commit 99c6e4c

Please sign in to comment.