跳至主要内容
此页面是从英文翻译而来的。请注意,与原始页面相比,可能会出现错误或差异。真实的文档来源应始终是英文版本。

状态数据

智能合约中有两种类型的数据:状态数据和瞬态数据。状态数据是存储在 区块链,并且是持久性的。瞬态数据是指在事务执行期间存储的数据,而不是 持久。交易结束的那一刻,瞬态数据就消失了。

在区块链上存储数据有两个部分:模型和表。模型就是你的数据结构 将存储,而表是存放数据的容器。有几种不同类型的表格,每种都有 一个有自己的用例。

数据模型

模型是您将存储在 EOS++ 表中的数据结构。它是一个可序列化的 C++ 结构,可以包含 任何也可以序列化的数据类型。所有常见的数据类型都是可序列化的,您也可以创建自己的数据类型 可序列化的数据类型,例如其他以 TABLE 关键字。

TABLE UserModel {
uint64_t id;
name eos_account;

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

定义模型与定义 C++ 结构非常相似,但有一些区别。第一个区别是你 必须使用 TABLE 用关键字代替 struct 关键字。第二个区别是你必须定义一个 primary_key 返回 a 的函数 uint64_t。此函数用于确定表的主键,该主键用于 为表编制索引。

可以把它想象成一个 NoSQL 数据库,其中主键是表的索引。主键用于 确定数据在表中的位置,并用于轻松高效地从表中检索数据。

主键数据类型

主键必须uint64_t (你也可以使用 name.value),并且该表中每一行都必须是唯一的。这意味着你不能 有两行具有相同主键。如果需要有多行具有相同主键,则可以使用 二级索引。

次要密钥数据类型

二级索引比主键更灵活,可以是以下任何一种数据类型:

  • uint64_t
  • uint128_t
  • double
  • long double
  • checksum256

它们也可以包含重复值,这意味着您可以使用相同的辅助键拥有多行。

💰 成本注意事项

每个索引每行消耗 RAM,因此只有在需要时才使用二级索引。如果你不需要查询表 通过某个字段,则不应为该字段创建索引。

付款人和范围

在我们深入研究如何在表中存储数据之前,我们需要先谈一谈 scopepayer

RAM Payer

RAM 付款人是支付用于存储数据的 RAM 的账户。这要么是那个账户 正在调用合约,或者调用合约本身。这有时在很大程度上依赖于博弈论,而且可能很复杂 决定。现在,您只需要使用合约本身作为RAM付款人。

您也不能拥有不属于交易授权的账户,为RAM付费。

💰 当心 RAM

RAM 是 EOS 区块链上的有限资源,你应该谨慎考虑允许其他人使用多少 RAM 你的合同。通常最好让用户为 RAM 付费,但这需要你为他们制定激励措施 花费自己的 RAM 来换取被认为等值或更高价值的东西。

作用域

表的范围是进一步隔离表中数据的一种方法。它是一个 uint64_t 用来确定 数据存储在哪个 bucket 中。

如果我们把数据库想象成一个 JSON 对象,它可能看起来像这样:

tables.json
{
"users": {
1: [
{
"id": 1,
"eos_account": "bob"
},
{
"id": 2,
"eos_account": "sally"
}
],
2: [
{
"id": 1,
"eos_account": "joe"
}
]
}
}

如上所示,您可以在不同的作用域中使用相同的主键,而不会发生冲突。这在各种不同情况下都很有用: -如果你想存储每个用户的数据 -如果你想存储每个游戏实例的数据 -如果你想存储每个用户库存的数据 -等等

多索引表

多索引表是在 EOS 区块链上存储数据的最常用方式。它是一个持久的键值存储 可以通过多种方式建立索引,但始终具有主键。回到 NoSQL 数据库的类比,你可以想 将多索引表视为文档的集合,而每个索引是从集合中查询或获取数据的不同方式。

定义表

要创建多索引表,必须使用至少一个主键定义一个模型。然后,您可以创建多索引 使用以下方法的表 multi_index 模板,然后传入表/集合的名称和要使用的模型。

TABLE UserModel ...

using users_table = multi_index<"users"_n, UserModel>;

这将创建一个名为的表 users 它使用 UserModel 模型。然后,您可以使用此表来存储和检索 来自区块链的数据。

实例化表

要对表执行任何操作,必须先对其进行实例化。为此,您必须传入拥有该表的合约, 以及你要使用的范围。

ACTION test() {
name thisContract = get_self();
users_table users(thisContract, thisContract.value);

插入数据

现在您已经引用了实例化表,可以向其中插入数据。为此,你可以使用 emplace 函数,它采用一个 lambda/anonymous 函数,该函数接受对要插入的模型的引用。

...

name ramPayer = thisContract;
users.emplace(ramPayer, [&](auto& row) {
row.id = 1;
row.eos_account = name("eosio");
});

也可以先定义一个模型,然后将其插入到整行中。

UserModel user = {
.id = 1,
.eos_account = name("eosio")
};

users.emplace(ramPayer, [&](auto& row) {
row = user;
});

检索数据

要从表中检索数据,您将使用 find 表上的方法,它采用该行的主键 你想找回。这将返回该行的迭代器(引用)。

auto iterator = users.find(1);

你需要检查你是否真的找到了该行,因为如果你没有,那么迭代器将等于 end 迭代器, 这是一个特殊的迭代器,代表表的结尾。

if (iterator != users.end()) {
// You found the row
}

然后,您可以通过两种方式访问行中的数据。第一种方法是使用 -> 操作员,它会给你 指向行数据的指针,第二种方法是使用 * 运算符,它将为您提供该行的原始数据。

UserModel user = *iterator;
uint64_t idFromRaw = user.id;
uint64_t idFromRef = iterator->id;

修改数据

如果你想打电话 emplace 两次你会因为主键已经存在而出错。修改数据 在表中,必须使用 modify 方法,它引用你想要修改的迭代器、RAM 支付者, 还有一个允许我们修改数据的 lambda/匿名函数。

users.modify(iterator, same_payer, [&](auto& row) {
row.eos_account = name("foobar");
});

什么是 same_payer

那个 same_payer 变量是一个特殊变量,用于指示 RAM 付款人应与 RAM 支付者相同 原始 RAM 付款人。当你想修改表中的数据,但你没有原始 RAM 付款人时,这很有用 授权。当你想修改自己插入的合约表中的数据时,通常会出现这种情况 使用其他用户的 RAM。您将无法向数据添加任何字段,但如果您只修改现有字段 那么内存使用量就没有增量了,因此无需支付任何额外的 RAM 费用或退还任何剩余的 RAM。

移除数据

要从表中移除数据,必须使用 erase 方法,它引用要删除的迭代器。

users.erase(iterator);

使用二级索引

使用二级索引将允许您以不同的方式查询表。例如,如果你想查询你的 旁边的桌子 eos_account 字段,则需要在该字段上创建二级索引。

重新定义我们的模型和表格

要使用二级索引,必须先在模型中对其进行定义。要做到这一点,请使用 indexed_by 模板,然后传递 在索引的名称中,以及索引的类型。

TABLE UserModel {
uint64_t id;
name eos_account;

uint64_t primary_key() const { return id; }
uint64_t account_index() const { return eos_account.value; }
};

using users_table = multi_index<"users"_n, UserModel,
indexed_by<"byaccount"_n, const_mem_fun<UserModel, uint64_t, &UserModel::account_index>>
>;

那个 indexed_by 模板可能有点混乱,所以让我们分解一下。

indexed_by<
<name_of_index>,
const_mem_fun<
<model_to_use>,
<index_type>,
<pointer_to_index_function>
>
>

那个 name_of_index 是您要使用的索引的名称。这可以是任何东西,但最好用点东西 这描述了索引的用途。

那个 model_to_use 是您要用于索引的模型。这通常与您使用的模型相同 桌子,但不一定是。如果你想为索引使用不同的模型,但仍然想要,这很有用 以便能够访问表中的数据。

那个 index_type 是索引的类型,仅限于我们前面讨论的类型。

那个 pointer_to_index_function 是一个指向函数的指针,该函数返回要用于索引的值。这个 函数必须是 const_mem_fun 函数,并且必须是用于索引的模型的成员函数。

使用二级索引

现在您有了二级索引,您可以使用它来查询您的表。为此,首先从表中获取索引,然后 然后使用 find 在索引上使用方法,而不是直接在表上使用它。

auto index = users.get_index<"byaccount"_n>();
auto iterator = index.find(name("eosio").value);

要使用二级索引修改表中的数据,可以使用 modify 索引上的方法,而不是使用它 直接放在桌子上。

index.modify(iterator, same_payer, [&](auto& row) {
row.eos_account = name("foobar");
});

Singleton 表

A singleton table 是一种特殊类型的表,每个作用域只能有一行。这对于存储以下数据很有用 你只想拥有一个实例,比如配置或玩家的物品栏。

a之间的主要区别 singleton 表和多索引表是: -单例不需要模型上的主键 -Singleton 可以存储任何类型的数据,而不仅仅是预定义的模型

定义表

要定义单例表,可以使用 singleton 模板,然后传入表的名称和类型 您要存储的数据。

您还必须导入 singleton.hpp 头文件。

#include <eosio/singleton.hpp>

TABLE ConfigModel {
bool is_active;
};

using config_table = singleton<"config"_n, ConfigModel>;

using is_active_table = singleton<"isactive"_n, bool>;

那个 singleton 模板等同于 multi_index 模板,唯一的不同是它不支持二级索引。

您已经定义了一个用于存储 ConfigModel,还有另一张存储 a 的表 bool。两张桌子都有 完全相同的数据,但是 bool table 效率更高,因为它不需要存储增加的开销,即 由于 ConfigModel 结构。

实例化表

就像 multi_index table,必须先实例化该表,然后才能使用它。

name thisContract = get_self();
config_table configs(thisContract, thisContract.value);

那个 singleton table 在其构造函数中采用两个参数。第一个参数是表格所在的合约 归所有,第二个参数是 scope

获取数据

有几种方法可以从中获取数据 singleton

要么获得,要么失败

如果没有现有数据,这将在运行时出错。 为了防止这种情况,你可以使用 exists 检查是否存在数据的方法。

if (!configs.exists()) {
// handle error
}
ConfigModel config = configs.get();
bool isActive = config.is_active;

获取或默认

这将返回默认值,但不会保留该值。

ConfigModel config = configs.get_or_default(ConfigModel{
.is_active = true
});

获取或创建

这将返回默认值,并且保留该值。

ConfigModel config = configs.get_or_create(ConfigModel{
.is_active = true
});

设置数据

将数据保存在 singleton,你可以使用 set 方法,它引用要设置的数据。

configs.set(ConfigModel{
.is_active = true
}, ramPayer);

移除数据

一旦你实例化了一个 singleton,很容易将其移除。刚刚打电话给 remove 实例本身上的方法。

configs.remove();