Need Help Storing multiple Properties for one value using Datastore? I think I can help

Why might I need this?
It’s possible that this might be useful if you want to store more than one value for say a player’s items that have multiple stats that can be different for each player. For this tutorial we will be using vanilla datastore and will be storing the two items, A pickaxe and a sword. and we will be storing two properties for each one, Strength and Wear.

Define the datastore
the easiest part
Ok so first just define the datastore that we’ll be saving this to, we’ll call ours inventory. Here’s the code

game:GetService("DataStoreService"):GetDataStore("2wef9asd32a3a278") --characters datastore

Alright now that we’ve done the easy part. It’s time to move onto the hard part.

How to Save the values
Ok so first you have to define the folders that we will be saving the and writing the values to

game.Players.PlayerAdded:Connect(function(player)--when the player is added
	local folder = Instance.new("Folder", player)--creates a character folder
	folder.Name = "Tools"--names the folder
	
	local savedData = nil --sets datastore to nil by default 

    savedData = dataStore:GetAsync(player.UserId) --sets saved data varible to the players datastore location

Ok now that we have the folders we’ll be saved to and written to lets write code to get the datastore that we write the values used in from

game.Players.PlayerAdded:Connect(function(player)--when the player is added
	local folder = Instance.new("Folder", player)--creates a character folder
	folder.Name = "Tools"--names the folder
	
	local savedData = nil --sets datastore to nil by default 

    savedData = dataStore:GetAsync(player.UserId) --sets saved data varible to the players datastore location

    if savedData ~= nil then -- if theres value in the datastores
		local lastIt = Instance.new("IntValue",player)--the variable that stores the index values of the last indexed item
		lastIt.Value = 1 --the default item to see if the index value is the first number that can be a item
		local llaassttIItt = Instance.new("StringValue",player)--The name of the last indexed item
		print("returning player")--prints that the player has been here before(Not neccessary, but nice for testing)
		print(savedData) --prints datastore's value(also not neccessary, but nice for testing)
		for i,v in pairs(savedData) do --gets the datastore's values
			if i == lastIt.Value + 2 then --checks if the indexed value is the second indexed number from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with the strength value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Strength" --names the Value Strength
				B.Value = v --writes the value with the indexed value of the saved data table		
			elseif i == lastIt.Value + 1 then--checks if the indexed value is the first indexed number away from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with a wear value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Wear" --names the Value HP 
				B.Value = v --writes the value with the indexed value of the saved data table	
			elseif i == lastIt.Value or i == lastIt.Value + 3 then --checks if the indexed value is the third indexed number from the character or at least the first index value that should appear
				local B = Instance.new("StringValue")-- creates a value to overwrite with the name value
				B.Parent = player.Tools--parents it to the Tools folder
				B.Name = v --names the Value the name of the Item
				B.Value = v --writes the value with the indexed value of the saved data table	
				lastIt.Value = i--changes the last indexed value to it's indexed value
				llaassttIItt.Value = v--changes the last item value to it's value(so other values can know what it needs to be parented to)
			end
			print(i)
		end

Testing the values
Now that we have the the values that we will write to, let’s create the values that we’ll feed to the compiler to test the saving of the value. For the sake of this example we’ll have it set to create these values if you have never played before. This can be different for all games, but since this isn’t a tutorial on a full inventory system I decided I’d use a different approach as it still teaches the main subject matter and is much easier to program. You can get full inventory tutorials elsewhere.

game.Players.PlayerAdded:Connect(function(player)--when the player is added
	local folder = Instance.new("Folder", player)--creates a character folder
	folder.Name = "Tools"--names the folder
	
	local savedData = nil --sets datastore to nil by default 

    savedData = dataStore:GetAsync(player.UserId) --sets saved data varible to the players datastore location

    if savedData ~= nil then -- if theres value in the datastores
		local lastIt = Instance.new("IntValue",player)--the variable that stores the index values of the last indexed item
		lastIt.Value = 1 --the default item to see if the index value is the first number that can be a item
		local llaassttIItt = Instance.new("StringValue",player)--The name of the last indexed item
		print("returning player")--prints that the player has been here before(Not neccessary, but nice for testing)
		print(savedData) --prints datastore's value(also not neccessary, but nice for testing)
		for i,v in pairs(savedData) do --gets the datastore's values
			if i == lastIt.Value + 2 then --checks if the indexed value is the second indexed number from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with the strength value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Strength" --names the Value Strength
				B.Value = v --writes the value with the indexed value of the saved data table		
			elseif i == lastIt.Value + 1 then--checks if the indexed value is the first indexed number away from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with a wear value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Wear" --names the Value HP 
				B.Value = v --writes the value with the indexed value of the saved data table	
			elseif i == lastIt.Value or i == lastIt.Value + 3 then --checks if the indexed value is the third indexed number from the character or at least the first index value that should appear
				local B = Instance.new("StringValue")-- creates a value to overwrite with the name value
				B.Parent = player.Tools--parents it to the Tools folder
				B.Name = v --names the Value the name of the Item
				B.Value = v --writes the value with the indexed value of the saved data table	
				lastIt.Value = i--changes the last indexed value to it's indexed value
				llaassttIItt.Value = v--changes the last item value to it's value(so other values can know what it needs to be parented to)
			end
			print(i)
		end
    else --the saved data has no value
        print("New Player")
		local g = Instance.new("StringValue", folder)--new string value for the
			g.Name = "Pickaxe"--pickaxe.
			g.Value = "Pickaxe"--just adding a value to add more to further declare it as such(Not Necessary)
			g.Parent = folder --(Parents it to the items folder)
		local f = Instance.new("StringValue", folder)--{
			f.Name = "Sword"--{Same Thing
			f.Value = "Sword"--{Here as the last one but with different values
			f.Parent = folder--{
		local Heg = Instance.new("IntValue")--creates an intval for the property
			Heg.Name = "Strength"--strength.
			Heg.Parent = g--parents it to an item 
			Heg.Value = 10 --Arbitrary Value
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Strength"--{same thing here but it parents it to a 
			Hevg.Parent = f --{ different item                                                        
		Hevg.Value = 6 --{
		local Heg = Instance.new("IntValue")--{
			Heg.Name = "Wear"--{Same thing here but
			Heg.Parent = g--{a different property name
			Heg.Value = 8--{
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Wear"--{
			Hevg.Parent = f --{you get the gist at this point                                                         
			Hevg.Value = 5--{
	end
end)

Saving the Values
ok now onto the last and undeniably easier parts, but still can be quite hard which is saving the data. Now datastores can’t store folders since it can only store UTC-8 Characters so we’ll have to store values in a table then feed the values in that table to the datastore so it can put it in it’s own table to be indexed later.

game.Players.PlayerAdded:Connect(function(player)--when the player is added
	local folder = Instance.new("Folder", player)--creates a character folder
	folder.Name = "Tools"--names the folder
	
	local savedData = nil --sets datastore to nil by default 

    savedData = dataStore:GetAsync(player.UserId) --sets saved data varible to the players datastore location

    if savedData ~= nil then -- if theres value in the datastores
		local lastIt = Instance.new("IntValue",player)--the variable that stores the index values of the last indexed item
		lastIt.Value = 1 --the default item to see if the index value is the first number that can be a item
		local llaassttIItt = Instance.new("StringValue",player)--The name of the last indexed item
		print("returning player")--prints that the player has been here before(Not neccessary, but nice for testing)
		print(savedData) --prints datastore's value(also not neccessary, but nice for testing)
		for i,v in pairs(savedData) do --gets the datastore's values
			if i == lastIt.Value + 2 then --checks if the indexed value is the second indexed number from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with the strength value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Strength" --names the Value Strength
				B.Value = v --writes the value with the indexed value of the saved data table		
			elseif i == lastIt.Value + 1 then--checks if the indexed value is the first indexed number away from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with a wear value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Wear" --names the Value HP 
				B.Value = v --writes the value with the indexed value of the saved data table	
			elseif i == lastIt.Value or i == lastIt.Value + 3 then --checks if the indexed value is the third indexed number from the character or at least the first index value that should appear
				local B = Instance.new("StringValue")-- creates a value to overwrite with the name value
				B.Parent = player.Tools--parents it to the Tools folder
				B.Name = v --names the Value the name of the Item
				B.Value = v --writes the value with the indexed value of the saved data table	
				lastIt.Value = i--changes the last indexed value to it's indexed value
				llaassttIItt.Value = v--changes the last item value to it's value(so other values can know what it needs to be parented to)
			end
			print(i)
		end
    else --the saved data has no value
        print("New Player")
		local g = Instance.new("StringValue", folder)--new string value for the
			g.Name = "Pickaxe"--pickaxe.
			g.Value = "Pickaxe"--just adding a value to add more to further declare it as such(Not Necessary)
			g.Parent = folder --(Parents it to the items folder)
		local f = Instance.new("StringValue", folder)--{
			f.Name = "Sword"--{Same Thing
			f.Value = "Sword"--{Here as the last one but with different values
			f.Parent = folder--{
		local Heg = Instance.new("IntValue")--creates an intval for the property
			Heg.Name = "Strength"--strength.
			Heg.Parent = g--parents it to an item 
			Heg.Value = 10 --Arbitrary Value
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Strength"--{same thing here but it parents it to a 
			Hevg.Parent = f --{ different item                                                        
		Hevg.Value = 6 --{
		local Heg = Instance.new("IntValue")--{
			Heg.Name = "Wear"--{Same thing here but
			Heg.Parent = g--{a different property name
			Heg.Value = 8--{
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Wear"--{
			Hevg.Parent = f --{you get the gist at this point                                                         
			Hevg.Value = 5--{
	end
end)

game.Players.PlayerRemoving:Connect(function(player) --when the player leaves the game
	local save = {} --table to save data
	
	for _, Child in pairs(player.Tools:GetChildren()) do --gets the tool storage folder
		if Child and dataStore ~= nil then --if the child exists and the datastore has value
			table.insert(save, Child.Value) --inserts the item name into the save table
			table.insert(save, Child:FindFirstChild("Strength").Value) --inserts the item's strength into the save table
			table.insert(save, Child:FindFirstChild("Wear").Value)--inserts the item's Wear into the save table
		end
	end
	print(save)--prints the table(Not necessary and has no real use outside of testing)
	dataStore:SetAsync(player.UserId, save) --saves the table's values into the datastore
end)

One last Thing
In order to prevent data loss from a server shutdown, we’ll just set the server to wait from fully closing for just a second while it wraps everything up. You may notice that when testing your game it’ll take a few seconds before debugging officially stops. This is fine, this is just once again the server wrapping up all of it’s unfinished business before heading out.

game.Players.PlayerAdded:Connect(function(player)--when the player is added
	local folder = Instance.new("Folder", player)--creates a character folder
	folder.Name = "Tools"--names the folder
	
	local savedData = nil --sets datastore to nil by default 

    savedData = dataStore:GetAsync(player.UserId) --sets saved data varible to the players datastore location

    if savedData ~= nil then -- if theres value in the datastores
		local lastIt = Instance.new("IntValue",player)--the variable that stores the index values of the last indexed item
		lastIt.Value = 1 --the default item to see if the index value is the first number that can be a item
		local llaassttIItt = Instance.new("StringValue",player)--The name of the last indexed item
		print("returning player")--prints that the player has been here before(Not neccessary, but nice for testing)
		print(savedData) --prints datastore's value(also not neccessary, but nice for testing)
		for i,v in pairs(savedData) do --gets the datastore's values
			if i == lastIt.Value + 2 then --checks if the indexed value is the second indexed number from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with the strength value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Strength" --names the Value Strength
				B.Value = v --writes the value with the indexed value of the saved data table		
			elseif i == lastIt.Value + 1 then--checks if the indexed value is the first indexed number away from the indexed item
				local B = Instance.new("IntValue")-- creates a value to overwrite with a wear value
				B.Parent = player.Tools[llaassttIItt.Value]--parents it to the last item indexed
				B.Name = "Wear" --names the Value HP 
				B.Value = v --writes the value with the indexed value of the saved data table	
			elseif i == lastIt.Value or i == lastIt.Value + 3 then --checks if the indexed value is the third indexed number from the character or at least the first index value that should appear
				local B = Instance.new("StringValue")-- creates a value to overwrite with the name value
				B.Parent = player.Tools--parents it to the Tools folder
				B.Name = v --names the Value the name of the Item
				B.Value = v --writes the value with the indexed value of the saved data table	
				lastIt.Value = i--changes the last indexed value to it's indexed value
				llaassttIItt.Value = v--changes the last item value to it's value(so other values can know what it needs to be parented to)
			end
			print(i)
		end
    else --the saved data has no value
        print("New Player")
		local g = Instance.new("StringValue", folder)--new string value for the
			g.Name = "Pickaxe"--pickaxe.
			g.Value = "Pickaxe"--just adding a value to add more to further declare it as such(Not Necessary)
			g.Parent = folder --(Parents it to the items folder)
		local f = Instance.new("StringValue", folder)--{
			f.Name = "Sword"--{Same Thing
			f.Value = "Sword"--{Here as the last one but with different values
			f.Parent = folder--{
		local Heg = Instance.new("IntValue")--creates an intval for the property
			Heg.Name = "Strength"--strength.
			Heg.Parent = g--parents it to an item 
			Heg.Value = 10 --Arbitrary Value
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Strength"--{same thing here but it parents it to a 
			Hevg.Parent = f --{ different item                                                        
		Hevg.Value = 6 --{
		local Heg = Instance.new("IntValue")--{
			Heg.Name = "Wear"--{Same thing here but
			Heg.Parent = g--{a different property name
			Heg.Value = 8--{
		local Hevg = Instance.new("IntValue")--{
			Hevg.Name = "Wear"--{
			Hevg.Parent = f --{you get the gist at this point                                                         
			Hevg.Value = 5--{
	end
end)

game.Players.PlayerRemoving:Connect(function(player) --when the player leaves the game
	local save = {} --table to save data
	
	for _, Child in pairs(player.Tools:GetChildren()) do --gets the tool storage folder
		if Child and dataStore ~= nil then --if the child exists and the datastore has value
			table.insert(save, Child.Value) --inserts the item name into the save table
			table.insert(save, Child:FindFirstChild("Strength").Value) --inserts the item's strength into the save table
			table.insert(save, Child:FindFirstChild("Wear").Value)--inserts the item's Wear into the save table
		end
	end
	print(save)--prints the table(Not necessary and has no real use outside of testing)
	dataStore:SetAsync(player.UserId, save) --saves the table's values into the datastore
end)

game:BindToClose(function() --when the game shuts down
	for i, player in pairs(game.Players:GetPlayers())do --gets the player list
		if player then --if the player exists
			player:kick("The game is shutting down, please rejoin later") --kick message
		end
	end

	wait(2)--wait to make sure the data saves
end)

Fin
And there you go let’s just test our final script out real quick


Ignore the first line that’s for a separate script, As you can see it recognizes me as someone who has no value in the data store. And when I return I’ll get…

…this…
Screenshot 2021-07-16 015526
…and this

That’s it. I hope that this tutorial helped someone I made this because I had to make a way to store a player’s character’s levels and it took forever to figure out how. It seems like there isn’t much documentation about storing multiple of a player’s Item’s properties in a datastore. Thanks to my coworker @LUMINADY1 who helped me out on getting it to work. And this is my first tutorial so I hope I did well on informing y’all on how to do this. Bye.

4 Likes

Why not just save a value as a table, then just add the properties to the table instead of having hard-coded indexes (which may or not break at one point in the future) ?

That also allows you to loop thru every table and create the correct value instances for them instead of having to create them manually.

but this is saved as a table, that’s the whole point of it, the only thing that’s hard coded is just what types of values have to be saved to the table. In the end looping through a table is exactly what my program does. It can handle instances that weren’t there before, that’s the entire point of it

1 Table for 1 Key, not 1 big table.

even if we do a table for every key, how are we supposed to index each value and know what property type it is but also what Item it belongs to

It’s saved as a mixed table which is a really bad idea overall. Roblox doesn’t like mixed tables very much when it comes to work and in practice it’s not clean to use either.

You have the right idea of encouraging developers to save in tables if they need to save multiple values but not in this manner. This is prone to various maintenance and organisation issues that can be resolved by doing it properly. Doubly so, you can get a nice implementation out of it.

Some things that you will want to do:

  • First off, divorce the anonymous function from PlayerAdded and connect it later, then loop across all players in the game and call it. This ensures that players who join ahead of PlayerAdded connecting will properly get the connection ran for them.

  • Wrap your DataStore calls in pcall. DataStore requests are web calls so you also need to ensure that the endpoints are actually working. If you don’t account for DataStore failures this code will cause data loss. Always pcall DataStore calls.

  • Divorce the creation of leaderboard stats from the status of the GetAsync call. Unless your data is purely abstract then it should be used to modify an existing structure rather than be what determines the structure. If your data defines your structure and not how the existing structure should be modified it may be difficult to refactor data later.

  • The parent argument should be the last thing set. Make sure all the object’s properties and children are created first before you parent the folder.

With this in mind, you can create a new rough code sample:
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

-- One DataStore for player data. 4MB is more than enough.
local PlayerDataStore = DataStoreService:GetDataStore("PlayerData")

-- Assuming tools are consistent, just gonna go ahead and abstract away the
-- process to create them. Don't want to write it several times over.
local function createToolStats(toolName, toolFolder)
	local tool = Instance.new("Folder")
	tool.Name = toolName

	local strength = Instance.new("IntValue")
	strength.Name = "Strength"

	local wear = Instance.new("IntValue")
	wear.Name = "Wear"

	strength.Parent = tool
	wear.Parent = tool

	tool.Parent = toolFolder

	return tool
end

-- We're going to serialise our hierarchy into a dictionary rather than
-- a mixed table. This will make it easier and predictable to read and modify.
-- We are not going to handle cases of non-serialisable instances. Please be
-- aware of this and don't put non-serialisable instances in datas or write
-- cases to handle them explicitly.
local function folderToDictionary(folder)
	local dictionary = {}

	for _, object in ipairs(folder:GetChildren()) do
		if object:IsA("Folder") then
			dictionary[object.Name] = folderToDictionary(object)
		elseif object:IsA("ValueBase") or object.ClassName:match("Value") then
			dictionary[object.Name] = object.Value
		end
	end

	return dictionary
end

local function playerAdded(player)
	local toolStats = Instance.new("Folder")
	toolStats.Name = "ToolStats"

	createToolStats("Pickaxe", toolStats)
	createToolStats("Sword", toolStats)

	local success, data = pcall(function()
		return PlayerDataStore:GetAsync(player.UserId)
	end)

	-- Check for success of DataStore call
	if success then
		-- Handle the data if they have it
		if data then
			-- Go over the data we have saved
			for tool, stats in pairs(data) do
				-- Find the relevant tool stats folder
				local toolData = toolStats:FindFirstChild(tool)
				-- If the tool stats folder exists, proceed
				if toolData then
					-- Check through the saved values
					for stat, value in pairs(stats) do
						-- If the stat exists, set its value
						if toolData:FindFirstChild(stat) then
							toolData[stat].Value = value
						end
					end
				end
			end
		end
	else
		warn(debug.traceback("DataStore call unsuccessful"))
		-- DataStore isn't successful, handle this case yourself
		-- You can prevent saves and/or save temporary data
	end
end

local function playerRemoving(player)
	local toolStats = player:FindFirstChild("ToolStats")
	-- I recommend adding different handling procedures, but in this case
	-- for the sake of example I'll ignore nonexistent ToolStats.
	if not toolStats then return end

	local toolData = folderToDictionary(toolStats)

	local success, result = pcall(function()
		return PlayerDataStore:UpdateAsync(player.UserId, function(oldData)
			-- For the sake of example, I'm going to use UpdateAsync like a SetAsync.
			return {Tools = toolData}
		end)
	end)
end

Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)
for _, player in ipairs(Players:GetPlayers()) do
	playerAdded(player)
end

Data will be stored as the following:

{
	["Tool"] = {
		["Strength"] = 0,
		["Wear"] = 0
	}
}

I omitted a lot of things and I don’t expect this to work out of the box because it’s a pure example that I wrote with some free time, but give some consideration if not to the code sample then to the proposed suggestions so you can write a cleaner implementation. This is, however, something that you can do to make the data human readable and know what stats belong to what tool.

3 Likes