中移鏈合約常用開發介紹 (二)多索引表的使用

BSN研習社發表於2023-01-13

一、目的

本文詳細介紹了開發、部署和測試一個地址簿的智慧合約的流程,適用於EOS的初學者瞭解如何使用智慧合約實現本地區塊鏈上資料的持久化和對持久化資料的增刪改查。

二、智慧合約介紹

區塊鏈作為一種分散式可信計算平臺,去中心化是其最本質的特徵。每筆交易的記錄不可篡改地儲存在區塊鏈上。智慧合約中定義可以在區塊鏈上執行的動作action和交易transaction的程式碼。可以在區塊鏈上執行,並將合約執行狀態作為該區塊鏈例項不可變歷史的一部分。因此,開發人員可以依賴該區塊鏈作為可信計算環境,其中智慧合約的輸入、執行和結果都是獨立的,不受外部影響。

三、術語解釋

EOS

EOS是Enterprise Operation System的縮寫,是商用分散式應用設計的一款區塊鏈作業系統。EOS引入了一種新的區塊鏈架構EOSIO,用於實現分散式應用的效能擴充套件。與比特幣、以太坊等貨幣不同,EOS是一種基於EOSIO軟體專案釋出的代幣,也被稱為區塊鏈3.0。

索引

索引一般是指關聯式資料庫中對某一列或多個列的值進行預排序的資料結構。在這裡,索引是記憶體表的某一欄位,我們可以根據該欄位操作記憶體表的資料。

多索引multi_index

EOS仿造Boost庫中的Multi-Index Containers,開發了C++類 eosio::multi_index(以下簡稱為multi_index),中文也可以叫作多索引表類。透過這個API,我們可以很簡單地支援資料庫表的多鍵排序、查詢、使用上下限等功能。這個新的API使用迭代器介面,可顯著提升掃表的效能。

四、編寫智慧合約

(一)定義程式基本結構

在鏈所在目錄下新建一個addressbook資料夾,在addressbook資料夾中建立一個addressbook.cpp檔案。

cd your_contract_path
mkdir addressbook
cd addressbook
touch addressbook.cpp

引入標頭檔案、名稱空間,

#include <eosio/eosio.hpp>
using namespace eosio;

定義合約類addressbook和其建構函式。合約類應當繼承自eosio::contract。eosio::contract具有三個保護的成員,和眾多公有成員函式。其中三個保護成員如下:

型別 名稱 意義
eosio::name _self 部署此賬戶的合約名稱
eosio::name _first_receiver 首次收到傳入操作的賬戶
datastream< const char * > _ds 合約的資料流

在宣告派生類建構函式時,需要指明這三個成員。

class [[eosio::contract("addressbook")]] addressbook : public eosio::contract {
 public:
 addressbook(name receiver, name code, datastream<const char*> ds):
 contract(receiver, code, ds) {}
 
 private:
 
};

(二)定義資料表結構及索引

1、定義結構體

首先使用struct關鍵字建立一個結構體,然後用[[eosio::table]]標註這個結構體是一個合約表,這裡宣告瞭一個person結構體:

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

型別說明:

name:名稱型別,賬號名、表名、動作名都是該型別,只能使用26個小寫字母和1到5的數字,特殊符號可以使用小數點,必須以字母開頭且總長不超過12。  uint64_t:無符號64位整數型別,表主鍵、name實質都是該型別。這裡需要注意,合約的表名與結構體的名稱沒有關係,因此結構體的名稱不必遵循name型別的規則。

表的結構如下:

型別 名稱 意義
eosio::name key 主鍵 賬戶名
string first_name 名字
string last_name 姓氏
uint64_t age 年齡
string street 街道
string city 城市
string state

2、定義主鍵

傳統資料庫表通常有唯一的主鍵,它允許明確標識表中的特定行,併為表中的行提供標準排列順序。 EOS合約資料庫支援類似的語義,但是在multi_index容器中主鍵必須是唯一的無符號64位整數(即uint64_t型別)。multi_index中的物件按主鍵索引,以無符號64位整數主鍵的升序排列。接下來我們定義一個主鍵函式,上文中已經說明name型別實質上是 uint64_t型別,該函式使用key.value返回一個uint64_t型別的值,並且由於key欄位的含義是EOS中的賬戶名,因此可以保證唯一性:

uint64_t primary_key() const { return key.value;}

3、定義二級索引

multi_index容器中非主鍵索引可以是:

  • uint64_t
  • uint128_t
  • double
  • long double
  • eosio::checksum256

常用的是uint64_t和double型別。使用age欄位作為二級索引:

uint64_t primary_key() const { return key.value;}

4、定義多索引表

合約裡的表都是透過multi_index容器來定義,我們將上面定義的person結構體傳入multi_index容器並配置主鍵索引和二級索引:

using address_index = eosio::multi_index<"people"_n, person,
indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary>>;
>;

說明:

  • _n運算子用於定義一個name型別,上述程式碼將“people” 定義為name型別並作為表名;
  • person結構體被傳入作為表的結構;
  • indexed_by結構用於例項化索引,第一個引數“byage”_n為索引名,第二個引數const_mem_fun為函式呼叫運算子,該運算子提取const值作為索引鍵。本例中,我們將其指向之前定義的getter函式get_secondary。

使用上述定義,現在我們有了一個名為people的表,目前addressbook.cpp的完整程式碼如下:

#include <eosio/eosio.hpp>
using namespace eosio;
class [[eosio::contract("addressbook")]] addressbook : public eosio::contract {
 public:
  
  // 建構函式,呼叫基類建構函式
 addressbook(name receiver, name code,  datastream<const char*> ds): contract(receiver, code, ds) {}
 
 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; }
  
 };
 
 // 定義多索引表
 using address_index = eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary_1>>>;
 
};

(三)定義資料操作方法

定義好表的結構後,我們透過[[eosio::action]]來定義對資料進行增刪改的基本動作。

1、增添和修改動作

首先是提供了插入或修改資料的動作upsert,為了簡化使用者體驗,使用單一方法負責行的插入和修改,並將其命名為 “upsert” ,即 “update” 和 “insert” 的組合。該方法的引數應當包括所有需要存入people表的資訊成員。

public:
[[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() 來驗證許可權,此方法接收name型別引數,並斷言執行該動作的賬戶等於接收的值,具有執行動作的許可權:

[[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,現在要例項化表,需要兩個引數:

  • 第一個引數code,它指定此表的所有者,在這裡,表的所有者為合約所部署的賬戶,我們使用get_first_receiver()函式傳入,get_first_receiver() 函式返回該動作的第一個接受者的name型別名字。
  • 第二個引數scope,它確保表在此合約範圍內的唯一性。在這裡,我們使用get_first_receiver().value傳入。
  address_index addresses(get_first_receiver(), get_first_receiver().value);

接下來,查詢迭代器,並用變數iterator來接收:

auto iterator = addresses.find(user.value);

之後我們可以使用emplace() 函式和modify() 函式來插入或修改記錄。當多索引表中未查詢到該賬戶的記錄時,使用emplace() 向表中新增;當多索引表中查詢到過往記錄時,使用modify() 修改原有記錄。

 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 {
    //修改
    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;
    });
  }

2、刪除動作

與上文類似,定義erase動作提供刪除資料的動作,查詢迭代器。

[[eosio::action]]
  void erase(name user) {
    require_auth(user);
    address_index addresses(get_self(), get_first_receiver().value);
    auto iterator = addresses.find(user.value);
  
  }

這裡使用check() 判斷表中是否存在該記錄,若不存在則給出報錯,存在則使用erase刪除該項。

  check(iterator != addresses.end(), "Record does not exist");
    addresses.erase(iterator);

3、儲存檔案

加入以上兩個動作後,目前addressbook.cpp的完整程式碼如下:

#include <eosio/eosio.hpp>
using namespace eosio;
class [[eosio::contract("addressbook")]] addressbook : public eosio::contract {
public:
  addressbook(name receiver, name code,  datastream<const char*> 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(get_first_receiver(),get_first_receiver().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 {
      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(get_self(), get_first_receiver().value);
    auto iterator = addresses.find(user.value);
    check(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; }
  };
  using address_index = eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary_1>>>;
};

五、部署測試

(一)部署

1、建立addressbook賬戶

cleos create account eosio addressbook EOS7yK5K2mRCijPpRzStvucn53D4SybVge8uQ3hSnLaUvT4CPXzgo

注意其中:EOS7yK5K2mRCijPpRzStvucn53D4SybVge8uQ3hSnLaUvT4CPXzgo 是儲存於cleos錢包中的一個公鑰,實際開發情況中需根據自己錢包中儲存的公鑰進行替換。

2、進入智慧合約所在目錄

cd your_contract_path/addressbook

3、編譯

eosio-cpp -abigen -o addressbook.wasm addressbook.cpp

undefined

4、部署合約到addressbook賬戶上

cd ..
cleos set contract addressbook addressbook

undefined

(二)測試

1、建立兩個測試賬戶alice和bob

cleos create account eosio alice 公鑰
cleos create account eosio bob 公鑰

2、插入資料

呼叫upsert動作插入資料

cleos push action addressbook upsert '["alice", "alice", "liddell", 9, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
cleos push action addressbook upsert '["bob", "bob", "is a guy", 49, "doesnt exist", "somewhere", "someplace"]' -p bob@active

插入成功,執行結果如下

undefined

3、查詢資料

查詢資訊一般在命令列使用cleos get table進行:

cleos get table 擁有表的賬戶 表所在合約名 表名

此條命令可以查詢出表內的所有資訊,也可以透過新增後續的約束來查詢指定資訊:

  • --upper XX等於或在此之前
  • --lower XX等於或在此之後
  • --key-type XXX型別
  • --index X根據第幾個索引
  • --limit XX顯示前幾個資料

我們可以透過主鍵key查詢表的資料

cleos get table addressbook addressbook people --lower alice

--lower alice表示查詢的下界,以“alice”作為下界可以查詢到兩條記錄:

undefined

接下來透過二級索引age查詢資料

cleos get table addressbook addressbook people --upper 10 \
--key-type i64 \
--index 2

--upper 10表示查詢上界,即查詢索引欄位小於等於10的記錄。--index 2表示使用二級索引查詢。查詢到一條記錄:

undefined

4、修改資料

呼叫upsert動作修改資料:

cleos push action addressbook upsert '["alice", "mary", "brown", 9, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active

查詢後可以看到記錄的first_name和last_name欄位已被修改:

undefined

5、刪除資料

呼叫erase動作刪除資料:

cleos push action addressbook erase '["alice"]' -p alice@active

刪除後查詢不到alice,說明刪除成功:

undefined

六、常見問題

更改資料表結構

需要注意的是,如果合約已經部署到合約賬戶上且表中已經儲存了資料,那在更改表的結構如新增刪除索引或欄位時,則需要將表中的資料全部刪除後,再將新的合約部署到合約賬戶上。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70012206/viewspace-2932058/,如需轉載,請註明出處,否則將追究法律責任。

相關文章