はじめに
本記事ではEOS Developer Portalで紹介されているaddressbookコントラクトを用いて、EOSにおけるデータの永続化と、コントラクト内でアクションを実行するインラインアクションについて、全3回で解説します。
第2回では、Multi-Index APIで利用できるセカンダリーインデックスを用いて、永続化されたデータの検索や絞り込みをするための方法を解説します。
本記事は、過去にコンセンサス・ベイスが主宰していたオンラインサロンの記事です。記事は2017年~2018年にかけて執筆されたため、一部は、既に古くなっている可能性があります。あらかじめご了承ください。
関連する記事
EOSIO Developer Portal 解説 最短でHello World!
EOSIO Developer Portal 解説 独自トークンの発行 第1回
EOSIO Developer Portal 解説 独自トークンの発行 第2回
EOSIO Developer Portal 解説 独自トークンの発行 第3回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第1回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第2回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第3回
セカンダリーインデックスの実装
EOSのMulti-Index APIでは、ユニークなプライマリーキーの他に、最大16個までのセカンダリーインデックスを定義できます。セカンダリーインデックスを用いることで、プライマリーキー以外の属性値による検索や絞り込みを、全データを参照することなく高速に実現できます。表1に、セカンダリーインデックスがサポートするキー型の一覧を示します。
表1. Multi-Index APIのセカンダリーインデックスがサポートするキー型
型 | 説明 |
idx64 | 64 bit の符号なし整数キー |
idx128 | 128 bit の符号なし整数キー、または、128 bit の固定長辞書順キー |
idx256 | 256 bit の固定長辞書順キー |
idx_double | 倍精度浮動小数点数キー |
idx_long_double | 四倍精度浮動小数点数キー |
セカンダリーインデックスを用いるには、インデックスを貼りたい属性からキー型のデータを返却する関数を定義します。
サンプルコントラクトへのセカンダリーインデックスの追加
第1回で用いたaddressbookのサンプルコントラクトを用いて、セカンダリーインデックスの使い方を紹介します。
addressbookコントラクトでは、personというstructを定義し、それらのデータを扱うpeopleテーブルを実装しました。今回は、このperson structにage属性を追加し、年齢を用いたデータの絞り込み機能をセカンダリーインデックスにより実装します。
addressbook.cppのperson struct定義の部分を、コード1に示すコードに修正します。このコードでは、person stcurtにuint64_t型のageという属性を追加し、その値を返すget_secondary_1関数を追加しています。
コード1. person structにage属性とget_secondary_1関数を追加
private:
struct [[eosio::table]] person {
name key;
std::string first_name;
std::string last_name;
uint64_t age;
std::string street;
std::string city;
std::string state;
uint64_t primary_key() const { return key.value; }
uint64_t get_secondary_1() const { return age; }
};
続いて、address_index型のテーブル定義を、コード2に示すコードに修正します。修正点は、indexed_by<~> を追加している箇所です。ここで、byageという名前のインデックスを追加し、const_mem_fun
コード2. address_indexテーブル型にセカンダリーインデックスを追加
typedef eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun>> address_index;
また、person structにage属性を追加したため、upsert関数の修正も必要です。コード3に、修正したupsert関数の定義を示します。upsert関数の引数にageを追加し、emplace, modifyそれぞれの場合にageの値の更新処理を追記しています。
コード3. upsert関数にage属性の扱いを追加
[[eosio::action]]
void upsert(name user, std::string first_name, std::string last_name, uint64_t age, std::string street, std::string city, std::string state) {
require_auth( user );
address_index addresses(_code, _code.value);
auto iterator = addresses.find(user.value);
if( iterator == addresses.end() )
{
addresses.emplace(user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.last_name = last_name;
row.age = age;
row.street = street;
row.city = city;
row.state = state;
});
}
else {
std::string changes;
addresses.modify(iterator, user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.last_name = last_name;
row.age = age;
row.street = street;
row.city = city;
row.state = state;
});
}
}
それでは、修正したサンプルコントラクトをデプロイし、セカンダリーインデックスの動作を確認してみましょう。
サンプルコントラクトによる動作確認
修正した addressbook.cpp から、コマンド1を用いて前回と同様にwasmファイルとabiファイルを生成します。
コマンド1. コントラクトのコンパイル
$ eosio-cpp -o addressbook.wasm addressbook.cpp --abigen
コンパイルが完了したら、コマンド2を用いてコントラクトのデプロイをおこないます。
コマンド2. コントラクトのデプロイ
$ cleos set contract addressbook CONTRACTS_DIR/addressbook
無事にコントラクトのデプロイが完了したら、データを追加してセカンダリーインデックスの挙動を試してみます。
コマンド3を用いてaliceのデータを追加し、コマンド4を用いてbobのデータを追加します。aliceとbobのデータには、それぞれageとして9, 49を追加しています。
コマンド3. aliceのデータを追加
$ cleos push action addressbook upsert '["alice", "alice", "liddell", 9, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
コマンド4. bobのデータを追加
$ cleos push action addressbook upsert '["bob", "bob", "is a guy", 49, "doesnt exist", "somewhere", "someplace"]' -p bob@active
セカンダリーインデックスを用いたデータ抽出
テーブルに追加されたデータを確認するには、cleos get tableコマンドを使用します。表2に、インデックスを用いてtableのデータを絞り込む際によく使用するパラメタを紹介します。
表2. cleos get tableコマンドのパラメタ抜粋
パラメタ | 説明 |
-L,–lower TEXT | 絞り込みたいデータのキーの値の下限値を指定する。 |
-U,–upper TEXT | 絞り込みたいデータのキーの値の上限値を指定する。 |
–index TEXT | 使用したいインデックスの番号を指定する。1であればプライマリーキー、2であればセカンダリーキー、3であれば3番目のキーを表す。 |
–key-type TEXT | インデックスのキー型を指定する。(i64, i128, i256, float64, float128, ripemd160, sha256など) |
-r,–reverse | データを逆順でイテレーションする。 |
具体的に、addressbookコントラクトに実装したbyageインデックスを用いたデータの絞り込みを実践してみましょう。コマンド5は、byageインデックスを用いてageが10以下のデータに絞り込むクエリ例を示します。byageインデックスは、uint64型でセカンダリーインデックスの最初に定義しているため、key-typeにはi64、indexには2を指定します。上限値として、upperには10を指定します。
このクエリにより、ageが49のbobのデータはフィルタされ、aliceのデータのみが表示されます。
コマンド5. byageインデックスを用いてageが10以下のデータに絞り込む
$ cleos get table addressbook addressbook people --upper 10 \
--key-type i64 \
--index 2
{
"rows": [{
"key": "alice",
"first_name": "alice",
"last_name": "liddell",
"age": 9,
"street": "123 drink me way",
"city": "wonderland",
"state": "amsterdam"
}
],
"more": false
}
コマンド6のとおり、upperを50に変更すると、ageが49のbobのデータが表示されることが分かります。aliceのデータもageが50以下である条件を満たしているので、同時に表示されています。
コマンド6. byageインデックスを用いてageが50以下のデータに絞り込む
$ cleos get table addressbook addressbook people --upper 50 \
--key-type i64 \
--index 2
{
"rows": [{
"key": "alice",
"first_name": "alice",
"last_name": "liddell",
"age": 9,
"street": "123 drink me way",
"city": "wonderland",
"state": "amsterdam"
},{
"key": "bob",
"first_name": "bob",
"last_name": "is a loser",
"age": 49,
"street": "doesnt exist",
"city": "somewhere",
"state": "someplace"
}
],
"more": false
}
もし、セカンダリーインデックスをコントラクトのコード内で参照したい場合は、multi_indexのget_indexメソッドを用います。コード4に、コントラクト内からbyageインデックスを用いてデータを検索するfindByAge関数の実装例を示します。
コード4. セカンダリーインデックスを用いたfindByAge関数の実装例
[[eosio::action]]
void findByAge(name user, uint64_t age) {
require_auth(user);
address_index addresses(_self, _code.value);
auto age_index = addresses.get_index<"byage"_n>();
auto itr = age_index.find(age);
eosio_assert(itr != age_index.end(), "Yes, we have someone with that age");
}
本記事では、セカンダリーインデックスを用いて永続化されたデータを効率的に取得する方法について紹介しました。複数のインデックスを用いることにより、さまざまな条件でデータを検索・絞り込みできます。
最後に、修正したサンプルコントラクトのコード全体をコード5に示します。
コード5. セカンダリーインデックスを追加したaddressbook.cpp
#include
#include
using namespace eosio;
class [[eosio::contract]] addressbook : public eosio::contract {
public:
using contract::contract;
addressbook(name receiver, name code, datastream ds): contract(receiver, code, ds) {}
[[eosio::action]]
void upsert(name user, std::string first_name, std::string last_name, uint64_t age, std::string street, std::string city, std::string state) {
require_auth( user );
address_index addresses(_code, _code.value);
auto iterator = addresses.find(user.value);
if( iterator == addresses.end() )
{
addresses.emplace(user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.last_name = last_name;
row.age = age;
row.street = street;
row.city = city;
row.state = state;
});
}
else {
std::string changes;
addresses.modify(iterator, user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.last_name = last_name;
row.age = age;
row.street = street;
row.city = city;
row.state = state;
});
}
}
[[eosio::action]]
void erase(name user) {
require_auth(user);
address_index addresses(_self, _code.value);
auto iterator = addresses.find(user.value);
eosio_assert(iterator != addresses.end(), "Record does not exist");
addresses.erase(iterator);
}
private:
struct [[eosio::table]] person {
name key;
std::string first_name;
std::string last_name;
uint64_t age;
std::string street;
std::string city;
std::string state;
uint64_t primary_key() const { return key.value; }
uint64_t get_secondary_1() const { return age; }
};
typedef eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun>> address_index;
};
EOSIO_DISPATCH( addressbook, (upsert)(erase))
次回は、コントラクト内でアクションを実行するインラインアクションについて解説します。
参考文献:
- EOSIO Developer Portal – 2.5 Secondary Indices
https://developers.eos.io/eosio-home/docs/secondary-indices
- EOSIO Developer Portal – Multi-Index DB API
https://developers.eos.io/eosio-cpp/v1.3.1/docs/db-api