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:
-
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. -
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.
Updated about 6 years ago