These docs are for v1.3.0. Click to read the latest docs for v1.6.

Multi Index Tables Example

Description

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

Notes

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.)

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.

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

         uint64_t primary_key() const { return key; } // getter for primary key
         uint64_t by_id() const {return secondid; } // getter for additional key
      };

Two additional things to note here:

  1. The attribute [[eosio::table]] is required for the 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. typedef the multi index table and the define the indexes

Define the multi index table which will use mystruct, 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<name(mystruct), mystruct> datastore;

This defines the multi index passing in the tablename "name(mystruct)" and the struct name "mystruct". name(mystruct) performs a compile conversion of the struct name to a uint64_t and this uint64_t is used to identify data belonging to the multi index table.

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

typedef eosio::multi_index<name(mystruct), mystruct, indexed_by<name(secondid), const_mem_fun<mystruct, uint64_t, &mystruct::by_id>>> datastore;

where

indexed_by<name(secondid), const_mem_fun<mystruct, uint64_t, &mystruct::by_id>>

the parameters

  • the name of the field converted to an integer, name(secondid)
  • a user defined key extractor, const_mem_fun<mystruct, uint64_t, &mystruct::by_id>

To have three indexes

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

      // local instances of the multi indexes
      pollstable _polls;
      votes _votes;

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. Here you can see how to iterate over the tables and how to use two tables in the same contract.

#include <eosiolib/eosio.hpp>

using namespace eosio;

class youvote : public contract
{
  public:
      youvote(eosio::name s):contract(s), _polls(s, s), _votes(s, s)
      {}

      // public methods exposed via the ABI
      // on pollsTable

      [[eosio::action]]
      void version()
      {
          print("YouVote version  0.01"); 

      };
      
      [[eosio::action]]
      void addpoll(eosio::name s, std::string pollName)
      {
          //require_auth(s);

          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;
                                      });
      };


      [[eosio::action]]
      void rmpoll(eosio::name s, std::string pollName)
      {
          //require_auth(s);

          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)
          {
              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 actions 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)
          {
              print("remove from _votes ", key);
              auto itr = _votes.find(key);
              if (itr != _votes.end())
              {
                _votes.erase(itr);
              }
          }


      };

      [[eosio::action]]
      void status(std::string pollName)
      {
          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)
          {

            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;
                                              });
            }
          }
      };

      [[eosio::action]]
      void statusreset(std::string pollName)
      {
          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)
          {
              print("modify _polls status", key);
              auto itr = _polls.find(key);
              if (itr != _polls.end())
              {
                _polls.modify(itr, get_self(), [&](auto& p)
                                                {
                                                  p.pollStatus = 0;
                                                });
              }
          }
      };


      [[eosio::action]]
      void addpollopt(std::string pollName, std::string option)
      {
          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
                    {
                        print("Can not add poll option ", pollName, "option ", option, " Poll has started or is finished.");
                    }

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

      [[eosio::action]]
      void rmpollopt(std::string pollName, std::string option)
      {
          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)
          {
              print("remove from _polls ", key);
              auto itr = _polls.find(key);
              if (itr != _polls.end())
              {
                  if (itr->option == option)
                  {
                      _polls.erase(itr);
                  }
              }
          }
      };


      [[eosio::abi]]
      void vote(std::string pollName, std::string option, std::string accountName)
      {
          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)
                  {
                      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)
              {
                  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;
                                      });        
      };

  private:    

    // 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<N(poll), poll, indexed_by<N(pollId), 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<name(pollvotes), pollvotes, indexed_by<name(pollId), const_mem_fun<pollvotes, uint64_t, &pollvotes::by_pollId>>> votes;

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

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

Note the EOSIO_ABI call, this exposes the functions via the abi, it's important the function names match the abi function name rules.