2.4 数据的持久化

优质
小牛编辑
143浏览
2023-12-01

为了学习数据的持久化,写一个简单的地址薄合约.虽然这个例子因为各种原因作为生产环境的合约不太实用,但它是一个很好的合约用来学习EOSIO的数据持久化并且不会因为与eosio multi_index不相关的相关业务逻辑分心.

Step 1:创建一个新的文件夹

进入之前的目录:

cd /Users/zhong/coding/CLion/contracts

为我们的合约创建一个新的目录并进去:

mkdir addressbook
cd addressbook

Step 2:创建并打开一个新文件

touch addressbook.cpp

用你喜欢的编辑器打开它.

Step 3:编写一个标准的继承类并Include EOSIO

通过先前的教程,你应该已经熟悉下面的结构了,该类被命名为addressbook.

#include <eosiolib/eosio.hpp>

using namespace eosio;

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

  private: 

};

Step 4:为Table创建Data Structrue

在一张表可以被配置和实例化前,代表地址簿的结构需要被写出来.把它当成一个"schema".因为它是地址簿,这个表会包含"人",所以创建一个struct叫做person.

struct person {};

当为multi_index表定义好schema后,你需要一个唯一的值用来作为主键.

对这个合约来说,用一个name类型的字段命名为"key".这个合约为每一个用户创建一个唯一的记录,所以这个键将会被持久化并根据用户的name保证是唯一值.

struct person {
    name key; 
};

因为这个合约是地址簿,所以可能应该为每个条目或人员存储一些相关的详细信息:

struct person {
 name key;
 string first_name;
 string last_name;
 string street;
 string city;
 string state;
};

很好,现在基本的schema就完成了.下一步,定义一个primary_key方法,它会被multi_index来迭代.每一个multi_index schema都需要一个primary key.为了实现它你简单创建一个命名为primary_key()的方法并返回一个值,这个情况下,key是定义在strcut中的.

struct person {
 name key;
 string first_name;
 string last_name;
 string street;
 string city;
 string state;

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

一个table's schema如果有数据了就不能再被修改.如果你不管怎样都需要改变table's schema,你应该先移除它的所有rows.

Step 5:配置Multi-Index Table

现在,table的schema已经通过我们定义struct来完成了. eosio::multi_index 的构造函数需要被命名以及通过我们刚刚定义好的struct配置以让其可被使用.

typedef eosio::multi_index<"people"_n, person> address_index;

1.使用 _n 操作符来定义 eosio::name类型并使用它来作为表的名称.这张表会包含很多个"persons",所以将这张表命名为"people"

2.传入上一步定义好的person struct.

3.声明该表的类型.这一类型会用于之后实例化该表.

//configure the table
typedef eosio::multi_index<"people"_n, person> address_index;

使用上面的multi_index配置,有一个名为people的multi_index ,基于它的schema,它的每一行都是由person这个结构组成的.

#include <eosiolib/eosio.hpp>

using namespace eosio;

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

  public:

  private:
    struct [[eosio::table]] person {
      name key;
      std::string first_name;
      std::string last_name;
      std::string street;
      std::string city;
      std::string state;

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

    typedef eosio::multi_index<"people"_n, person> address_index;
};

Step 6:构造函数

当使用C++类的时候,第一个创建的public method应该是构造函数(constructor).

我们的构造函数代表初始化该合约.

EOSIO 合约继承contract这个类.使用合约的code name和receiver初始化我们的父合约类.这里有个重要参数:code ,这是个参数代表该合约部署到区块链时它将会属于谁.

addressbook(name receiver, name code,  datastream<const char*> ds):contract(receiver, code, ds) {}

Step 7:向表添加记录

刚刚,multi-index table 的主键已经定义好,以约束该合约对每个用户只存储一条记录.为了让其正常运行,需要建立一些关于设计的假设.

1.只有用户本身的账户有权限去修改他的地址簿.

2.表的primary_key是唯一的,它基于username.

3.为了可用性,合约应该有能力使用单一的action创建和修改table的一行.

在EOSIO的链中,账户是唯一的,所以account_name在当前情况下是作为primary_key的理想选择. account_name 的类型是uint64_t.

接下来,定义一个action让user可以添加和更新记录.该action需要接收值以让该action有能力安放(创建)或修改.

现在将定义格式化让其便于阅读.为了用户体验以及让接口更简洁,创建一个方法让其可以创建和修改数据.因此,将其命名为"upsert","update"和"insert"的组合词.

void upsert(
  name user, 
  std::string first_name, 
  std::string last_name, 
  std::string street, 
  std::string city, 
  std::string state
) {}

在前面提到过,只有用户自己可以控制自己的记录,因为这个合约是选择加入的.为了达到这个目的,使用eosio.cdt提供的 require_auth 方法.这个方法接受一个name类型的参数,并断言执行交易的账户等同于提供的值.

void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
}

实例化表: 首先, 一个multi_index的表被配置好, 并将其声明为address_index. 去实例化一张表, 需要考虑它的两个所需参数:

1."code",它代表合约账户.可以通过scoped variable _code访问此值.

2."scope",它确保了合约的唯一性.在当前这个情况,因为我们只有一张表,我们可以使用"_code"

void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
}

下一步,从迭代器查询,将其赋值给一个变量因为它会使用好几次.

void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
  auto iterator = addresses.find(user.value);
}

安全性已经得到保证并且table也实例化好了,真棒!

接下来,编写创建或更改table的逻辑.检测特定用户是否已经存在.

为了实现,通过传入user参数来使用table的 find 方法.find方法会返回一个迭代器.使用该迭代器来对 end 函数测试."end"函数是"null"的别称.

void upsert(name user, std::string first_name, std::string last_name, 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() )
  {
    //The user isn't in the table
  }
  else {
    //The user is in the table
  }
}

在table中使用multi_index的方法 emplace 创建一个记录.这个方法接受两个参数,给该记录存储到链上支付RAM费用的"payer"和一个回调函数.

emplace方法的回调函数必须使用lambda来创建引用.在回调函数的函数体中将提供给upsert函数的值分配给row.

void upsert(name user, std::string first_name, std::string last_name, 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.street = street;
      row.city = city;
      row.state = state;
    });
  }
  else {
    //The user is in the table
  }
}

接下来,在"upsert"函数中使用 modify 方法处理修改或更新,传入一些参数:

  • 先前定义好的iterator,在调用此action时设置为声明的用户(我们将要修改的是该iterator中的数值).
  • "ram payer",当前用例应该是用户,就和设计该合约提议时决定的一样.
  • 处理表修改的回调函数.
void upsert(name user, std::string first_name, std::string last_name, 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.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.street = street;
      row.city = city;
      row.state = state;
    });
  }
}

addressbook合约现在已经有功能性action来允许用户在没有记录的时候创建,有记录的时候进行修改了.

但如果用户想完全移除记录呢?

Step 8: 从table移除记录

就像先前的一步,在addressbook创建一个public method,确保包含了ABI声明以及 require_auth 来确保action的参数user被验证,只有记录的拥有者才能修改他们的记录.

    void erase(name user){
      require_auth(user);
    }

实例化表.在addressbook,每个账户只有一个记录,用 find 来设置iterator.

...
    void erase(name user){
      require_auth(user);
      address_index addresses(_code, _code.value);
      auto iterator = addresses.find(user.value);
    }
...

一个合约不能清除一条不存在的记录,所以在执行前先断言该记录是存在的.

...
    void erase(name user){
      require_auth(user);
      address_index addresses(_code, _code.value);
      auto iterator = addresses.find(user.value);
      eosio_assert(iterator != addresses.end(), "Record does not exist");
    }
...

最后,调用 erase 以抹去迭代器.

...
  void erase(name user) {
    require_auth(user);
    address_index addresses(_code, _code.value);
    auto iterator = addresses.find(user.value);
    eosio_assert(iterator != addresses.end(), "Record does not exist");
    addresses.erase(iterator);
  }
...

现在,合约差不多完成了.用户可以创建,修改,和清除记录.但是,合约还没有准备好被编译.

Step 9:为ABI做准备

完成下面的步骤来完成合约.

9.1EOSIO_ABI

在文件的地步使用 EOSIO_ABI 宏,传入合约名和单独action "upsert".

该宏通过在我们的合约里分配调用到特定函数,以处理应用处理程序.

将下面的代码添加到addressbook.cpp的底部能让我们的cpp文件与EOSIO wasm解析器相兼容.引入该声明如果失败,可能会导致部署合约的时候发生错误.

EOSIO_DISPATCH( addressbook, (upsert) )

9.2 ABI Action Declarations

eosio.cdt 包含了一个ABI生成器,使用它需要对我们的合约进行一些小小的修改.

upserterase函数的上方都添加下面这C++11的声明:

[[eosio::action]]

该声明能将action的参数提取出来并创建必要的ABI struct声明到生成的ABI文件中.

9.3 ABI Table Declarations

在table中添加ABI声明.将你合约中private区域中的这一行:

struct person {

修改成:

struct [[eosio::table]] person {

[[eosio::table]]声明会将必须的描述添加到ABI文件中.

现在我们的合约准备好被编译了.

下面是addressbook合约的最终状态:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

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

public:
  using contract::contract;

  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, 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.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.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;
    std::string street;
    std::string city;
    std::string state;
    uint64_t primary_key() const { return key.value; }
  };
  typedef eosio::multi_index<"people"_n, person> address_index;

};

EOSIO_DISPATCH( addressbook, (upsert)(erase))

Step 10:编译合约

在你的terminal执行下面的命令.

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

Step 11:部署合约

先执行以下命令为合约创建账户:

cleos create account eosio addressbook EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV -p eosio@active

Result

executed transaction: 59643121a490244ee866277dc4e4676444d3e24b29c52cc020e580d10569f333  200 bytes  598 us
#         eosio <= eosio::newaccount            {"creator":"eosio","name":"addressbook","owner":{"threshold":1,"keys":[{"key":"EOS8QPw89hqrzohK6gKTH...
warning: transaction executed locally, but may not be confirmed by the network yet    ]

部署addressbook合约:

cleos set contract addressbook /Users/zhong/coding/CLion/contracts/addressbook -p addressbook@active

result:

5f78f9aea400783342b41a989b1b4821ffca006cd76ead38ebdf97428559daa0  5152 bytes  727 us
#         eosio <= eosio::setcode               {"account":"addressbook","vmtype":0,"vmversion":0,"code":"0061736d010000000191011760077f7e7f7f7f7f7f...
#         eosio <= eosio::setabi                {"account":"addressbook","abi":"0e656f73696f3a3a6162692f312e30010c6163636f756e745f6e616d65046e616d65...
warning: transaction executed locally, but may not be confirmed by the network yet    ]

Step 12:测试合约

添加一行记录到table

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

Result:

executed transaction: 003f787824c7823b2cc8210f34daed592c2cfa66cbbfd4b904308b0dfeb0c811  152 bytes  692 us
#   addressbook <= addressbook::upsert          {"user":"alice","first_name":"alice","last_name":"liddell","street":"123 drink me way","city":"wonde...

检查alice不能替其他用户添加记录

cleos push action addressbook upsert '["bob", "bob", "is a loser", "doesnt exist", "somewhere", "someplace"]' -p alice@active

和预期的一样,合约中的require_auth阻止了alice 创建/修改 其他用户的数据.

Result:

Error 3090004: Missing required authority
Ensure that you have the related authority inside your transaction!;
If you are currently using 'cleos push action' command, try to add the relevant authority using -p option.

检索 alice的记录

cleos get table addressbook addressbook people --lower alice --limit 1

Result:

{
  "rows": [{
      "key": "alice",
      "first_name": "alice",
      "last_name": "liddell",
      "street": "123 drink me way",
      "city": "wonderland",
      "state": "amsterdam"
    }
  ],
  "more": false
}

测试alice能否移除记录

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

Result:

executed transaction: 0a690e21f259bb4e37242cdb57d768a49a95e39a83749a02bced652ac4b3f4ed  104 bytes  1623 us
#   addressbook <= addressbook::erase           {"user":"alice"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]

再次查询,确保已经删除:

cleos get table addressbook addressbook people --lower alice --limit 1

Result:

{
  "rows": [],
  "more": false
}

看起来不错!

总结

你已经学习到如何配置表,初始化表,创建新的数据,更改已经存在的数据以及使用iterators.你学习到了如何通过一个空的iterator结果来测试,以及如何配置合约的ABI.