Advanced EOS Series — Part 5 — One-to-many Relationships

Mitch Pierias
Coinmonks
Published in
5 min readJan 11, 2019

--

Welcome back to the Advanced EOS development series, here I’ll be touching on advanced techniques and functionality which is rarely covered by tutorials or courses. The purpose of this series is to bring together the missing pieces you’ll need to complete your skills as a distributed application developer on the EOS network. Each post is ordered by difficulty, so if you’d like a general overview I’d recommend starting with Part 1 and working your way up. The full code for these examples can be found here on GitHub.

As these are advanced or extended topics, I’m dangerously assuming you already know the basics and are looking to further your knowledge. For that reason, the code shared in these articles will be concise to the purpose being discussed.

This post will be relatively short, but cover a fundamental and seemingly uncovered method with multi index tables, unique primary indexes.

One-to-many Relationships

I come from a background in Graph Databases, a wonderful land where data is directly linked too it’s related data, like MySQL joins but on steroids. So if you’re like me and have mastered the basics of EOS tables, then you’ve probably asked the question, but how do I create a relationship between multiple tables and rows? Well in this example we’ll be looking into not one, but multiple methods for creating one-to-many relationships using EOS multi_index tables.

In these examples we will look at three different methods for creating relationships between our EOS table rows; Vectors, Indexes and Scopes.

For simplicity, the following examples will not be validating the caller account.

Vectors

Vectors in C++ represent the structure of a dynamic array, allowing us to store an arbitrarily sized collection of values. Let’s explore vectors by storing an array of unique Item identifiers for a players Profile. Here identifiers stored in the Profile table will correspond to the id of rows in our Item table.

struct Profile {
name account;
vector<uint64_t> items;
}

struct Item {
uint64_t id;
string name;
}

Now inside our create() method, we can simply push a reference to the Item using the Item.id we stored prior and the push_back() method of the vector class.

owners.modify(currentPlayer, 0, [&](auto& owner) {
owner.items.push_back(itemID);
});

We can even go a step further and define a sub-collection within our item vector vector<uint64_t, uint32_t> items; where uint64_t is our item identifier and uint32_t is the item’s age. Alternatively a secondary struct (collection) can be stored inside the vector like vector<Item> items;.

Vectors are a great way to store related data and references directly within our table, however table rows can quickly grow large from excessive user input or improper data management. Let’s explore Secondary Indexes instead to create relationships between our tables.

Secondary Indexes

Using secondary indexes provides an alternative way we can scale our references while avoiding excessively large arrays (vectors) in our table rows. It also allows us to move seamlessly between parent-child rows in our table by using a backwards reference from the child to the parent. Let’s take a practical look at this by extending our project from our previous Vector section. First we’re going to modify our structures, removing the items vector from the Profile struct and adding a reference to our owner within the Item struct.

struct Profile {
name account;
}

struct Item {
uint64_t uid;
string name;
name owner;
}

Now we need to add search functionality so we can find our Item by owner. Let’s create a secondary index and add it too our multi_index definition.

struct Item {
...
uint64_t get_owner() const { return owner; }
}

typedef multi_index<N(items), Item, indexed_by<N(byowner), const_mem_fun<Item, uint64_t, &Item::get_owner>> item_table;

Where the name of our index will be byowner and it will return the uint64_t owner key using the get_owner function.

The multi_index method allows us to define up too 16 additional indexes for each table.

We will no longer be updating our Player from within our add item function. Instead we will be storing a reference to the signee account in the owner property of our newly created item. Let’s update our additem action to achieve this;

items.emplace(account, [&](auto& item) {
...
item.owner = account;
});

Now we can get all our items for a player like so;

void indexes::get(const name account) {
item_table items(_self, _self);
auto accounts_items = items.get_index<N(byowner)>();
auto iter = accounts_items.lower_bound(account);
while (iter != accounts_items.end(); iter++) { // Do stuff }
}

Scope

For this final method we’re going to use the table scope to represent relationships between our table rows. Using the scope adds a form of protected security, where data can only be found if it’s scope is known.

The EOS Data Structure

- code       * The account name assigned write permission (contract)
-- scope * The account where the data is stored
--- table * Name of the table being stored
---- record * A table row

We’re going to modify our Item to utilize the table scope for looking up all player's items. To simplify our project, we’ll remove remove the owner reference and get_owner() index from our Item struct and multi_index table definition.

struct Item {
uint64_t id;
string name;
auto primary_key() const { return id; };
EOSLIB_SERIALIZE(Item, (id)(name));
};

typedef multi_index<N(items), Item> item_table;

The real magic happens over in our setters and getters, let’s take a look at how they’ve changed.

item_table items(_self, account);
auto item = items.emplace(account, [&](auto& item) {
... configure item
});

If you look at our table instantiating, you’ll notice we are now using the scope of our account, rather than the contract’s code itself. Our get action has been modified in the same way.

item_table items(_self, account);
auto iter = playerItems.lower_bound();
while (iter != playerItems.end()) { ... }

Simple as that! Isolating data with the scope makes finding data relative to the user is much simpler, however, things like counting all items for all users becomes much more difficult. It’s important to choose a method(s) that apply to the objective of your application.

Get Best Software Deals Directly In Your Inbox

--

--