2.4 数据一致性

要了解数据持久性,我们将编写一个简单的智能合约,作为地址簿。 虽然由于各种原因,这个例子在实际生产中不是一个好的智能合约,但就学习EOSIO数据一致性而言,它是一个很好的合约。

第一步:创建一个新目录

可以直接切换至你之前创建的目录

cd CONTRACTS_DIR

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

mkdir addressbook
cd addressbook

第二步:创建新文件并打开

touch addressbook.cpp

用你喜欢的编辑器打开文件

第三步:编写扩展标准类

在之前的教程中,通过创建一个hello world合约,您已学习了相关基础知识。 您一定对下面的结构十分熟悉,但这次我们对类地址簿的命名反映了合约的目的。

##include 
##include
using namespace eosio;

class addressbook : public eosio::contract {
public:

private:

};

第四步:为表创建数据结构

我们需要一个表示地址簿数据结构的结构体。 在此,我们使用成员定义多索引表的结构体。 由于它是一个地址簿,我们的表将包含“用户”,因此我们将创建一个名为“person”的结构体

struct person {
};

首先,我们给主键赋予唯一值。 主键必须是uint64_t类型。 我们将使用一个名为“key”的字段。 此合约将为每个用户提供一个唯一入口,因此该密钥需为基于用户的“account_name”的一致且有保证的唯一值。 我们将在本教程后面部分说明它的工作原理。

struct person {
uint64_t key;
};

由于此合约是地址簿,我们希望存储每个人的详细信息。

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

我们现在已设置好基本结构,现在我们需要定义表的主键,该主键将由multi_index迭代器使用。 每个multi_index架构都需要一个主键。 为实现上述目的,您需创建一个名为primary_key()并返回一个值,在本例中该值为结构中定义的键成员。

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

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

当表中包含数据时无法被修改。 如果您需要以任何方式更改表的架构则需要删除其所有行。

第五步:配置多索引表

现在已经使用结构体定义了表,我们接下来需要配置表。 需要对eosio :: multi_index构造函数进行命名和配置以使用我们之前定义的结构体。

typedef eosio::multi_index<N(people), person> address_index;
  1. 使用N宏(N)命名表,它能将account_name类型转换为字符串。
  2. 我们传入上一步中定义的person结构体
  3. 声明了这个表的类型,用于实例化表。
    //configure the table
    typedef eosio::multi_index<N(people), person> address_index;

至此,结果应如下所示。

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

using namespace eosio;

class addressbook : public eosio::contract {

public:

private:
struct [[eosio::table]] person {
uint64_t 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; }
};

typedef eosio::multi_index<N(person), person> address_index;

};

第六步:构造函数

使用C ++类时,创建的第一个方法是使用构造函数。
我们的构造函数用于设置最初合约。EOSIO合同增加了合约等级。 因此,我们首先使用合同范围初始化父类合约。 我们的构造函数传递的范围参数是正在部署合约的区块链上的帐户。

addressbook(account_name self): contract(self){}

第七步:向表中添加记录

之前我们设置了多索引表的主键。 我们规定此合约仅为每个用户存储一条记录。 为了使一切顺利,我们必须建立一些假设

  1. 用户是能修改通讯簿的唯一帐户。
  2. 我们表的主键是基于用户名的唯一的。
  3. 为实用性,我们应该能够通过单次操作创建和修改表行。

我们知道该链具有唯一的帐户,因此在该例中,可选则账户名作为主键。 账户名类型是uint64_t。我们需要为用户定义一个添加或更新记录的操作。 此操作需要能够放置(创建)或修改的任何值。
我们对定义进行规范化,以便更容易阅读。 为了简化用户体验和界面,我们需要有一个方法负责创建和修改行。 因此,我们将其命名为“upsert”,即“更新”和“插入”的组合。

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

这个合约具有灵活性,但之前我们提及希望只有用户对自己的记录有控制权。 为此,我们将使用eosio.cdt自带的require_auth来实现该目的。 此方法接受一个参数,即account_name类型,并需要执行的帐户等于提供的值。

void upsert(account_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 _self variable.
  2. scope, 定义与ram fee有关的合约的付款人

接下来,我们需要查询迭代器,将其设置为变量。 我们将多次使用这个迭代器,并且会比多次调用它看到性能改进。

现在我们已经确保了安全性并实例化了表,我们下一步需编写用于创建或修改表的逻辑。 我们需要做的第一件事是检测特定用户是否已经存在。
为此,我们将通过传递user参数来使用table的find方法。 find方法将返回一个迭代器。 我们将使用该迭代器来对照end方法进行测试。 “end”方法是“null”的别名。
C++

void upsert(account_name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
require_auth( user );
auto iterator = addresses.find( user );
address_index addresses(_self, _self );
if( addresses.find( user ) == addresses.end() )
{
//The user isn't in the table
}
else {
//The user is in the table
}
}

首先,我们先来看使用multi_index方法设置表中的记录。 此方法接受两个参数,即此记录的“scope”和回调函数。
emplace方法的回调函数必须使用lamba来创建引用。 在主体中分配行的值和提供给upsert的值。

void upsert(account_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(_self, _self );
auto iterator = addresses.find( user );
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方法,传递一些参数

现在我们能实现用户在表中为自己创建新一栏数据及对已有数据进行编辑的操作。但是如果用户想要完全删除记录呢?

第七步:从表中移除数据

与前面的步骤类似,我们将在地址簿中创建一个方法,确保其包含ABI声明和require_auth来确保用户只能访问其记录

[[eosio::action]]
void erase(account_name user){
require_auth(user);
}

我们将实例化该表。 在地址簿中,每个帐户只有一个记录。 我们用find来设置迭代器。

...
[[eosio::action]]
void erase(account_name user){
require_auth(user);
address_index addresses(_self, _self);
auto iterator = addresses.find(user);
}
...

由于无法删除不存在的记录,因此我们判断记录确实存在

最后,我们将调用erase方法来擦除迭代器。

我们现在已经写完了大部分的合同。 用户可以用于创建,修改和删除记录。 但我们还没有准备好编译合约。

第八步:为ABI做准备

最后,我们需要采取一些步骤来准备我们的合同编译。

8.1 EOSIO_ABI

在文件的底部,我们需要使用EOSIO_ABI宏来传递合同的名称以及我们唯一的动作“upsert”。此宏处理wasm用于将调用分派给合同中的特定方法的apply处理程序。将以下内容添加到addressbook.cpp的底部将使我们的cpp文件与EOSIO的wasm解释器兼容。 未能包含此声明可能会在部署合同时导致错误。

Copy

EOSIO_ABI( addressbook, (upsert) )

8.2 ABI Action Declarations

eosio.cdt 包括一个ABI生成器,但为了它的工作将要求我们在我们的合约中添加一些小的声明。在我们的“upsert”函数之上,我们将添加以下C ++ 11声明

上述声明将提取操作的值,并在生成的abi文件中创建必要的ABI描述。

8.2 ABI 表声明

现在我们需要在表中添加一个ABI声明。 修改合约私有区域中定义的以下行:

复制

struct person {

至此:

[[eosio.table]]声明将向ABI文件添加必要的说明。现在我们的合约已经做好了,以下是我们的地址簿合约的最终状态:

第九步:编译合约

从终端执行以下命令。

第十步:部署合同

首先,我们需要为合同创建一个帐户,执行以下shell命令

现在我们来部署现有合同帐户。

第十一步:检测合约

现在我们来尝试加一栏数据

接下来,我们进行测试以确保alice无法为其他用户添加记录。

正如预期的那样,我们合同中的require_auth阻止了alice创建/修改另一个用户的行的行为。

现在让我们看看我们是否可以检索“alice”账户的记录。

下一步我们来测试“alice”账户是否能移除记录。

Copy

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

下一步我们来检查数据是否被移除

结果

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

总结

您已经学习了如何配置表,实例化表,创建新行,修改现有行以及如何使用迭代器。 您已经学习了如何针对空迭代器结果进行测试,以及如何配置合同的ABI。

原文链接:https://developers.eos.io/eosio-home/docs/data-persistence