上篇文章发出来之后,群内前辈@郭其淼 针对性的提出两个问题:
1、 创建用户时可以用emplace的返回值作为已创建对象的编号。
2、 未对Multi-Index的一个关键性二级索引作出说明。
今天我们结合这两个问题,去看看Multi-Index中的相关函数的实现,以及Multi-Index和chainbase之间的交互。
本文主要包含有以下内容:
Multi-Index中增、删、改、查的实现
Multi-Index和chainbase之间的交互
Multi-Index中增、删、改、查的实现
Multi-Index的实现集中在multi-index.hpp里,可参考:https://github.com/EOSIO/eos/blob/master/contracts/eosiolib/multi_index.hpp
以问题1为例,primary-key可以用emplace的返回值替代,那么emplace返回的到底是什么呢?emplace的代码实现如下:
1 template
2 const_iterator emplace( uint64_t payer, Lambda&& constructor ) {
3 using namespace _multi_index_detail;
4 ......
5 //1、做判断,不能在别的合约数据表内创建对象
6 //2、创建对象并根据对象大小使用malloc分配内存
7 //3、数据使用datastream写入对象
8 ......
9 auto pk = obj.primary_key();
10
11 i.__primary_itr = db_store_i64( _scope, TableName, payer, pk, buffer, size );
12
13 if ( max_stack_buffer_size < size ) {
14 free(buffer);
15 }
16
17 if( pk >= _next_primary_key )
18 _next_primary_key = (pk >= no_available_primary_key) ? no_available_primary_key : (pk + 1);
19 //4、二级索引的处理
20
21 const item* ptr = itm.get();
22 auto pk = itm->primary_key();
23 auto pitr = itm->__primary_itr;
24
25 _items_vector.emplace_back( std::move(itm), pk, pitr );
26
27 return {this, ptr};
28 }
由于代码内容较多,其中一部分转化为我个人的总结,详细内容可参考代码中的注释内容。在获取到当前对象的primary_key之后调用db_store_i64函数将数据存储到chainbase中,关于db_store_i64我们在本文的下半部分会具体提到。再来看emplace函数分别传入了什么参数和返回了什么,emplace函数的相关注释如下:
1* Adds a new object (i.e., row) to the table.(在数据表中创建一个新的对象)
2* @brief Adds a new object (i.e., row) to the table.
3* @param payer - Account name of the payer for the Storage usage of the new object
4//谁创建,谁支付内存消耗的费用
5* @param constructor - Lambda function that does an in-place initialization of the object to be created in the table
6//Lambda函数,可以直接初始化刚才创建对的对象
7* @pre A multi index table has been instantiated //前置条件:multi-index已经被初始化
8* @post A new object is created in the Multi-Index table, with a unique primary key (as specified in the object). The object is serialized and written to the table. If the table does not exist, it is created.
9//使用emplace之后:
10//带有唯一主键的新对象在multi-index表中被创建;
11//这个对象会被序列化,然后写入表中;
12//如果表不存在,则创建表。
13* @post Secondary indices are updated to refer to the newly added object. If the secondary index tables do not exist, they are created.
14//二级索引被更新,用以引用新添加的对象;
15//如果二级索引表不存在,则创建它们。
16* @post The payer is charged for the storage usage of the new object and, if the table (and secondary index tables) must be created, for the overhead of the table creation.
17//payer为创建新对象所使用的存储付费;
18//如果multi-index表和二级索引表需要被创建,则payer为表的创建付费。
19 * @return A primary key iterator to the newly created object
20//返回一个新创建的对象的主键迭代器
21* Exception - The account is not authorized to write to the table.
22//异常:没有权限去写入这张数据表
通过官方注释我们可以知道,emplace返回了一个新创建对象的主键迭代器,而从代码中可以看出,ptr调用了get函数,而get又使用了我们上篇文章中所用到的find,当然上篇文章中我们也使用了get根据主键的id来获取英雄的相关信息。我们继续来看get和find:
1//get
2const T& get( uint64_t primary, const char* error_msg = "unable to find key" )const {
3 auto result = find( primary );
4 eosio_assert( result != cend(), error_msg );
5 return *result;
6 }
7//find
8const_iterator find( uint64_t primary )const {
9 auto itr2 = std::find_if(_items_vector.rbegin(), _items_vector.rend(), [&](const item_ptr& ptr) {
10 return ptr._item->primary_key() == primary;
11 });
12 if( itr2 != _items_vector.rend() )
13 return iterator_to(*(itr2->_item));
14
15 auto itr = db_find_i64( _code, _scope, TableName, primary );
16 if( itr < 0 ) return end();
17
18 const item& i = load_object_by_primary_iterator( itr );
19 return iterator_to(static_cast(i));
20 }
可以看出,这其实和我们上篇文章中所使用的get来获取英雄相关信息是殊途同归,也就是使用emplace的返回结果也是该主键的一个迭代器。同时我们可以发现在find中使用了db_find_i64,他也包含在我们接下来要说的二级索引以及和chainbase的交互中,现在我们来将上篇文章中的代码稍作修改,然后再执行create action看看emplace的返回结果:
1void tianlongbabu::create(account_name heroname,uint64_t heroforceidx,uint64_t heroinsideidx,string herodes,string heroborn)
2{
3 print(name{heroname},"出生在:",heroborn,",现在是",herodes,",他的武力值:",heroforceidx,",他的内力值:",heroinsideidx);
4 uint64_t heroindex = 0;
5 auto dbhero = ht.emplace(_this_contract,[&](auto &p)
6 {
7 p.heroid = ht.available_primary_key(); //主键自增
8 heroindex = p.heroid;
9 p.herodes = herodes;
10 p.heroborn = heroborn;
11 p.heroforceidx = heroforceidx;
12 p.heroinsideidx = heroinsideidx;
13
14 });
15 print("--emplace返回该英雄的编号:",dbhero->heroid);
16}
17//push action
18ubuntu@VM-16-6-ubuntu:~/eos/contracts/tlbb$ cleos push action tlbb.code create '["xiaofeng","1000","500","丐帮帮主","辽国"]' -p xiaofeng
19executed transaction: a92821dacc552d8cb9a9b60330a878b4d95e32a11a148fe7559a7189d6253e89 136 bytes 1274 us
20# tlbb.code <= tlbb.code::create {"heroname":"xiaofeng","heroforceidx":1000,"heroinsideidx":500,"herodes":"丐帮帮主","heroborn":"...
21>> xiaofeng出生在:辽国,现在是丐帮帮主,他的武力值:1000,他的内力值:500--emplace返回该英雄的编号:0
看完了第一个问题,我们再来看看关于二级索引的使用,在Multi-Index.hpp中对二级索引做了部分介绍可以参考下官方的例子来实现下,这里实现一个action并简单的测试下,根据英雄的武力值来查找该英雄的信息:
1//关于hero_table的multi-index的声明修改如下:
2typedef eosio::multi_index>> heros_table;
3heros_table ht;
4//根据武力值二级索引来查找天龙英雄的相关信息
5void tianlongbabu::selectbyfidx(uint64_t heroforceidx)
6{
7 auto force_index = ht.get_index();
8 auto findhero = force_index.lower_bound(heroforceidx);
9 eosio_assert(findhero != force_index.end(),"您查找的英雄不存在");
10 print("武力值为",heroforceidx,"的英雄其编号为",findhero->heroid);
11}
12//push action
13ubuntu@VM-16-6-ubuntu:~/eos/contracts/tlbb$ cleos push action tlbb.code selectbyfidx '["1000"]' -p tlbb.code
14executed transaction: b2ccddeacabe7f9b5abe7f92725dd91c0f028ed1da414f276597d821def3c9dd 104 bytes 1013 us
15# tlbb.code <= tlbb.code::selectbyfidx {"heroforceidx":1000}
16>> 武力值为1000的英雄其编号为0
Multi-Index和chainbase之间的交互
在上面的内容中我们多次提到了db_store_i64以及db_find_i64,通过这两个函数来实现数据的存储及查找,那么这两个函数又分别是什么呢?我们以db_store_i64为例来看Multi-Index和chianbase之间是如何关联并实现数据的增、删、改、查操作的,代码如下:
1int apply_context::db_store_i64( uint64_t code, uint64_t scope, uint64_t table, const account_name& payer, uint64_t id, const char* buffer, size_t buffer_size ) {
2// require_write_lock( scope );
3 const auto& tab = find_or_create_table( code, scope, table, payer );
4 auto tableid = tab.id;
5
6 FC_ASSERT( payer != account_name(), "must specify a valid account to pay for new record" );
7
8 const auto& obj = db.create( [&]( auto& o ) {
9 o.t_id = tableid;
10 o.primary_key = id;
11 o.value.resize( buffer_size );
12 o.payer = payer;
13 memcpy( o.value.data(), buffer, buffer_size );
14 });
15
16 db.modify( tab, [&]( auto& t ) {
17 ++t.count;
18 });
19 //而后我们再来看db.create,其最终还是会走向chainbase中的emplace
20 /**
21 * Construct a new element in the multi_index_container.//使用boost::multi-index容器创建一个新的元素
22 * Set the ID to the next available ID, then increment _next_id and fire off on_create().//将id设置为下一个可用id
23 */
24 template
25 const value_type& emplace( Constructor&& c ) {
26 auto new_id = _next_id;
27
28 auto constructor = [&]( value_type& v ) {
29 v.id = new_id;
30 c( v );
31 };
32
33 auto insert_result = _indices.emplace( constructor, _indices.get_allocator() );
34
35 if( !insert_result.second ) {
36 BOOST_THROW_EXCEPTION( std::logic_error("could not insert object, most likely a uniqueness constraint was violated") );
37 }
38
39 ++_next_id;
40 on_create( *insert_result.first );
41 return *insert_result.first;
42 }
可以看出,db_store_i64的最终归宿还是通过chainbase走向了boost::multi-index,在chainbase.hpp中还有若干操作来实现对数据的增、删、改、查,代码阅读起来相对较难,因笔者能力和笔力有限,不再对和boost::multi-index的相关内容做分析,感兴趣的朋友可以一起来讨论。
在db_store_i64中我们发现了一个很有趣的函数update_db_usage,我们知道在eosio整个系统中ram扮演着举足轻重的角色,没有ram不管是开发者还是代币持有人都无法在主网上进行相应的操作。而ram的使用规则也很明了,即:谁使用,谁支付,那么这个使用的限定又是什么?什么样的数据写入需要使用ram?update_db_usage实现了什么功能?这其中的buffer_size和我们真正使用的内存之间有什么关系?我们在下篇文章中继续讨论。
本文主要针对上篇文章中存在的问题作出解释说明,并从源码的角度给出相应的答案。包含有emplace的返回值探讨,Multi-Index二级索引的查询使用,Multi-Index和chainbase之间是如何建立连接的,在本文的最后留下update_db_usage的相关疑问,待下一步学习探讨。
如果你觉得我的文章对你有一定的帮助,请点击文章末尾的喜欢该作者。
如果你对eos开发感兴趣,欢迎关注本公众号,一起学习eos开发。
微信公众号
有任何疑问或者指教请添加本人个人微信,当然有对eos开发感兴趣或者金庸粉的也可以添加一起交流,备注eos开发或金庸。
个人微信号