Multi Index Tables Example

❗️

Deprecated - This document is deprecated. Please refer here instead

Description

In this tutorial we will go through the steps to create and use Multi Index Tables in your smart contract.

Introduction

Multi Index Tables are a way to cache state and/or data in RAM for fast access. Multi index tables support create, read, update and delete (CRUD) operations, something which the blockchain doesn't (it only supports create and read.) Conceptually Multi index tables are stored the the EOSIO RAM cache; each smart contract using a multi index table reserves a partition of the RAM cache; access to each partition is controlled using tablename, code and scope.

Multi Index Tables provide a fast to access data store and are a practical way to store data for use in your smart contract. The blockchain records the transactions, but you should use Multi Index Tables to store application data.

They are multi index tables because they support using multiple indexes on the data, the primary index type must be uint64_t and must be unique, but the other, secondary, indexes can have duplicates. You can have up to 16 additional indexes and the field types can be uint64_t, uint128_t, uint256_t, double or long double

If you want to index on a string you will need to convert this to an integer type, and store the results in a field that you then index.

1. Create a struct

Create a struct which can be stored in the multi index table, and define getters on the fields you want to index.

Remember that one of these getters must be named "primary_key()", if you don't have this the compiler (eosio-cpp) will generate an error ... it can't find the field to use as the primary key.

If you want to have more than one index, (up to 16 are allowed) then define a getter for any field you want to index, at this point the name is less important as you will pass the getter name into the typedef. In the example below there are two getters defined primary_key() and by_pollId().

struct [[eosio::table]] poll 
{
  uint64_t key; // primary key
  uint64_t pollId; // second key, can be non-unique
  std::string pollName; // name of poll
  uint8_t pollStatus =0; // staus where 0 = closed, 1 = open, 2 = finished
  std::string option; // the item you can vote for
  uint32_t count =0; // the number of votes for each time

  uint64_t primary_key() const { return key; }
  uint64_t by_pollId() const {return pollId; }
};

Two additional things to note here:

  1. The attribute [[eosio::table]] is required for the EOSIO.CDT ABI generator, eosio-cpp, to recognise that you want to expose this table via the ABI and make it visible outside the smart contract.

  2. The struct name is less than 12 characters and all in lower case.

2. Create a type

Use a c++ typedef to define a type based on the multi index table template.

Our template takes three arguments:

  • the table name (part of the key used to identify the section of RAM cache that I will use.)
  • the type or struct defining what data we intend to store in the multi index table.
  • the additional indexes if we have them (it's a variadic template.)

In this example we will define the multi index table to use the poll struct shown above. So we will give the type a tablename, the name of the struct it will store, tell it what to index, and how to get the data which is being indexed. A primary key will automatically be created, so using the struct above if I want a multi index table with only a primary key I would define it as :

typedef eosio::multi_index<"poll"_n, poll> pollstable;

The thing to note here is tablename parameter where we use the operator "_n" to convert the string "poll" to an eosio::name. The struct eosio::name wraps uint64_t to provide 'safe' names, and is part of the 'composite key' used to identify data belonging to the multi index table. The operator _n replaces the N() macro. We use the struct as the second parameter, and at this point we have defined no additional indexes (we will always have a primary index.)

We now have defined a class based on the multi index template

To add additional or secondary indexes use the indexed_by template as a parameter, so the definition becomes

typedef eosio::multi_index<"poll"_n, poll, eosio::indexed_by<"pollid"_n, eosio::const_mem_fun<poll, uint64_t, &poll::by_pollId>>> pollstable;

where

indexed_by<"pollid"_n, const_mem_fun<mystruct, uint64_t, &mystruct::by_id>>

the parameters

  • the name of the field converted to an uint64, "pollid"_n
  • a user defined key extractor, const_mem_fun<poll, uint64_t, &poll::by_pollid>

To have three indexes see the generalised example below:

struct [[eosio::table]] mystruct 
      {
         uint64_t     key; 
         uint64_t     secondid;
         uint64_t			anotherid;
         std::string  name; 
         std::string  account; 

         uint64_t primary_key() const { return key; }
         uint64_t by_id() const {return secondid; }
         uint64_t by_anotherid() const {return anotherid; }
      };
      
typedef eosio::multi_index<name(mystruct), mystruct, indexed_by<name(secondid), const_mem_fun<mystruct, uint64_t, &mystruct::by_id>>, indexed_by<name(anotherid), const_mem_fun<mystruct, uint64_t, &mystruct::by_anotherid>>> datastore;

and so on.

An important thing to note here is that struct name matches the table name, and that the the names that will appear in the abi file follow the rules (12 characters and all in lower case.) If they don't then the tables are not visible via the abi (you can get around this by editing the abi file.)

3. Create local variables which are of the defined type

So far we have defined a class based on the multi index template using typedef, now we need an instance of the class so that we can actually use it.

The class name is pollstable so

      // local instances of the multi indexes
      pollstable _polls;

Declares a variable of type pollstable.

I also need to initialise _polls and depending on how I use _polls decides how I will do this.

In the example below I will create _polls as a member of my smart contract so I will need to initialise it in the smart contract constructor. The constructor for pollstable is defined in the multi index table template class. This constructor takes two parameters "code" and "scope"* - these combined with "tablename" provide access to the partition of the RAM cache used by this multi index table. See the youvote constructor in the example below.

Note that the variable _polls is ultimately a reference to the already existing RAM cache so creating instances as I need to use them does not have a memory allocation overhead.

Now I have defined a multi index table with two indexes and I can use this in my smart contract.

An example working smart contract using two multi index tables is shown below, split into a .hpp and .cpp file. Here you can see how to iterate over the tables, add items to the table, remove data from the table and how to use two tables in the same contract.

*Scopes are used to logically separate tables within a multi-index (see the eosio.token contract multi-index for an example, which scopes the table on the token owner). Scopes were originally intended to separate table state in order to allow for parallel computation on the individual sub-tables. However, currently inter-blockchain communication has been prioritized over parallelism. Because of this, scopes are currently only used to logically separate the tables as in the case of eosio.token.

// this is the header file youvote.hpp

#pragma once

#include <eosiolib/eosio.hpp>

//using namespace eosio; -- not using this so you can explicitly see which eosio functions are used.

class [[eosio::contract]] youvote : public eosio::contract {

public:

    //using contract::contract;

    youvote( eosio::name receiver, eosio::name code, eosio::datastream<const char*> ds ): eosio::contract(receiver, code, ds),  _polls(receiver, code.value), _votes(receiver, code.value)
    {}

    // [[eosio::action]] will tell eosio-cpp that the function is to be exposed as an action for user of the smart contract.
    [[eosio::action]] void version();
    [[eosio::action]] void addpoll(eosio::name s, std::string pollName);
    [[eosio::action]] void rmpoll(eosio::name s, std::string pollName);
    [[eosio::action]] void status(std::string pollName);
    [[eosio::action]] void statusreset(std::string pollName);
    [[eosio::action]] void addpollopt(std::string pollName, std::string option);
    [[eosio::action]] void rmpollopt(std::string pollName, std::string option);
    [[eosio::action]] void vote(std::string pollName, std::string option, std::string accountName);

    //private: -- not private so the cleos get table call can see the table data.

    // create the multi index tables to store the data
    struct [[eosio::table]] poll 
    {
        uint64_t      key; // primary key
        uint64_t      pollId; // second key, non-unique, this table will have dup rows for each poll because of option
        std::string   pollName; // name of poll
        uint8_t      pollStatus =0; // staus where 0 = closed, 1 = open, 2 = finished
        std::string  option; // the item you can vote for
        uint32_t    count =0; // the number of votes for each itme -- this to be pulled out to separte table.

        uint64_t primary_key() const { return key; }
        uint64_t by_pollId() const {return pollId; }
    };
    typedef eosio::multi_index<"poll"_n, poll, eosio::indexed_by<"pollid"_n, eosio::const_mem_fun<poll, uint64_t, &poll::by_pollId>>> pollstable;

    struct [[eosio::table]] pollvotes 
    {
        uint64_t     key; 
        uint64_t     pollId;
        std::string  pollName; // name of poll
        std::string  account; //this account has voted, use this to make sure noone votes > 1

        uint64_t primary_key() const { return key; }
        uint64_t by_pollId() const {return pollId; }
    };
    typedef eosio::multi_index<"pollvotes"_n, pollvotes, eosio::indexed_by<"pollid"_n, eosio::const_mem_fun<pollvotes, uint64_t, &pollvotes::by_pollId>>> votes;

    //// local instances of the multi indexes
    pollstable _polls;
    votes _votes;
};
// youvote.cpp

#include "youvote.hpp"

// note we are explcit in our use of eosio library functions
// note we liberally use print to assist in debugging

// public methods exposed via the ABI

void youvote::version() {
    eosio::print("YouVote version  0.22"); 
};

void youvote::addpoll(eosio::name s, std::string pollName) {
    // require_auth(s);

    eosio::print("Add poll ", pollName); 

    // update the table to include a new poll
    _polls.emplace(get_self(), [&](auto& p) {
        p.key = _polls.available_primary_key();
        p.pollId = _polls.available_primary_key();
        p.pollName = pollName;
        p.pollStatus = 0;
        p.option = "";
        p.count = 0;
    });
}

void youvote::rmpoll(eosio::name s, std::string pollName) {
    //require_auth(s);
    
    eosio::print("Remove poll ", pollName); 
        
    std::vector<uint64_t> keysForDeletion;
    // find items which are for the named poll
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            keysForDeletion.push_back(item.key);   
        }
    }
    
    // now delete each item for that poll
    for (uint64_t key : keysForDeletion) {
        eosio::print("remove from _polls ", key);
        auto itr = _polls.find(key);
        if (itr != _polls.end()) {
            _polls.erase(itr);
        }
    }


    // add remove votes ... don't need it the axtions are permanently stored on the block chain

    std::vector<uint64_t> keysForDeletionFromVotes;
    // find items which are for the named poll
    for(auto& item : _votes) {
        if (item.pollName == pollName) {
            keysForDeletionFromVotes.push_back(item.key);   
        }
    }
    
    // now delete each item for that poll
    for (uint64_t key : keysForDeletionFromVotes) {
        eosio::print("remove from _votes ", key);
        auto itr = _votes.find(key);
        if (itr != _votes.end()) {
            _votes.erase(itr);
        }
    }
}

void youvote::status(std::string pollName) {
    eosio::print("Change poll status ", pollName);

    std::vector<uint64_t> keysForModify;
    // find items which are for the named poll
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            keysForModify.push_back(item.key);   
        }
    }
    
    // now get each item and modify the status
    for (uint64_t key : keysForModify) {
        eosio::print("modify _polls status", key);
        auto itr = _polls.find(key);
        if (itr != _polls.end()) {
            _polls.modify(itr, get_self(), [&](auto& p) {
                p.pollStatus = p.pollStatus + 1;
            });
        }
    }
}

void youvote::statusreset(std::string pollName) {
    eosio::print("Reset poll status ", pollName); 

    std::vector<uint64_t> keysForModify;
    // find all poll items
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            keysForModify.push_back(item.key);   
        }
    }
    
    // update the status in each poll item
    for (uint64_t key : keysForModify) {
        eosio::print("modify _polls status", key);
        auto itr = _polls.find(key);
        if (itr != _polls.end()) {
            _polls.modify(itr, get_self(), [&](auto& p) {
                p.pollStatus = 0;
            });
        }
    }
}


void youvote::addpollopt(std::string pollName, std::string option) {
    eosio::print("Add poll option ", pollName, "option ", option); 

    // find the pollId, from _polls, use this to update the _polls with a new option
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            // can only add if the poll is not started or finished
            if(item.pollStatus == 0) {
                _polls.emplace(get_self(), [&](auto& p) {
                    p.key = _polls.available_primary_key();
                    p.pollId = item.pollId;
                    p.pollName = item.pollName;
                    p.pollStatus = 0;
                    p.option = option;
                    p.count = 0;});
            }
            else {
                eosio::print("Can not add poll option ", pollName, "option ", option, " Poll has started or is finished.");
            }

            break; // so you only add it once
        }
    }
}

void youvote::rmpollopt(std::string pollName, std::string option)
{
    eosio::print("Remove poll option ", pollName, "option ", option); 
        
    std::vector<uint64_t> keysForDeletion;
    // find and remove the named poll
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            keysForDeletion.push_back(item.key);   
        }
    }
        
    for (uint64_t key : keysForDeletion) {
        eosio::print("remove from _polls ", key);
        auto itr = _polls.find(key);
        if (itr != _polls.end()) {
            if (itr->option == option) {
                _polls.erase(itr);
            }
        }
    }
}


void youvote::vote(std::string pollName, std::string option, std::string accountName)
{
    eosio::print("vote for ", option, " in poll ", pollName, " by ", accountName); 

    // is the poll open
    for(auto& item : _polls) {
        if (item.pollName == pollName) {
            if (item.pollStatus != 1) {
                eosio::print("Poll ",pollName,  " is not open");
                return;
            }
            break; // only need to check status once
        }
    }

    // has account name already voted?  
    for(auto& vote : _votes) {
        if (vote.pollName == pollName && vote.account == accountName) {
            eosio::print(accountName, " has already voted in poll ", pollName);
            //eosio_assert(true, "Already Voted");
            return;
        }
    }

    uint64_t pollId =99999; // get the pollId for the _votes table

    // find the poll and the option and increment the count
    for(auto& item : _polls) {
        if (item.pollName == pollName && item.option == option) {
            pollId = item.pollId; // for recording vote in this poll
            _polls.modify(item, get_self(), [&](auto& p) {
                p.count = p.count + 1;
            });
        }
    }

    // record that accountName has voted
    _votes.emplace(get_self(), [&](auto& pv){
        pv.key = _votes.available_primary_key();
        pv.pollId = pollId;
        pv.pollName = pollName;
        pv.account = accountName;
    });        
}


EOSIO_DISPATCH( youvote, (version)(addpoll)(rmpoll)(status)(statusreset)(addpollopt)(rmpollopt)(vote))

The contract above has been written for building using the eosio.cdt 1.3.2 and above.

To build the contract and to generate the .abi go to the directory containing the .hpp and .cpp and execute the following command:

 eosio-cpp -abigen youvote.cpp -o youvote.wasm

The EOSIO_DISPATCH call exposes the functions via the abi, it's important the function names match the abi function name rules.