通过本文,你将学会:1\区块链应用和传统应用在数据存储层的不同之处;2\使用区块链进行数据存储时遇到的约束;3\Substrate可用的存储数据类型和使用方法。
通过本文,你将学会:
如果想更好的理解本文的内容,最好有Substrate runtime的开发经验,你可以根据官方的教程(Proof Of Existence 或 Cryptokitties)来实践,也可以参考本专栏的其它文章。需要说明的是Substrate源码正在快速迭代更新中,部分语法可能不适用。遇到任何问题欢迎到相应的渠道来咨询。本文源码位于kaichaosun/play-substrate。
在传统web应用开发领域,数据库相关内容的设计和操作是极为重要的一部分。底层数据库可以分为:
以关系型数据为例,在开发过程中通常涉及以下几个方面:
区块链作为去中心应用最典型的一种形式,被很多开发者所热衷。区块链应用通常有这样几个特点:
一个已经存在的业务,通过使用区块链的技术、去中心的思想,或许能绽放出新的生命力。
典型的区块链应用如Bitcoin和Ethereum,它们的客户端软件依赖高效的键值对数据库,比如Bitcoin core 和 Ethereum Go 客户端使用的是LevelDB,Parity Ethereum 和 Substrate 内置的是RocksDB。
现实世界的霸权,导致了区块链应用受到大家的追捧,越来越多的开发者、创业者选择区块链作为自己业务的载体,然而它目前的基础设施还不足以支撑过于复杂的业务场景,以存储为例比如:
现在转向今天我们关注的主题,当使用Subsrate开发一条应用链的时候,可以用到哪些存储数据类型和它们相应的操作API。
需要指出的是,存储数据结构的设计需要结合自己的业务进行高度定制。在Substrate的开发过程中不涉及关系表的设计,而是通过它定义的一套标准化接口对数据库中存储的键值对进行增删改查的操作,开发者只需要关注自己的业务,而无需过多地关注与数据库底层的交互,真正地从繁杂的底层开发中解放出来。
Substrate作为一个通用的区块链开发框架,提供了丰富的数据类型用于在链上存储数据。它是基于Rust语言开发的,所支持的数据类型是Rust原生类型的子集(定义在核心库和alloc库中),以及这些原生类型构成的映射类型,同时要满足一定的编解码条件。我们通常把它们分为以下四种:
Rust提供了丰富的基本类型和组合类型,大部分可以在runtime开发中直接使用,并且Substrate还内置了一些独有的类型可以方便地开发去中心应用,部分类型如下表所示:
定义枚举类型:
#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug)]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Other,
}
impl Default for Weekday {
fn default() -> Self {
Weekday::Monday
}
}
存储的数据需要有默认值,所以需要为自定义的枚举类型实现Default接口,并且让Rust编译器自动为我们生成其它所需的接口实现,比如Copy、Clone、Eq,其中Encode和Decode是Substrate用来编解码存储内容的接口,需要提前引入依赖:use codec::{Encode, Decode};。
接着就可以定义自己的枚举存储单元:
MyEnum get(my_enum): Weekday;
增删改查基本用法同u8,示例代码如下:
// 转换u8为自定义的枚举
impl From<u8> for Weekday {
fn from(value: u8) -> Self {
match value {
1 => Weekday::Monday,
2 => Weekday::Tuesday,
3 => Weekday::Wednesday,
_ => Weekday::Other,
}
}
}
// 进行存储
let weekday: Weekday = workday.into();
MyEnum::put(weekday);
定义我们的结构体,和枚举类似,夜需要实现所需的接口:
#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, Default)]
pub struct People {
name: Vec<u8>,
age: u8,
}
增删改查基本用法同u8,示例代码如下:
let people = People {
name: input_name,
age: input_age,
}
MyStruct::put(value);
let my_people = MyStruct::get();
my_people.name;
my_people.age;
总结:对于单值类型的存储单元,除了表中列出的一些常用操作如get,put,mutate,kill等,Substrate还提供了更多可用的API,具体请参考文档 Trait frame_support::storage::StorageValue。如果需要参考更多的示例代码,可以在frame模块下找到。
即map,用来保存键值对,所有上面支持的单值类型都可以用作map中的key或者value。其中,key需要实现FullEncode接口,value需要实现FullCodec接口,而单值类型实现了FullCodec接口,而FullCodec继承自FullEncode。
定义map:
MyMap get(my_map): map u8 => Vec<u8>;
基本使用方法:
// 插入一个元素
MyMap::insert(key, value);
// 通过key获取value
MyMap::get(key);
// 删除某个key对应的元素
MyMap::remove(key);
// 覆盖或者修改某个key对应的元素
MyMap::insert(key, new_value);
MyMap::mutate(key, |old_value| old_value+1);
更多可用的API,请参考文档:frame_support::storage::StorageMap。
需要注意的是,当使用map来模拟list,也就是将key设置为自增整数时候,直接删除元素,会造成索引的空隙,list长度无限增长,这时可以通过swap and pop的方式来消除这种影响。
即linked_map,也用于存储键值对,拥有map的所有操作,并且可以对所有的键值对进行遍历。
定义linked_map:
MyLinkedMap get(my_linked_map): linked_map u8 => Vec<u8>;
使用基本方法,增删改查和map相同:
// 遍历键值对
let result: Vec<(u8, Vec<u8>)> = MyLinkedMap::enumerate()
.filter(|(k, _)| k > &10)
.collect();
通过调用enumerate返回了一个Iterator,然后就可以使用各种帮助方法比如map,filter,collect,fold等等。更多内容请参考 Rust Iterator 模式。
即double_map,和map,linked_map不同的是,它使用两个key来索引value,用于快速删除key 1对应的任意记录,也可以遍历key 1对应的所有记录。
定义double_map:
MyDoubleMap get(my_double_map): double_map T::AccountId, blake2_256(u32) => Vec<u8>; // syntax changed for master
使用基本方法:
let sender = ensure_signed(origin)?; // 获取key1
// 插入一个元素
MyDoubleMap::<T>::insert(sender, key2, value); // key2为参数传入
// 获取某一元素
MyDoubleMap::<T>::get(sender, key2);
// 删除某一元素
MyDoubleMap::<T>::remove(sender, key2);
// 删除key对应的所有元素
MyDoubleMap::<T>::remove_prefix(sender);
Substrate Frame内置的一些模块也使用到了double_map,如:
遍历key 1对应的记录请参考StorageDoubleMap API文档。
通过本文,你已经基本了解了:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!