Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partially updating an object sends the full object to the server #242

Closed
4 tasks done
dblythy opened this issue Sep 22, 2021 · 65 comments · Fixed by #243 or #248
Closed
4 tasks done

Partially updating an object sends the full object to the server #242

dblythy opened this issue Sep 22, 2021 · 65 comments · Fixed by #243 or #248
Labels
type:bug Impaired feature or lacking behavior that is likely assumed

Comments

@dblythy
Copy link
Member

dblythy commented Sep 22, 2021

New Issue Checklist

Issue Description

When fetching a saved object and then calling .save, all keys are marked as dirty and saved to the backend, causing unnecessary traffic and database ops.

Steps to reproduce

  1. Fetch an existing object
  2. Have a Parse Cloud beforeSave trigger that logs object.dirtyKeys().

Actual Outcome

All fields are dirty.

Expected Outcome

No fields should be dirty unless mutated.

Environment

Client

  • Parse Swift SDK version: 1.9.10
  • Xcode version: 10.15.7
  • Operating system (iOS, macOS, watchOS, etc.): iOS
  • Operating system version: 14.7

Server

  • Parse Server version: 4.5.0
  • Operating system: macOS
  • Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): localhost

Database

  • System (MongoDB or Postgres): mongodb
  • Database version: 4.2
  • Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): Atlas

Logs

@parse-github-assistant
Copy link

parse-github-assistant bot commented Sep 22, 2021

Thanks for opening this issue!

  • 🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our Contribution Guide for how to make a pull request, or read our New Contributor's Guide if this is your first time contributing.

@cbaker6 cbaker6 added the type:question Support or code-level question label Sep 22, 2021
@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

Posting my responses to your comments in #241:

So there's no way to only send mutated keys to the server using Parse Swift?

There are a few ways, but the developer has to take some action here. For example, developers can add the following method (or a similar one) to their ParseObject's:

struct MyUser: ParseUser {
    var authData: [String: [String: String]?]?
    var username: String?
    var email: String?
    var emailVerified: Bool?
    var password: String?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    // method that creates empty versions of self
    func emptyObject() -> Self {
        var object = Self()
        object.objectId = objectId
        return object
    }
}

Then add properties what will be mutated and saved. Only these get sent to the server:

var myObject = MyUser()
myObject.email = "myNewEmail@parse.com"
myObject.save { _ in }

This also can be easily achieved by using ParseOperation:

//: You can have the server do operations on your `ParseObject`'s for you.
//: First lets create another GameScore.
let savedScore: GameScore!
do {
savedScore = try GameScore(score: 102).save()
} catch {
savedScore = nil
fatalError("Error saving: \(error)")
}
//: Then we will increment the score.
let incrementOperation = savedScore
.operation.increment("score", by: 1)
incrementOperation.save { result in
switch result {
case .success:
print("Original score: \(String(describing: savedScore)). Check the new score on Parse Dashboard.")
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

These examples should address most of your comments below:

Just a quick comment - whilst this will likely fix the issue, the core problem of dirtyKeys still remains. This is problematic because:

  • unnecessary data is sent to parse server, which in developing countries may result in unbearable latency, or, on a high scaled system, result in additional data expenses
  • unnecessary database ops will happen when no data is actually changing
  • Cloud functions will tell developers to assume keys are dirty when they are not
  • email being set to dirty is just an example of an unpredictable consequences of all the keys being sent, there could be other cases which we don’t know about.

When it comes to:

@cbaker6 is there a way we could perhaps store the serverData from the initial fetch of the object, and then compare changes to keys onSave if we can’t directly listen to property changes on a struct?

"Listening to property changes" is typically associated to reference types or "instances of Classes" in many languages, not just Swift. Most/All of the Parse SDK's use "classes" with the exception of the Swift SDK. value types or "structs" don't have this ability and are one of the distinct difference between a struct and a class. The Swift SDK doesn't need to listen property changes and instead should use a method similar to what I mentioned at the beginning of this comment. It's important not to think or compare of the Swift SDK to the other SDKs as it has a completely different design philosophy. Flo's comments and thread provide good insight into the design philosophy and the reasoning. For these reasons, the Swift SDK can exist with zero dependencies, not run into the threading issues the other client SDKs encounter, along with having ParseObject's play nicely with MVVM and SwiftUI (from what I see reference types is what's causing a major barrier when using MVVM in the issues you and others mention with the JS SDK).

@dblythy
Copy link
Member Author

dblythy commented Sep 23, 2021

Thank you for your reply here @cbaker6.

In my view, it shouldn't be up to the developer to custom code a solution for the SDK to work as expected with the core Parse Server. One of our attractive features is simple SDKs which offer a broad range of consistent features. I understand that there are tradeoffs with the current implementation, but I still think it's problematic that at the end of the day, Parse Server has no distinction for dirtyKeys in the Swift SDK, which is different from any other SDK.

Although you've said in terms of philosophy it's not valuable to compare across SDKs, I think from both a developer and Server team perspective, it would be expected that beforeSave functions would function the same, regardless of the SDK. It just seems a little odd to me to say "if you use .dirtyKeys in Parse Cloud, make sure you customise ParseSwift so it doesn't send all the data".

And on the race condition, let's say you have an object that is being managed by two Swift SDK users at the same time. They both fetch the object at the same time.

The object looks like:

{
   foo: "bar",
   yolo: "xyz"
}

Both users query this object at the same time.

User one calls:

obj.foo = "aaa", and then saves.

A few minutes later, user two calls:

obj.yolo = "aaa", and then saves.

The result that will be saved in Parse Server would be:

{
   foo: "bar",
   yolo: "aaa"
}

As all keys saved by user two will be overriden. If this was any other SDK, the result would be:

{
   foo: "aaa",
   yolo: "aaa"
}

Meaning again, if you have a multi tenant solution (iOS App, Web App, Android App), using the Swift SDK in its purest form could be problematic and deliver unexpected results.

Could we perhaps store a clone of the fetched object somewhere, and then compare keys as part of the save operation? So when building the save operation, Parse Swift runs something like:

const fetchedObj // this has the fetched state
const updatedObj // this has all the user mutations
const bodyToSend = {};
for (const key in updatedObject) {
  if (fetchedObj[key] !== updatedObj[key]) {
    bodyToSend[key] = updateObj[key]
  }
}

Forgive the JS as said I am not a Swift dev 😊

@dblythy
Copy link
Member Author

dblythy commented Sep 23, 2021

If this technically can't be solved on the SDK side because the use of struct, maybe the solution is to override dirtyKeys in RestWrite if object.key === original.key. I've discussed this with @mtrezza, however he has stated that believes:

Adding this to the server may be a mitigating step, but that doesn’t fully solve the cost and efficiency issue

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

In my view, it shouldn't be up to the developer to custom code a solution for the SDK to work as expected with the core Parse Server. One of our attractive features is simple SDKs which offer a broad range of consistent features. I understand that there are tradeoffs with the current implementation, but I still think it's problematic that at the end of the day, Parse Server has no distinction for dirtyKeys in the Swift SDK, which is different from any other SDK.

Although you've said in terms of philosophy it's not valuable to compare across SDKs, I think from both a developer and Server team perspective, it would be expected that beforeSave functions would function the same, regardless of the SDK. It just seems a little odd to me to say "if you use .dirtyKeys in Parse Cloud, make sure you customise ParseSwift so it doesn't send all the data".

It seems you are thinking in terms of the other SDKs, OOP, and Classes. I pointed to Flo's comment because the philosophy eludes to a number of things traditional users of the other SDKs may be unfamiliar with and the Swift SDK wasn't designed with the restrictions of OOP and Classes. I recommend looking into protocol oriented programing (POP) as it already forces the developer to customize and add their own keys for their ParseObjects, even for the Parse provided keys. The addition of the method I mentioned is simple and straightforward. In POP, developers should feel comfortable using the provided methods, replacing with their own, and adding extensions. As I mentioned in my previous comment, the design patterns of the other SDKs in terms of using OOP and Classes lead to other issues that are not present in the Swift SDK.

Could we perhaps store a clone of the fetched object somewhere, and then compare keys as part of the save operation? So when building the save operation, Parse Swift runs something like:

const fetchedObj // this has the fetched state
const updatedObj // this has all the user mutations
const bodyToSend = {};
for (const key in updatedObject) {
if (fetchedObj[key] !== updatedObj[key]) {
bodyToSend[key] = updateObj[key]
}
}

ParseSwift doesn't have local storage (besides using the Keychain for current) and if it did have storage, it would be protocol based, meaning the developer would implement it or use some 3rd party storage implementation that conformed to the "parse storage protocol". The 3rd party storage can have some notion of "dirty" if it felt it was needed, but depending on the implementation, it may not be necessary. The JS code you mentioned is in the style of the other SDKs which all use runtime encoders/decoders to dynamically access key/value pairs. Those encoders/decoders are also slower and prone to runtime bugs. ParseSwift uses compile time encoders/decoders and doesn't/shouldn't be doing anything like toJSON like the other SDKs to access/modify properties of an object.

Of course, a developer doesn't have to use the Swift SDK and can use the iOS SDK if they want OOP and Class based parse objects.

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

As all keys saved by user two will be overriden. If this was any other SDK, the result would be:

{
foo: "aaa",
yolo: "aaa"
}
Meaning again, if you have a multi tenant solution (iOS App, Web App, Android App), using the Swift SDK in its purest form could be problematic and deliver unexpected results.

This looks like a synchronization issue as a developer can run into the same issue with any of the SDKs if 2 clients attempted to modify/save the same property of an object at the same time. The race condition you mentioned is because Parse doesn't have a real synchronization mechanism and out-of-the-box asks developers to depend on a wall-clock (createdAt and updatedAt). If developers implement their storage in a decentralized way as I suggested, they will never run into the issue you mentioned with the Swift SDK or any of the other SDKs.

@dblythy
Copy link
Member Author

dblythy commented Sep 23, 2021

Right, you're much more familiar with Swift and POP than I am, so I can't confidently argue either way. It just seems strange to me that updating an object with save fundamentally interacts differently with Parse Server than any other SDK. Creating, deleting objects is great with Parse Swift, but I'm having to create a custom method to update an existing object - a core feature of Parse. As I've said previously, I think this should be a consideration developers should be aware of, especially if they use cloud functions.

The synchronization issue will happen for the clients in the other SDKs, but it won't happen on the Parse Server as with the other sdks, only mutated keys are dirty. When a user calls .fetch in the JS SDK, they will get an object with both keys set by each respective user, whereas in the swift sdk they will only get the second users' save.

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

As I've said previously, I think this should be a consideration developers should be aware of, especially if they use cloud functions.

Agreed, they should definitely be made aware and see the workarounds I mentioned earlier.

@dblythy
Copy link
Member Author

dblythy commented Sep 23, 2021

Right, so the consideration here to be aware of, is that if you use Parse Swift, calling .save on an existing object, will always send all keys to be updated, regardless of whether they are mutated? And this can't be avoided internally on the SDK side because of the way that POP works? If this is the case, I think we should mitigate the dirtyKeys on the server to minimise the potential confusion with cloud code, or unpredictable consequences of internal read-only keys becoming modified.

@dblythy dblythy changed the title All keys are marked as dirty using Parse Swift All keys are marked as dirty using Parse Swift, full body is sent when only one key is updated Sep 23, 2021
@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

If this is the case, I think we should mitigate the dirtyKeys on the server to minimise the potential confusion with cloud code, or unpredictable consequences of internal read-only keys becoming modified.

Im not sure what you mean here. Internal parse keys such as objectId, createdAt, etc. are always skipped when using the Swift SDK (unless when using customObjectId), if they get modified, it’s not because of the Swift SDK. Some of the other Client SDKs send the internal keys and depends on the server to strip or ignore them.

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

I see your server PR, makes sense and my guess is the trade off will be negligible when compared to the DB writes. The server does a lot of key checking already, the only possible improvement I can think of there is if you added your check to a place that was already checking keys to avoid another for loop, but I’m not sure if/where that place exists

@dblythy
Copy link
Member Author

dblythy commented Sep 23, 2021

The main concern for the server PR is to make sure cloud functions that use .dirty and dirtyKeys can be use as expected with ParseSwift

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 23, 2021

I think the PR I just opened with respect to this issue solves this problem and makes it pretty easy for the developer, server, and cloud code to handle dirty keys properly (server and cloud code due what they normally do). Essentially, the developer just has to:

  1. add .emptyObject when turning their ParseObject into a mutable object
  2. make modifications/mutations as they normally would do
  3. save as they normally would do

Let me know what you think...

@dblythy
Copy link
Member Author

dblythy commented Sep 27, 2021

That sounds like a good solution to me!

@mtrezza
Copy link
Member

mtrezza commented Sep 27, 2021

What are the risks for the developer when using emptyObject? I assume the risks mentioned in previous comments have not been eliminated.

@dblythy
Copy link
Member Author

dblythy commented Sep 27, 2021

My understanding is that if a developer uses emptyObject, they still need to compare the updated fields to the fields on fetch.

E.g,

  1. object is fetched
  2. text fields are entered, save is pressed
  3. Check if text !== originalObject.key
  4. Assign to emptyObject if the key is dirty
  5. Call .save on emptyObject
  6. Enumerate through emptyObject keys and set them to the original object

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 27, 2021

What are the risks for the developer when using emptyObject? I assume the risks mentioned in previous comments have not been eliminated.

what risk has emptyObject not eliminated?

@mtrezza
Copy link
Member

mtrezza commented Sep 27, 2021

I am referring to the comments against offering a mutable object. If the Parse SDK is now offering a way for a developer to implement this, do the arguments against a mutable object still stand?

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 27, 2021

My understanding is that if a developer uses emptyObject, they still need to compare the updated fields to the fields on fetch.

E.g,

  1. object is fetched
  2. text fields are entered, save is pressed
  3. Check if text !== originalObject.key
  4. Assign to emptyObject if the key is dirty
  5. Call .save on emptyObject
  6. Enumerate through emptyObject keys and set them to the original object

The Swift SDK doesn't work like this. If a developer is "enumerating through keys" they are using the SDK incorrectly, introducing unnecessary client overhead and processing, and possibly introducing errors that are unrelated to the SDK. The proper process of using emptyObject correctly is outlined here #243 (comment) and examples are in the playground files:

Regular ParseObject:

//: Define initial GameScores.
let score = GameScore(score: 10)
let score2 = GameScore(score: 3)
/*: Save asynchronously (preferred way) - Performs work on background
queue and returns to specified callbackQueue.
If no callbackQueue is specified it returns to main queue.
*/
score.save { result in
switch result {
case .success(let savedScore):
assert(savedScore.objectId != nil)
assert(savedScore.createdAt != nil)
assert(savedScore.updatedAt != nil)
assert(savedScore.ACL == nil)
assert(savedScore.score == 10)
/*: To modify, need to make it a var as the value type
was initialized as immutable. Using `emptyObject`
allows you to only send the updated keys to the
parse server as opposed to the whole object.
*/
var changedScore = savedScore.emptyObject
changedScore.score = 200
changedScore.save { result in
switch result {
case .success(var savedChangedScore):
assert(savedChangedScore.score == 200)
assert(savedScore.objectId == savedChangedScore.objectId)
/*: Note that savedChangedScore is mutable since it's
a var after success.
*/
savedChangedScore.score = 500
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

Current User:

/*: Save your first `customKey` value to your `ParseUser`
Asynchrounously - Performs work on background
queue and returns to specified callbackQueue.
If no callbackQueue is specified it returns to main queue.
Using `emptyObject` allows you to only send the updated keys to the
parse server as opposed to the whole object.
*/
var currentUser = User.current?.emptyObject
currentUser?.customKey = "myCustom"
currentUser?.score = GameScore(score: 12)
currentUser?.targetScore = GameScore(score: 100)
currentUser?.allScores = [GameScore(score: 5), GameScore(score: 8)]
currentUser?.save { result in
switch result {
case .success(let updatedUser):
print("Successfully save custom fields of User to ParseServer: \(updatedUser)")
case .failure(let error):
print("Failed to update user: \(error)")
}
}

Current Installation:

/*: Update your `ParseInstallation` `customKey` value.
Performs work on background queue and returns to designated on
designated callbackQueue. If no callbackQueue is specified it
returns to main queue. Using `emptyObject` allows you to only
send the updated keys to the parse server as opposed to the
whole object.
*/
currentInstallation = currentInstallation?.emptyObject
currentInstallation?.customKey = "updatedValue"
currentInstallation?.save { results in
switch results {
case .success(let updatedInstallation):
print("Successfully save myCustomInstallationKey to ParseServer: \(updatedInstallation)")
case .failure(let error):
print("Failed to update installation: \(error)")
}
}

There's no checking for dirty on the client-side or checking for any keys. You simply mutate the keys on emptyObject you want changed on the server and save. On the server side, you should do everything the same as you normally would. No special handling is needed unless you are creating your own way to handle data.

@dblythy
Copy link
Member Author

dblythy commented Sep 27, 2021

The Swift SDK doesn't work like this. If a developer is "enumerating through keys"

in the first example you showed, you set the required fields to emptyObject, and then after save set those fields on originalObject, which is the process I was explaining. If a developer wanted only updated keys to be assigned to emptyObject, they would add a simple if statement comparing new value to original object value.

If the developer doesn’t use an if statement to selectively assign the keys to emptyObject, then they may as well send the full object to the server.

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 27, 2021

I am referring to the comments against offering a mutable object. If the Parse SDK is now offering a way for a developer to implement this, do the arguments against a mutable object still stand?

Can you clarify what you are referring to? The Swift SDK has always offered mutable objects. The original concern from @dblythy was all keys were always sent to the server, no matter if their values were dirty/modified or not. My original response was developers could add a simple method to their ParseObjects to address this. The PR added the method for the developer to make the process easy. The Swift SDK still doesn't have a notion of dirty, because it isn't needed from the SDK. If a developer wants/needs dirty, they could do so when they implement their local storage option.

@mtrezza
Copy link
Member

mtrezza commented Oct 10, 2021

Maybe if you can give a specific problem statement, we can post it on Stack Overflow to get advice on how to implement the change from a wider audience. If we can't find a solution, I could approach someone from the Apple engineering team to discuss this.

@cbaker6
Copy link
Contributor

cbaker6 commented Oct 10, 2021

I'm assuming you are asking for yourself and @dblythy to come up with a problem statement since you both opened/labeled the issue. If you are asking me, not sure why I would make one when I don't see a current problem with the SDK design. Since you are both proposing the change, you should come up with it.

@mtrezza
Copy link
Member

mtrezza commented Oct 10, 2021

I mean a problem statement for why we cannot change the SDK to send only modified fields on save.

@cbaker6
Copy link
Contributor

cbaker6 commented Oct 10, 2021

That should be on you all, since you both believe the current solutions don't address the problem.

@dblythy
Copy link
Member Author

dblythy commented Oct 10, 2021

this isn’t an option for this SDK, if you both are for this you should commit to making your updates to the objective-c SDK as it’s designed for this

Ok, this is good constructive feedback as to why classes isn't the option.

So, it seems that if someone wanted to pick up this issue and submit a PR, the recommended approach would be to store objects on fetch?

As the Swift expert here Corey your view on the most optimal solution and downsides of any suggested changes is valuable, I've already stated how much I appreciate your work on this SDK and how easy and clean it is to used when compared to other SDKs. The focus here is on focusing on what is best for the community, and gracefully accepting constructive criticism (as per the code of conduct).

you both keep trying to suggest massive changes

Our discussions here are canvassing different options to make the SDK more efficient. We're trying to get to a solution as easy and quick to implement as possible, and @mtrezza and I are simply only discussing potential ideas to solve the problem at this stage.

Technical complexity aside, I think we all can agree that a more efficient .save will improve this SDK, making it easier and cheaper for developers. It might be that the change is too hard or too breaking that it’s not possible, but that’s why we’re having the discussion.

@dblythy
Copy link
Member Author

dblythy commented Oct 11, 2021

For context, it's also worth mentioning that using save for existing objects with highly scaled applications is problematic. Consider an app with hundreds of concurrent users who access the same Parse object, with two fields, a counter and a name field. Using operation.increment on a the counter field and operation.save will increment the counter irrespective of the objects' state when the client fetched it. This allows for all hundred users to concurrently increment the counter once, and the counter value to be 100.

However, if one client calls .save on the entire object, even for a single key update (such as changing the "name" field), all increment calls between the last fetch / save will be wiped. If the counter was 2 when the object was fetched, it will be hard reset to 2 in the database when .save is called.

This as well as scalable network costing concerns (especially for developers in regions with slower bandwidth capacity / networking costs), in my view, is the reason why this issue is relevant and the discussion is open. operation.set or a custom emptyObject is the current work-around, but I think working towards default behaviour similar to it would help the ease of use for this SDK, especially for concurrent and network conscious applications.

There's no where that says our SDKs should function the same, but simplicity across SDKs makes Parse Server attractive to developers. It's reasonable to assume each SDK has its own caveats, but I don't think developers expect the core interaction with the REST API to change across SDKs (where one SDK replaces the whole object using .save, and another only updates the changed keys). If cloud code has to be changed depending on the SDK, this only adds unnecessary complexity and confusion imo.

In summary:

Pros of changing .save:

  • Reduced payload size makes a cheaper Parse Server for developers who are charged per request payload
  • Reduced payload size makes a faster Parse Server for all developers
  • Marginally saves computing resources for all developers
  • Reduces database operations
  • Faster and cheaper makes Parse Server more accessible for developers in developing regions, not just regions with vast, high-speed networks.
  • Reduced potential for unwanted concurrency issues for objects that have multiple editors
  • Cross-SDK simplicity
  • Simplicity between saving and updating an object, .save can be used on both existing and new objects without having to consider side effects of .save on an existing object.
  • No unforeseen side effects in cloud code / Parse Server (such as the email verified bug which opened this issue)

Cons of changing .save:

  • Technically poses a challenge, could result in breaking changes which have consistently been a pain point for the community. The technical change could also have client performance implications.
  • Developers can just use .operation or emptyObject, no need to waste time developing a complicated .save solution when this time could be better focused elsewhere
  • Against the initial design philosophy of this SDK
  • If I have missed any here please let me know

I'm happy for operation to be the recommendation for updating objects, but I personally can't see the downside in reducing the payload size for updating a Parse Object using .save 🤷‍♂️

@mtrezza
Copy link
Member

mtrezza commented Aug 19, 2022

@cbaker6 You mentioned in #392 (comment) that this issue has been fixed, which would be great news.

It may help to untangle this discussion if you could provide a simple example here that shows what's needed to make this SDK only send the updated keys. This may help to condense the result of #315, which was merged after @dblythy's last comment here.

As we know, in other SDKs it's as simple as this:

const obj = new Parse.Object(`Example`);
obj.save();
obj.set(key, value);
obj.save();

What would the code look like for an equivalent example using the Swift SDK?

@cbaker6
Copy link
Contributor

cbaker6 commented Aug 19, 2022

It may help to untangle this discussion if you could provide a simple example here that shows what's needed to make this SDK only send the updated keys.

Synchronous

//: Save synchronously (not preferred - all operations on current queue).
let savedScore: GameScore?
do {
savedScore = try score.save()
} catch {
savedScore = nil
fatalError("Error saving: \(error)")
}
assert(savedScore != nil)
assert(savedScore?.objectId != nil)
assert(savedScore?.createdAt != nil)
assert(savedScore?.updatedAt != nil)
assert(savedScore?.points == 10)
/*: To modify, need to make it a var as the value type
was initialized as immutable. Using `mergeable`
allows you to only send the updated keys to the
parse server as opposed to the whole object.
*/
guard var changedScore = savedScore?.mergeable else {
fatalError("Should have produced mutable changedScore")
}
changedScore.points = 200
let savedChangedScore: GameScore?
do {
savedChangedScore = try changedScore.save()
print("Updated score: \(String(describing: savedChangedScore))")
} catch {
savedChangedScore = nil
fatalError("Error saving: \(error)")
}

Asynchronous

//: Define initial GameScores.
let score = GameScore(points: 10)
let score2 = GameScore(points: 3)
/*: Save asynchronously (preferred way) - Performs work on background
queue and returns to specified callbackQueue.
If no callbackQueue is specified it returns to main queue.
*/
score.save { result in
switch result {
case .success(let savedScore):
assert(savedScore.objectId != nil)
assert(savedScore.createdAt != nil)
assert(savedScore.updatedAt != nil)
assert(savedScore.points == 10)
/*: To modify, need to make it a var as the value type
was initialized as immutable. Using `mergeable`
allows you to only send the updated keys to the
parse server as opposed to the whole object.
*/
var changedScore = savedScore.mergeable
changedScore.points = 200
changedScore.save { result in
switch result {
case .success(let savedChangedScore):
assert(savedChangedScore.points == 200)
assert(savedScore.objectId == savedChangedScore.objectId)
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

User

/*: Save your first `customKey` value to your `ParseUser`
Asynchrounously - Performs work on background
queue and returns to specified callbackQueue.
If no callbackQueue is specified it returns to main queue.
Using `.mergeable` allows you to only send the updated keys to the
parse server as opposed to the whole object.
*/
var currentUser = User.current?.mergeable
currentUser?.customKey = "myCustom"
currentUser?.gameScore = GameScore(points: 12)
currentUser?.targetScore = GameScore(points: 100)
currentUser?.allScores = [GameScore(points: 5), GameScore(points: 8)]
currentUser?.save { result in
switch result {
case .success(let updatedUser):
print("Successfully saved custom fields of User to ParseServer: \(updatedUser)")
case .failure(let error):
print("Failed to update user: \(error)")
}
}

Installation

/*: Update your `ParseInstallation` `customKey` and `channels` values.
Performs work on background queue and returns to designated on
designated callbackQueue. If no callbackQueue is specified it
returns to main queue.
*/
var installationToUpdate = Installation.current?.mergeable
installationToUpdate?.customKey = "myCustomInstallationKey2"
installationToUpdate?.channels = ["newDevices"]
installationToUpdate?.save { results in
switch results {
case .success(let updatedInstallation):
print("Successfully saved myCustomInstallationKey to ParseServer: \(updatedInstallation)")
case .failure(let error):
print("Failed to update installation: \(error)")
}
}

@mtrezza
Copy link
Member

mtrezza commented Aug 19, 2022

So looking again at the JS example:

const obj = new Parse.Object(`Example`);
obj.save();
obj.set(key, value);
obj.save();

Would this be the complete equivalent in Swift or is there anything else needed?

struct Example: ParseObject {
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var key: Int?
}

let obj = Example()
obj.save()
var objMutable = obj.mergeable
objMutable.key = value
objMutable.save()

@cbaker6
Copy link
Contributor

cbaker6 commented Aug 19, 2022

//: Create your own value typed `ParseObject`.
struct GameScore: ParseObject {
//: These are required by ParseObject
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
var originalData: Data?
//: Your own properties.
var points: Int?
//: Implement your own version of merge
func merge(with object: Self) throws -> Self {
var updated = try mergeParse(with: object)
if updated.shouldRestoreKey(\.points,
original: object) {
updated.points = object.points
}
return updated
}
}

@mtrezza
Copy link
Member

mtrezza commented Aug 19, 2022

So let me try again,

struct Example: ParseObject {
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data? 
    var key: Int?

    func merge(with object: Self) throws -> Self { 
         var updated = try mergeParse(with: object) 
         if updated.shouldRestoreKey(\.key, 
                                      original: object) { 
             updated.key = object.key 
         } 
         return updated 
     } 
}

let obj = Example()
obj.save()
var objMutable = obj.mergeable
objMutable.key = value
objMutable.save()

Is that now the complete equivalent of this:

const obj = new Parse.Object(`Example`);
obj.save();
obj.set(key, value);
obj.save();

@cbaker6
Copy link
Contributor

cbaker6 commented Aug 19, 2022

It looks right

@mtrezza
Copy link
Member

mtrezza commented Aug 20, 2022

Is it correct that in order to send only updated keys to the server the developer must:

  • add the originalData property but doesn't have to interact with it
  • add the merge method but doesn't have to interact with it
  • when changing custom properties keep in mind to manually reflect those changes in the merge method
  • do that for every ParseObject class

Is your conclusion in #315 that there is no way to prevent that the developer has to implement and maintain all this overhead and instead build this into the SDK?

@cbaker6
Copy link
Contributor

cbaker6 commented Aug 20, 2022

add the originalData property but doesn't have to interact with it

originalData is added automatically to all ParseObject's by the compiler due to the protocol, just like objectId, etc. The developer doesn't need to interact with it

add the merge method but doesn't have to interact with it
when changing custom properties keep in mind to manually reflect those changes in the merge method

The developers override the default implementation of merge for each ParseObject. When overriding, they add their custom properties by using the additional helper methods like so and they never call merge themselves:

//: Implement your own version of merge
func merge(with object: Self) throws -> Self {
var updated = try mergeParse(with: object)
if updated.shouldRestoreKey(\.points,
original: object) {
updated.points = object.points
}
return updated
}
}

If the developer doesn't override merge, only the modified keys are sent to the server (assuming they mutated mergeable). They won't have the merged changes locally, instead, they will need to call fetch on the respective object to have the original and merged updates locally. Fetching will be required when using any of the REST based Parse SDK's if an application is doing what was mentioned here, #242 (comment). My response to that comment was/is Parse isn't designed out-of-the-box to handle that correctly, #242 (comment)

If a developer prefers, they can also use operations which looks more like your JS example, #268 (comment)

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2022

If the developer doesn't override merge, only the modified keys are sent to the server (assuming they mutated mergeable). They won't have the merged changes locally, instead, they will need to call fetch on the respective object to have the original and merged updates locally.

Can you confirm that you see no technical way to avoid the developer having to override the merge method to achieve the same behavior?

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 3, 2022

Can you confirm that you see no technical way to avoid the developer having to override the merge method to achieve the same behavior?

If I knew of a better way it would have been added in PR 316-400

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2022

Thanks, I've updated the Migration guide with a condensed version of this thread. Could you please review the code examples and suggest any changes you may want to see?

My suggestion would be to close this issue, as the bug has been addressed and it's now possible to send only the updated properties to the server, albeit with additional code and not by default. I would also move the point in the migration guide from "Known Issues" to "Behavioral Differences", as it's just the required difference in code we want to point out. At the same time I would open a "feature request" issue to track efforts to reduce the code overhead. PR #263 can then be linked to it as it suggests some approaches to achieve that.

@cbaker6
Copy link
Contributor

cbaker6 commented Sep 3, 2022

Thanks, I've updated the Migration guide with a condensed version of this thread. Could you please review the code examples and suggest any changes you may want to see?

  1. I don’t see a reason to have a “third” place to have code examples. Instead you should have added a “migration” section following the directions in Convert Playgrounds to DocC Interactive Tutorials #325
  2. I mention in 325 that all examples should be written in the “async await” form. There’s no reason to teach developers to make synchronous networking calls. In addition, synchronous networking calls in Apple OS hold up the main thread and Xcode will throw a ton of purple run time warnings along with creating a sluggish app. This encourages developers to come back to this repo and complain that it’s an issue with the SDK. The issue is their usage of the SDK that they learned from bad documentation. All examples should be asynchronous. If a developer uses synchronous frequently in the Objective-C app, chances are their app is sluggish,
  3. Multiple variants of Swift examples? There should only be one variant shown, encouraging developers to use mergeable. Showing multiple variants is encouraging bad decisions that will lead to open issues and complaints in this repo.

My suggestion would be to close this issue, as the bug has been addressed and it's now possible to send only the updated properties to the server, albeit with additional code and not by default. I would also move the point in the migration guide from "Known Issues" to "Behavioral Differences", as it's just the required difference in code we want to point out.

I’m fine with this

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2022

I don’t see a reason to have a “third” place to have code examples. Instead you should have added a “migration” section following the directions in Convert Playgrounds to DocC Interactive Tutorials

The examples are to explain the behavioral difference between ObjC and Swift SDK. I don't intend on writing DocC tutorials (because of time), but anyone is free to do so. For now it's important to have a central migration guide, rather than bits of information scattered across issues and PR threads. Even a simple txt file would be better than nothing at this point.

I mention in 325 that all examples should be written in the “async await” form. There’s no reason to teach developers to make synchronous networking calls.

This is a migration guide not a programming guide. It's not intended to teach when to use async/sync. There are use cases for both. There's a clear note that sync calls are used for simplicity. Feel free to open a PR to add async calls to the examples.

Multiple variants of Swift examples? There should only be one variant shown, encouraging developers to use mergeable.

The goal of the guide is to help developers transition their way of thinking coming from the ObjC SDK. Each variant gives context to lead them to the final recommended approach. That is described in the code comments. If you find a way to convey that info in 1 condensed example please feel free to open a PR.

Showing multiple variants is encouraging bad decisions that will lead to open issues and complaints in this repo.

We'll analyze why if that happens.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2022

If there's no objection from @dblythy I'll go ahead and close this issue and open a feature request as commented above.

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2022

Closing as limitation has been addressed; further improvement tracked in #401.

@mtrezza mtrezza closed this as completed Sep 3, 2022
@mtrezza mtrezza unpinned this issue Sep 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:bug Impaired feature or lacking behavior that is likely assumed
Projects
None yet
4 participants