DataStore: SetAsync() vs. UpdateAsync()

Greetings! I store my player data in a dictionary. This dictionary contains things such as level, experience, items the player has, etc. When a player enters the game, I create a default dictionary or get one from GetAsync() that looks like this:

local Stats = {
 ["A"] = {"a1" = 1, "a2" = 2},
 ["B"] = {"b1" = 1, "b2" = 2}
}

Later on, after some changes are made to it (level increased, cash value changed, new items added, etc.), we have this:

local Stats = {
 ["A"] = {"a1" = 67, "a2" = 55, "a3" = 5},
 ["B"] = {"b1" = 0, "b2" = 1, "b3" = 1000, "b4" = 12}
}

Now I need to save this updated dictionary (when PlayerRemoving(), BindToClose() and so on).

Probably my best (only) option is SetAsync()? Some people say that UpdateAsync() should be used instead. But it’s probably pointless in this case since, as @colbert2677 mentioned in a number of previous posts, …

DataStore.UpdateAsync(Key, function(OldStats)
    return Stats
end)

… is equal to …

DataStore:SetAsync(Key, Stats)

… ? Or, maybe, there’s something more in it? 1) In other words, is there any benefit to using UpdateAsync() when trying to save a dictionary? I am quite new to scripting, but the only thing that comes to mind is to check if OldStats is equal to Stats (return nil to cancel saving), or, I dont know, refuse to save (return nil) if Stats equals to nil.

Also - 2) when choosing a method to save data, is it considered good practice to create a dictionary, do things with it (on Server), and just brutally rewrite entire data, that is, replace OldStats (old version) with Stats (new version) by using SetAsync() or UpdateAsync()?

1 Like

Idk, but I always save dictionary, yes you should use SetAsync.

I give you advice to use DataStore2 with cache system.

Thank you for your response! Is it likely that there is no advantage to using UpdateAsync () in this case?

If you aren’t directly mutating or using the previous data then there’s a good chance (but not a guarantee) you aren’t making use of the full benefits of UpdateAsync. UpdateAsync being used like SetAsync isn’t the exact same since they’re both implemented differently but they’re similar.

UpdateAsync essentially “puts more thought” into what it’s doing when saving data before doing so - respecting other servers’ attempts to manipulate the same key, retrying until the data is set and allow the developer to manage how data gets written, up to cancelling the write. It also returns the data you’ve written to the DataStore since it’s a callback - some systems may need this to avoid getting trapped into using GetAsync and SetAsync where not necessary.

SetAsync, on the other hand, is comparable to attempting to change the value of a key in a dictionary. It puts no thought into data management, only giving the key-value pair to the data service and then dropping right after. It attempts no validation, does not ensure your data is properly saved and it doesn’t return anything. SetAsync realistically should only be used if you need to force a change, otherwise UpdateAsync is canonical for all changes. Even the name suggests as such.

Though it’s a more complex example, ProfileService only uses UpdateAsync when interfacing with DataStores (no GetAsync, no SetAsync). Here’s the standard transform function it gives to UpdateAsync. Essentially UpdateAsync is called whenever DataStores are required and if edits are required then they’re committed to the latest data (value passed to UpdateAsync) before returning the data. It also has various settings it can read off of to help it determine what to do within the UpdateAsync. You get fetching and saving all at once (UpdateAsync can technically be used as a Get and a Set in one call, which ProfileService awesomely does).

DataStore2 also uses UpdateAsync when writing data as opposed to SetAsync, though unlike ProfileService it does not hybrid UpdateAsync as a getter and a setter so it does feature GetAsync. Check SavingMethods. This is not a bad thing however, as DataStore2 is also reputable and battle-tested in production-level games with large traffic. DataStore2 was initially the “solution” a lot of developers chose and still choose to work with data in their games.

There’s absolutely benefit in using UpdateAsync over SetAsync. I do recall it was a point of contention at one time and I too didn’t have too much knowledge on it but when you figure out some neat use cases for it it’s a pretty helpful function to apply. Generally it’ll all depend on your exact systems and needs so you don’t have to use UpdateAsync however I strongly recommend it in all cases.


That being said, your questions.

For question 1:
UpdateAsync is very flexible and can cover your use cases whether or not you’re using a dictionary, however I would more strongly recommend UpdateAsync for dictionaries and typically most data you’ll ever need to save will be dictionaries. For non-table data, I don’t really recommend UpdateAsync because you probably don’t need the whole line of benefits just to ensure a single value gets saved.

You could make your own wrapper with SetAsync but you’ll have to do a bit more work and problem solving to get that wrapper ready for good use. It’s ultimately not worth it for what you can get with UpdateAsync and this is just from talking about the developer-side, not even getting into how internally different SetAsync and UpdateAsync are.

For question 2:
Since UpdateAsync passes the latest saved value as an argument to the function, you should try and make good use of that in your transform function. You can just hard-write the data by returning the new data and doing nothing else but consider if that’s the best approach or if you need anything else. Additionally because you can cancel the write by returning nil, this further incentivises using the old data in your transform function to determine how data should get written/modified.

Never SetAsync. SetAsync doesn’t care and isn’t mindful. It takes what you give it and throws it at the data service without looking back. UpdateAsync will get your data and ask you what you want to do with it. You can then tell it that it doesn’t need to ask the data service to write anything or you can ask it to make a few changes before carefully ensuring that what you told it to save does in fact save and then even giving back the newly saved data so you can take that home to other code in your game.


Might’ve repeated a few things unintentionally but good for emphasis. Hope the last analogy of “SetAsync the uncaring” and “UpdateAsync the mindful” helped you further consider which to use.

13 Likes

Your response is treasure, thank you! In my case, it’s probably a good idea to use UpdateAsync() - it asks me how I want the old data to be changed. When the old data is …

local Stats = {
 ["A"] = {"a1" = 1},
 ["B"] = {"b1" = 1}
}

…, and the new data is …

local Stats = {
 ["A"] = {"a1" = 2},
 ["B"] = {"b1" = 1, "b2" = 1}
}

…, we see that the changes needed are: 1) a1 value increased (from 1 to 2); 2) new item b2 was added. So, instead of simply rewriting, I should explain to the UpdateAsync() how I want old data changed? Some values can only increase (level), others increase and decrease (cash), numerous new (b1, b2, … b99) items can be added, existing ones can’t get deleted. But how can I explain this to a computer (convert to LUa)? And is this explanation needed?

It would be absolutely swell if you could translate the new data into changes on the old one but sometimes that can get a bit understandably complicated especially for larger data sets and sometimes you do have to settle for just overwriting tables. It really depends on the scope of your project.

Developers typically employ caching behaviour to move updates from UpdateAsync over to the script environment. That way all changes you commit are on the data itself but for the current game session and are only passed along to UpdateAsync when the game session ends (by teleporting, leaving, shutdown, so on). Acceptable too - hope I didn’t only imply that mutating old data is the proper way to go about things, I can be a little tacky with wording. :stuck_out_tongue_closed_eyes:

In the case that you suggest so far, you can opt for two things:

  • Depending on how complicated you intend to make your data (how many entries?), you can ditch the old data set, make modifications to the new one if you need any and then return that.
  • If your data set is as simple as a few key-value pairs that don’t change deeply, you can modify the old values. Level-Cash is a good example of where you can directly change. The items could be changeable like that too.
-- Either of these are ok as simple updates!

local function transformFunction(oldData)
    for key, value in pairs(newData) do
        -- Reconciles and doesn't update unchanged keys, one layered
        if not (oldData[key] == newData[key]) then
            oldData[key] = value
        end
    end

    return oldData
end

local function transformFunction(oldData)
    return newData
end

Speaking from a general perspective, the best way to articulate myself here is that UpdateAsync has a lot of benefits over SetAsync internally and because of what you can do with it, given that it provides you the currently saved data. Your system will highly determine if you want to use the old data to influence how the new data gets changed or if you just want to settle for something simple for now.

1 Like

I think I finally understand it now! I hope this topic will be useful to other developers as well - it’s probably one of the best explanations of the difference between SetAsync() and UpdateAsync() out there.

One more thing that confuses me! When a player joins the game for the first time, there is no (old) data for this player yet. Can I use UpdateAsync() instead of SetAsync() in this situation? Or is it required to use SetAsync() for the first time, and UpdateAsync() only when there there is old data?

Yep, you can still use UpdateAsync for new players. The old data parameter will be nil so you’ll have to return them with some fresh data.

2 Likes