[EOS源码分析] EOS智能合约开发实践之数据库持久化
以太坊智能合约定义的全局变量的值是持久性的,就相当于智能合约一直在运行着。而EOS的智能合约更加接近我们平时使用的程序,每次执行action都相当于启动智能合约的一个新实例,一旦执行完,代码定义的变量就释放了,不会影响下一次执行环境。但是智能合约肯定需要有持久化存储的需求,比如永久保存智能合约代币的状态,不能代币转账执行完,代币的balance余额信息和转账前一样吧。这个持久化存储就是数据库存储数据。EOS允许智能合约定义自己的私有数据库表。比如下图,Apply Context的内容都是一次性的,一次action执行完成,对象就释放了,只有存储到EOSIO database的才被保存。
这个私有数据表是通过multi_index来访问和交互的。EOS的multi_index类似boost的multi_index,即多索引容器。有了多级索引,智能合约就具备了操作类似数据库模块的功能。eosio::multi_index支持主键,但是必须是唯一的无符号64位整数。eosio::multi_index中的对象容器按主键索引按无符号64位整数主键的升序排序。
申请私有数据表
定义一个multi_index对象即可
纯数据表
multi_index<table_name, record >
> table( code, scope );
这里的code必须是contract的account_name, 因为这个数据表的操作api都有check,比如'modify' 函数.
void modify( const T& obj, uint64_t payer, Lambda&& updater ) {
eosio_assert( _code == current_receiver(), "cannot modify objects in table of another contract" ); // Quick fix for mutating db using multi_index that shouldn't allow mutation. Real fix can come in RC2.
}
scope参数,这个参数的初衷是用来实现交易action并行执行的。我们知道,action之间可能是存在依赖的,比如alice转给bob 5块钱(action1),alice转给james 5块钱(action2), action1和action2就有依赖关系。因为如果Alice只有6块钱,那么这两个action中必然有一个action是失败的。如果这两个action并行执行那这两个都会成功,明显是不合理的。你可能会说可以使用互斥锁解决冲突啊?事实上市不行的,申请互斥锁中的两段代码的执行顺序是随机的,所以导致action1和action2的执行顺序是随机的,这就违背了区块链的基本要求---确定性和可重复性,每个节点按相同顺序执行一个action列表,结果应该是一致的。所以为了解决这个问题就提出了scope的。假设这个balance的数据保存在‘balance’数据表里,scope为'balance_scope', 所以在提交action1时就会传入一个scope参数'balance_scope‘,这样系统根据action1和action的scope很容易知道action1, action2都会操作'balance_scope'对应的'balance'表,就不会让这两个action并行执行。其实,这里的核心问题是action操作的数据表的信息,如果提交的action不告知系统,系统必须要执行完action才能知道操作了哪个数据表,这自然没法实现并行。当然,目前版本scope基本没有起作用,action并行老版本有实现过的框架(但不起作用),最新的版本框架甚至已经删除了相关逻辑,看来action的并行化路漫慢而修远兮。
纯数据表示例
eosio.token合约
struct currency_stats {
asset supply;
asset max_supply;
ccount_name issuer;
//必须有该函数
uint64_t primary_key()const { return supply.symbol.name(); }
};
typedef eosio::multi_index<N(stat), currency_stats> stats;
主键查找数据
纯数据表只能通过table.find函数以主键为参数查找数据
stats statstable( _self, sym.name() );
auto existing = statstable.find( sym.name() );
多索引数据表
基本格式
multi_index<table_name, record,
indexed_by<index_name, index_func>,
indexed_by<index_name_1, index_func_1>,
….>
> table( code, scope );
和纯数据表相比,就多了index_by索引定义
索引的定义格式是:indexed_by<索引名,索引键值函数>,比如这个实例:
eosio::multi_index<N(orders), limit_order,
indexed_by< N(byexp), const_mem_fun<limit_order, uint64_t, &limit_order::get_expiration> >,
indexed_by< N(byprice), const_mem_fun<limit_order, uint128_t, &limit_order::get_price> >
> orders( N(multitest), N(multitest) );
<N(orders), limit_order,
这部分和纯数据表是一样的,第一个参数是表名,第二个参数是表行对象。这里有两个索引对象。分别是indexed_by< N(byexp), const_mem_fun<limit_order, uint64_t, &limit_order::get_expiration>>
indexed_by< N(byprice), const_mem_fun<limit_order, uint128_t, &limit_order::get_price> >
N(byprice):索引名称是'byprice'
在const_mem_fun<limit_order, uint128_t,&limit_order::get_price>
中的const_mem_fun定义了一个索引键值函数,这个其实是通过boost::bind函数将对象的const成员函数转化为函数指针.其格式是<ObjectType, indexType(索引类型), &ObjectType::function>
索引查找数据
auto priceidx = orders.get_index<N(byprice)>();
print("Items sorted by price:\n");
for( const auto& item : priceidx ) {
print(" ID=", item.id, ", price=", item.price, ", owner=", name{item.owner}, "\n");
}
二级索引类型限制
- idx64 - 原始的64位无符号整数密钥.
- idx128 - 原始128位无符号整数密钥.
- idx256 - 256位固定大小的字典键.
- idx_double - 双精度浮点键.
- idx_long_double - 四倍精度浮点键.
查看数据表数据
$cleos get table cleos get table [OPTIONS] contract scope table
实例运行
源码
class tabletest : public eosio::contract {
private:
/// @abi table
struct contact {
account_name name;
uint64_t phone;
auto primary_key()const { return name; }
uint64_t get_phone()const { return phone; }
EOSLIB_SERIALIZE( contact, (name)(phone))
};
typedef eosio::multi_index<N(contact), contact, indexed_by< N(byphone), const_mem_fun<contact, uint64_t, &contact::get_phone> >
> contacts;
public:
using contract::contract;
/// @abi action
void add(account_name owner, account_name name, uint64_t phone ) {
//新增数据项
contacts contacttable( _self, owner );//(code, scope)
//(player, item),player域指由谁来支付这个数据存储费用
contacttable.emplace( _self, [&]( auto& o ) {
o.name = name;
o.phone = phone;
});
}
/// @abi action
void remove(account_name owner, account_name contact_name) {
//删除数据项
contacts contacttable( _self, owner );
name n{contact_name};
auto item = contacttable.find( contact_name );
if( item == contacttable.end() ) {
print_f("Not found name:%sn", n.to_string().data());
} else {
contacttable.erase(item);
};
}
/// @abi action
void modify(account_name owner, account_name contact_name, uint64_t phone) {
//修改数据项
contacts contacttable( _self, owner );
name n{contact_name};
auto item = contacttable.find( contact_name );
if( item == contacttable.end() ) {
print_f("Not found name:%sn", n.to_string().data());
} else {
contacttable.modify( item, _self, [&]( auto& o ) {
o.phone = phone;
});
};
}
/// @abi action
void findbyname(account_name owner, account_name contact_name) {
//根据name主键查找数据项
contacts contacttable( _self, owner );
name n{contact_name};
auto item = contacttable.find( contact_name );
if( item == contacttable.end() ) {
print_f("Not found name:%s\n", n.to_string().data());
} else {
n = name{item->name};
print_f("Found phone:%, for %s\n", item->phone, n.to_string().data());
};
}
/// @abi action
void findbyphone(account_name owner, uint64_t phone) {
//根据phone索引查找数据项
contacts contacttable( _self, owner );
auto phoneinx = contacttable.get_index<N(byphone)>();
auto item = phoneinx.find(phone);
if( item == phoneinx.end() ) {
print_f("Not found phone:%\n", phone);
} else {
name n{item->name};
print_f("Found name:%s for phone:%\n", n.to_string().data(), item->phone);
};
}
};
EOSIO_ABI( tabletest, (add) (remove) (modify) (findbyname) (findbyphone))
编译
请参考【EOS编写HelloWorld智能合约】
执行
$cleos create account eosio table.code $KEY_PUB_1 $KEY_PUB_1
$cleos set contract table.code ./table -p table.code
$cleos create account eosio itleaks $KEY_PUB_2 $KEY_PUB_2
$cleos push action table.code add '[ "itleaks", "jackma", "13456" ]' -p itleaks
$cleos get table table.code itleaks contact
源码一键实践
https://github.com/itleaks/eos-contract/tree/master/table-exp