Skip to content

Instantly share code, notes, and snippets.

@Life-In-Dev
Last active January 17, 2023 12:31
Show Gist options
  • Save Life-In-Dev/edc72d7b960dcc90a9fe6ee0a731011b to your computer and use it in GitHub Desktop.
Save Life-In-Dev/edc72d7b960dcc90a9fe6ee0a731011b to your computer and use it in GitHub Desktop.
A layman's guide to Data Stores in Roblox.

What are data stores?

Data stores are a service offered to developers which allow them to save data and fetch it from any server in the universe (game), and are essentially structured as associative arrays.

How are data stores structured?

Data stores are structured similarly to dictionaries/records. They consist of key-value pairs. Each key is unique and acts like a "header" or "identifier", if you like, and can hold a value that can be converted or serialised into JSON. This means that you cannot explicitly set a key's value to a userdata (which includes but is not limited to Instances), and can only save primitive types (numbers, booleans, strings and tables containing only these types).

Example

  • PlayerDataStore
    • 98401192 | {Currency = 100, IsAdmin = false}
    • 46383457 | {Currency = 238, IsAdmin = true}

The above example is a representation of a data store that holds a collection of key-value pairs which hold player data. In the context of saving player data, the key should be a unique identifier relevant to the corresponding player, hence you can use the player's UserId.

Making requests to a data store

Getting Data

You can fetch data from a data store using the GetAsync method. It takes a string as it's key parameter, which will be used as a unique identifier for your data.

API

  • Method

     variant GetAsync(string Key)
  • Sample call

     local PlayerDataStore = game:GetService("DataStore"):GetDataStore("PlayerDataStore")
     
     game.Players.PlayerAdded:Connect(function(player)
     	local playerData = PlayerDataStore:GetAsync(player.UserId)
     end)

Setting Data

You can set a key's value using the SetAsync method. It takes the key whose value will be set, and a primitive value as the arguments.

API

  • Method

     void SetAsync(string Key, variant Value)
  • Sample call

     local PlayerDataStore = game:GetService("DataStore"):GetDataStore("PlayerDataStore")
     
     local ServerPlayerData = {}
    
     game.Players.PlayerRemoving:Connect(function(player)
     	PlayerDataStore:SetAsync(player.UserId, ServerPlayerData[player])
     end)

There are other member functions available to us which allow us to "Update" a data store field, but these are rarely needed and you can find them on the wiki here.

Error handling

Data store requests are prone to errors, and to have good design you must be able to handle errors effectively whenever they happen. Something as simple as wrapping the request around a pcall() and repeating the request after an arbitrary interval should do the trick.

Throttling

Data stores are assigned a request budget. If the number of requests made exceed this budget, the data store will throttle.

Sample code

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local PlayerDataStore = DataStoreService:GetDataStore("PlayerDataStore")

local RETRY_LIMIT = 3
local RETRY_INTERVAL = 5

local function GetPlayerData(userId)
	local success, e = pcall(PlayerDataStore.GetAsync, PlayerDataStore, userId)

	-- Return player data if the request was successful.
	if success then return e end

	-- Check if the data store has throttled, if so, wait until the request budget is increased.
	while DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.GetAsync) < 1 do
		wait(1)
		warn("Data store budget depleted")
	end

	local currentRetries = 0

	-- Repeat the request until the request is successful or the request limit is reached.
	repeat
		wait(RETRY_INTERVAL)
		currentRetries = currentRetries + 1

		success, e = pcall(PlayerDataStore.GetAsync, PlayerDataStore, userId)
		
		if success then
			return e
		end
	until currentRetries == RETRY_LIMIT
end

Players.PlayerAdded:Connect(function(player)
	local playerData = GetPlayerData(player.UserId)
end)

Good design

You've probably found youtube videos that recommend you assign multiple keys to one specific value, even the wiki is guilty of this:

DataStore:SetAsync(player.UserId .. "_cash", player.Cash.Value)

It's simply bad design, you should be saving one dictionary per player, which contains all of the player's data. This reduces the total number of requests made, and consequently reduces the risk of throttling. This method also allows for scalability.

Here's a template or schema (blueprint of a data store) that one may use to save player data:

local PlayerDataSchema = {
	General = {
		Currency = 0,
		Experience = 0
	},
	Moderation = {
		AdminStatus = "Normal",
		ModerationHistory = {},
		ModerationStatus = "None"
	},
	PurchaseLog = {}
}

If you're familiar with OOP you could also save an object, do whatever is intuitive for you.

Closing remarks

Remember to maintain good error handling, scalability and always design your data stores intuitively. If your game is in production and your data stores are designed poorly, you may have issues later down the line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment