Stop using SetAsync() to save player data

My problems with the argument for UpdateAsync() still stand as you have not provided an answer to any of them, merely echoed what was already said in the OP:

  • You say that it will help data be more up-to-date when a player teleports from place to place, but never explain how it does this. Unless it saves the data faster I fail to see how it helps at all.
  • Perhaps I’m being naive, but I don’t see why respecting previous data is at all important, or even possible in certain scenarios (such as mine where I’m saving complex inventories that can change vastly from save to save).
  • Being able to “cancel” the save seems useless when you can just not initiate a save in the first place.
  • If GetAsync() returns an old value, why wouldn’t UpdateAsync() pass an old value?
5 Likes

I feel like there is a really vital point being missed in this thread. If you use UpdateAsync the same way you would use SetAsync, meaning you only set data, almost all the value in using UpdateAsync is lost. UpdateAsync can be super useful, but to maximize its usefulness, your data structures need to revolve around changes and transitions in data. Consider the following cases:

  1. You are using SetAsync twice at the same time, for incrementing how many “points” a player has. Data has now been lost because it was overwritten. Ouch!

  2. You use UpdateAsync twice at the same time for the same reason, but you use something akin to a “data id” as mentioned in this thread. Data has now been lost because an entire set of data was rejected, because it had the wrong id. Ouch!

How do we solve this? By locally storing what has changed, and then using UpdateAsync to commit those changes. When a player gains points, use UpdateAsync, and increment the old value, retrieved from the UpdateAsync callback, by the amount changed. This is what the entire purpose of UpdateAsync is, because it gives you the most recent data in the callback. You can use this on complex tables, all you need is the correct implementation. And if you want optimal data safety, you should.

slight edits for wording

18 Likes

Something I’ve suggested to devs before is to use a “First time playing!” badge or something of the sorts, which is given after you first SetAsync (successfully) that player’s data, then whenever you GetAsync, if they have the badge but no data then there must be an error with the data-stores, so handle that appropriately. This could alleviate data-loss if that is a concern.

5 Likes

I’m quite sure I did. Throughout the post, I’ve mentioned that one of the key differences between Set and Update are how they operate - said that a few times.

GetAsync caches. This means that the data you give the DataStore from SetAsync may not necessarily be the same thing that you get from another GetAsync. On the other hand, as UpdateAsync is a callback, not only does it update the data in the DataStore but it also returns the new value that was saved to the DataStore. Even if GetAsync caches at this point, you can use the return from UpdateAsync instead of spending another request on GetAsync.

A place-to-place teleport is a very specific scenario I brought up as a preference, considering I manipulate data fairly often in said project. Multiple servers and places have the opportunity to manipulate data, therefore I don’t want calls to conflict and save the wrong data for players.

The speed difference between SetAsync and UpdateAsync is heavily negligible and not something worth bringing to the table when discussing this. It’s mainly about the practice of which one to use in what scenario, why and what the merits or caveats of each method are.

It’s not just about respecting previous data, it’s also about respecting conflicting calls. UpdateAsync also calls the transform function as many times as needed and ensures that data saves in the DataStore. SetAsync does not do this - it is a one and done, with nothing to be returned.

While this is a fair point in itself that you can avoid initiating a save, it’s not at all useless. Avoiding using SetAsync if certain conditions do not pass is the same as passing nil in UpdateAsync. That doesn’t make it useless. It’s the same method with a different implementation.

I believe that a cancelled UpdateAsync does not spend any budget. Correct me if I’m wrong about that.

I’m not too sure what you mean by this. Both pass the value from the DataStore. GetAsync can cache the result, UpdateAsync returns a live result to be used with the transform function. When the update is finished its call, it returns what is currently in the DataStore.


This all in mind, your implementation is first and foremost the most important thing between distinguishing which one is best for your use case or if there is any difference at all. I don’t want to parrot anything, though filip’s post above is something to consider.

6 Likes

So would calling UpdateAsync() every time a change is made (e.g. adding a new item, increment their cash) to the player’s data (complex table) be better than doing a one time SetAsync() once the player leaves/auto-saving with SetAsync()?

I would assume yes but I just want to confirm.

1 Like

It depends on your usage. You said you only wanted to save when the player leaves, or automatically every X seconds. SetAsync is completely fine for this. You can also use UpdateAsync.

2 Likes

Not at all. Calling any DataStore methods every time a change is made is bad practice, regardless of which write method you use. What you should be doing is caching data in ModuleScripts (or ValueObjects at the cost of memory, efficiency and more) designed to handle data for the play session.

All data should be held somewhere. When you need to update data, you use a method that the ModuleScript exposes to write to a data table. After that, you should be using DataStore methods minimally for various cases (saving [leaving, autosave, manual save, after purchases], loading [first join, refreshing], writing [deleting data], etc).

This all being said, your second use case - when a player leaves or the game autosaves - is completely valid and fine.

Always try to keep your production in a Lua environment and be mindful of DataStore limitations. Saving every change is bad practice and will undoubtedly lead to throttling.

8 Likes

Ah okay, thanks for the clarification.

Hi, is this tutorial still recommended to use?

And is it better than data store2?

If you’re less familiar with lua I’d recommend using DataStore2 as they handle a lot of the technical work for you, otherwise feel free to utilise this tutorial (which is up-to-date but requires a bit more know-how).

1 Like

I’m quite familliar with Lua, but not as much with Data stores.

My only problem is that the Data Store 2 tutorial is not very in-depth tutorial.

Anyways, i’ll try to learn Data Store 2 a little more.

Thanks for taking time to answer me, big fan of you and your work!

1 Like

I share your disgust with DataStores and appreciate the talk about using them wisely. I agree the hardest thing to work with is DataStores simply because they are not reliable.

1 Like

Lots of people talking about the downsides of GetAsync, but has anyone mentioned that GetAsync caches values yet? That could introduce some seriously bad bugs if mishandled.

3 Likes

I personally suggest utilizing a system that is able to detect if datastore had issues loading data so you can make sure data doesn’t overwrite if somebody’s data fails to load. I had this issue in a game awhile back where my data failed to load in and when I left and came back a day later, I found my entire game to be reset back to the beginning even with all my purchases made.

I cannot fret how important it is to make sure that you don’t accidentally overwrite data. I genuinely ended up leaving that game in a storm of frustration. The developer was also unreachable for contact.

3 Likes

Never realized how dangerous my code really was! Great tutorial, definitely going to go back and rewrite my datastores!

1 Like

Don’t really see how this could work within my code frame, as I only use SetAsync when a player leaves. I don’t want to have to use UpdateAsync on every single piece of data that may need updating (levels, exp, currency, items they own, etc. Seems much much easier to just go

PlayerDataStore:SetAsync(player.UserId, PlayerData[player.UserId])

Never had problems with it before

Do you know an dev desire this mate? Obviously no.
Can’t agree more.

Can’t agree more mate this is truly the biggest worst what a game can have mate.

I am bringing this topic back up because I want to know what is DataId? And what do we have to set it to in the playersData table (Assuming playersData is a table)?

DataId is just a value that increments to show that your data has changed since you last saved it to the DataStore to prevent overwriting new data with old data. Notice how in the :UpdateAsync() callback, you ONLY return playersData if their DataIds are equal.

1 Like

How can I use this with multiple different data points?

return {
	-- Main Data
	Cash = 0,
	
	FavouriteColor = {83, 209, 255},
	
	Codes = {},
	
	-- House Data
	House = 'Default',
	
	Houses = {
		['Default'] = {
			Exterior = {},
			Interior = {},
			Furniture = {}
		},
	},
	
	Furniture = {},
	
	-- Character Data
	Character = {
		Name = '',
		Age = 'Adult',
		HatAccessories = {},
		HairAccessories = {},
		FaceAccesssories = {},
		NeckAccessories = {},
		ShoulderAccessories = {},
		BackAccesssories = {},
		WaistAccesssories = {},
		Face = '',
		Shirt = '',
		Pants = '',
		Color = 'Light orange',
	},
}

Cause then I’d have to do this…

function DataManager.UpdateCash(player, amount)
	local User = PlayerData[player.UserId]
	if not User then return end
	
	if User then
		PlayerDataStore:UpdateAsync(player.UserId, function(oldValue)
			local previousData = oldValue or {Cash = 0}
			if User.Cash == previousData.Cash then
				User.Cash = User.Cash + amount
				
				return User
			else
				return nil
			end
		end)
	end
end

function DataManager.UpdateFavouriteColor(player, newColor)
	local User = PlayerData[player.UserId]
	if not User then return end
	
	if User then
		PlayerDataStore:UpdateAsync(player.UserId, function(oldValue)
			local previousData = oldValue or {FavouriteColor = {0, 0, 0}}
			if User.FavouriteColor == previousData.FavouriteColor then
				User.FavouriteColor = newColor
				
				return User
			else
				return nil
			end
		end)
	end
end

function DataManager.UpdateHouse(player, newHouse)
	local User = PlayerData[player.UserId]
	if not User then return end
	
	if User then
		PlayerDataStore:UpdateAsync(player.UserId, function(oldValue)
			local previousData = oldValue or {House = 'Default'}
			if User.House == previousData.House then
				User.House = newHouse
				
				return User
			else
				return nil
			end
		end)
	end
end
-- and etc..... forever and ever and ever.....
2 Likes