2.4 数据的持久化
为了学习数据的持久化,写一个简单的地址薄合约.虽然这个例子因为各种原因作为生产环境的合约不太实用,但它是一个很好的合约用来学习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生成器,使用它需要对我们的合约进行一些小小的修改.
在upsert
和erase
函数的上方都添加下面这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.