Storage engine structure

Hello.
I need some help to understand how i would go about the data stored the storage engine.

For instance, i do have some data that belongs to describe a Player.

As how it is now i have a collection called “player_data” and a key value for each value stored in the engine:

collection: “player_data”, key: “position”
collection: “player_data”, key: “username”
collection: “player_data”, key: “exp”

is this a valid thing to do? or is it preferable to store everything under one key?

Thanks! :slight_smile:

Hi @gruset. You ask a great question because I’ve noticed more and more game teams that seem to separate out each storage object to handle a single value rather than a complex JSON object. This is not how we envisaged the storage engine would be used with Nakama server so let me share some example data structures from a game project we worked on with a studio.

I hope it will help to share how we think is the most optimal way to store data for players with Nakama.

  • collection = economy, key = inventory
{
  "consumables": {
    "spawner_A": {
      "count": 40,
      "owned_time": 1636888665
    },
    "spawner_B": {
      "count": 0,
      "owned_time": 1636888665
    },
    "spawner_C": {
      "count": 24,
      "owned_time": 1636888665
    }
  },
  "collectables": {}
}

We implemented an inventory system which uses the concept of both collectables and consumables which exist for the user. These could be objects like swords, shields, collectable cards or really any entity which is owned by the user but can be accumulated, spent, and/or traded.

  • collection = economy, key = donations
{
  "donations": {
    "team_help_tickets": {
      "count": 4,
      "claim_count": 4,
      "expire_time": 1637101354,
      "contributors": {
        "16cafe3c-3ea7-4fb2-8d74-4bab45304762": {
          "count": 1,
          "update_time": 1637090139
        },
        "2e1ac665-ffd8-4014-a83e-b28e34b4b2ac": {
          "count": 1,
          "update_time": 1637091079
        },
        "f623189c-98fd-4302-bf6b-dc11d5343766": {
          "count": 1,
          "update_time": 1637089806
        }
      }
    }
  }
}

This is your typical donation system where users in the same guild or team can gift objects to each other in exchange for rewards. We track how much contribution each user has made to the current user’s donation request.

  • collection = progression, key = achievements
{
  "achievements": {
    "R1_TASK1": {
      "count": 1,
      "claim_time": 1639493455,
      "create_time": 1639493455,
      "update_time": 1639493455
    },
    "R1_TASK2": {
      "count": 1,
      "claim_time": 1639495509,
      "create_time": 1639495509,
      "update_time": 1639495509
    },
    "R1_TASK3": {
      "count": 2,
      "claim_time": 1639497209,
      "create_time": 1639497209,
      "update_time": 1639497209
    }
  }
}

This is your typical achievement system where the user has to obtain some count of progress towards the achievement before its unlocked and they can optionally obtain a reward for it.

A few other notes about the example data you showed:

collection: “player_data”, key: “position”
collection: “player_data”, key: “username”
collection: “player_data”, key: “exp”

The user account in Nakama for each player already handles the username field and has a separate field called display_name (same as Steam API) for situations where the name can be the same as other users. You shouldn’t need to store that separately ever in the storage engine.

Hope this helps.

1 Like

is this a valid thing to do? or is it preferable to store everything under one key?

I forgot to answer your specific question. :slight_smile:

The answer is really to store everything under a single key but only for logically related bits of data. For example achievements are not part of the player’s inventory which means they make sense as separate storage objects.

Hello there and thank u for ur answer! :slight_smile: I fully understand what u are saying, and i assume its the way to go! Though, one thing that comes to mind:

What if i have to store just one property in the while table. For instance, i want to save the state of a players position. Thats a read and a write operation. instead of just one write.

First we have to read the whole table. Get the right property to modify, in our case the position. then store the updated table once again.

Wouldnt it be better to seperate the data as mentioned above? :slight_smile:

Take care!
/G

Updating a single field in a JSON object can be achieved with a custom SQL query using our runtime provided functions in Lua/JavaScript or the *sql.DB handle in Go.

For example, if I had the following storage object:

  • collection: economy
  • key: inventory
  • owner_id: <some_user_id>
{
   "consumables":{
      "item_A":{
         "count":3
      },
      "item_B":{
         "count":10
      }
   }
}

Let’s say we’d like to increment “item_B” count by 1 in a single operation, it can be achieved with the following custom SQL query:

UPDATE storage SET value = jsonb_set(value, '{consumables,item_B,count}', to_jsonb((value->'consumables'->'item_B'->'count')::INT + 1)) WHERE collection = 'economy' AND user_id = '<some_user_id>' AND key = 'inventory';

The full reference for jsonb type column manipulation can be found here.

We’d discourage anyone from writing custom SQL unless absolutely needed, our provided APIs ensure optimal performance when reading/writing to the db, we cannot guarantee the same in custom written SQL. The performance gain from sparing a read might arguably not be needed for most use-cases; or worth it from a code maintainability standpoint.

In the context of this post, this doesn’t really make a lot of sense.

This is a good question. Especially for the case of Achievements. @novabyte mentions that it’s “best practice” to bundle them achievements together in the same Collection/Key. And I agree. For ease of maintenance, for a clear view in the console, etc it makes sense to have the all together in under the same key.

But in the context of Achievements, these will usually require a specific event to be fired in game to update. So user wins a game and this gets him the “R1_TASK1” achievement. The way I understand it, the client will have to update the value locally and Write the ENTIRE dictionary/list to the server (even if just one value was changed). For a system with say 600 achievements, this creates quite a lot of network traffic doesn’t it? Especially if achievements update often, but separately.

The alternative is to trigger an RPC for each achievement, but based on what you said here, you can’t update a single value in a Collection-Key even from the server code.

I see this quite a bit, sadly, where the answer to a question is “Use SQL queries” usually accompanied by “but we don’t recommend this”.

So @gruset 's question still stands, I think. Without using SQL queries and potentially introduce vulnerabilities in the runtime code, what really is the best way to trigger an achievement without having to write the entire dictionary of achievements to the server every time?

The sequence is:

  • Client triggers an event
  • Each achievement has a listener for particular events
  • If an achievement receives an event it listens for, it updates the “COUNT” and pushes the value to the server
  • …What’s next here, avoiding pushing the rest of the unchanged achievements in a massive Json to the server for every individual “COUNT” update or writing custom SQL queries?

Thank you in advance for your time! :slight_smile:

Just want to say that I absolutely love Nakama and how it easy it has made development! This isn’t a complaint or a criticism, just want to make sure the structure and code is as efficient as possible.

@Antviss We discourage writing custom SQL unless needed, the reason is mostly to prevent performance related issues that stem from poorly written queries. We want to introduce a runtime storage update function that supports JSONPATH in the future to support the use case you’re describing, but for the time being the custom SQL route is the best approach for such cases.

Hope this helps, and thank you for the kind words!