环境 : fisco bcos 3.11.0
webase-front : 3.1.1
console 3.8.0
前言
最近在做毕设,数据的存储方式考虑使用fisco-bcos的table表存储,经过这几天的研究,发现对于fisco2
和 fisco3
版本的table表合约功能差异还是比较大的,比较起来V3的table合约功能性更丰富,更加的方便开发。
读者们要是没用过v3的链子,可以在fisco3 这里简单启动一条链子,Air版本的搭建和fisco2搭建的链子命令无多大差别【文章后面也有对应的控制台搭建命令,注意控制台2和3版本的链子不互通 】
然后就是webase-front要使用3.0以上的版本,链接在这里https://webasedoc.readthedocs.io/zh-cn/lab/docs/WeBASE-Install/developer.html
关于fisco3 的Table合约
不知道是不是webase-front 版本的问题,我并未在其代码仓库里找到Table.sol
合约的文件,只有KVTable.sol
合约。然后我去github的fisco仓库找到了fisco3的版本合约文件,还附带两个合约,都需要import进去才行
[更新一下,这些合约也可以在控制台3.8.0上的contracts/solidity
文件夹上找到,注意:v3版本有两个Table合约,一个是3.2.0版本以上,一个是3.2.0以前的版本,我所有介绍的是3.2.0以上
的,官方文档给的是3.2.0以前
的例子]
Table.sol
js
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.6.10 <0.8.20;
pragma experimental ABIEncoderV2;
import "./EntryWrapper.sol";
// KeyOrder指定Key的排序规则,字典序和数字序,如果指定为数字序,key只能为数字
enum KeyOrder {Lexicographic, Numerical}
struct TableInfo {
KeyOrder keyOrder;
string keyColumn;
string[] valueColumns;
}
// 更新字段,用于update
struct UpdateField {
string columnName;
// 考虑工具类
string value;
}
// 筛选条件,大于、大于等于、小于、小于等于
enum ConditionOP {GT, GE, LT, LE, EQ, NE, STARTS_WITH, ENDS_WITH, CONTAINS}
struct Condition {
ConditionOP op;
string field;
string value;
}
// 数量限制
struct Limit {
uint32 offset;
// count limit max is 500
uint32 count;
}
// 表管理合约,是静态Precompiled,有固定的合约地址
abstract contract TableManager {
// 创建表,传入TableInfo
function createTable(string memory path, TableInfo memory tableInfo) public virtual returns (int32);
// 创建KV表,传入key和value字段名
function createKVTable(string memory tableName, string memory keyField, string memory valueField) public virtual returns (int32);
// 只提供给Solidity合约调用时使用
function openTable(string memory path) public view virtual returns (address);
// 变更表字段
// 只能新增字段,不能删除字段,新增的字段默认值为空,不能与原有字段重复
function appendColumns(string memory path, string[] memory newColumns) public virtual returns (int32);
// 获取表信息
function descWithKeyOrder(string memory tableName) public view virtual returns (TableInfo memory);
}
// 表合约,是动态Precompiled,TableManager创建时指定地址
abstract contract Table {
// 按key查询entry
function select(string memory key) public virtual view returns (Entry memory);
// 按条件批量查询entry,condition为空则查询所有记录
function select(Condition[] memory conditions, Limit memory limit) public virtual view returns (Entry[] memory);
// 按照条件查询count数据
function count(Condition[] memory conditions) public virtual view returns (uint32);
// 插入数据
function insert(Entry memory entry) public virtual returns (int32);
// 按key更新entry
function update(string memory key, UpdateField[] memory updateFields) public virtual returns (int32);
// 按条件批量更新entry,condition为空则更新所有记录
function update(Condition[] memory conditions, Limit memory limit, UpdateField[] memory updateFields) public virtual returns (int32);
// 按key删除entry
function remove(string memory key) public virtual returns (int32);
// 按条件批量删除entry,condition为空则删除所有记录
function remove(Condition[] memory conditions, Limit memory limit) public virtual returns (int32);
}
abstract contract KVTable {
function get(string memory key) public view virtual returns (bool, string memory);
function set(string memory key, string memory value) public virtual returns (int32);
}
EntryWrapper.sol
这个类似于mybatisplus里面的wrapper条件构造器,用来进行附加查询条件使用,还有Entry用来做返回数据的数据结构
js
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.6.10 <0.8.20;
pragma experimental ABIEncoderV2;
import "./Cast.sol";
// 记录,用于select和insert
struct Entry {
string key;
string[] fields; // 考虑2.0的Entry接口,临时Precompiled的问题,考虑加工具类接口
}
contract EntryWrapper {
Cast constant cast = Cast(address(0x100f));
Entry entry;
constructor(Entry memory _entry) public {
entry = _entry;
}
function setEntry(Entry memory _entry) public {
entry = _entry;
}
function getEntry() public view returns(Entry memory) {
return entry;
}
function fieldSize() public view returns (uint256) {
return entry.fields.length;
}
function getInt(uint256 idx) public view returns (int256) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return cast.stringToS256(entry.fields[idx]);
}
function getUInt(uint256 idx) public view returns (uint256) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return cast.stringToU256(entry.fields[idx]);
}
function getAddress(uint256 idx) public view returns (address) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return cast.stringToAddr(entry.fields[idx]);
}
function getBytes64(uint256 idx) public view returns (bytes1[64] memory) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return bytesToBytes64(bytes(entry.fields[idx]));
}
function getBytes32(uint256 idx) public view returns (bytes32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return cast.stringToBytes32(entry.fields[idx]);
}
function getString(uint256 idx) public view returns (string memory) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
return entry.fields[idx];
}
function set(uint256 idx, int256 value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = cast.s256ToString(value);
return 0;
}
function set(uint256 idx, uint256 value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = cast.u256ToString(value);
return 0;
}
function set(uint256 idx, string memory value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = value;
return 0;
}
function set(uint256 idx, address value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = cast.addrToString(value);
return 0;
}
function set(uint256 idx, bytes32 value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = cast.bytes32ToString(value);
return 0;
}
function set(uint256 idx, bytes1[64] memory value) public returns(int32) {
require(idx >= 0 && idx < fieldSize(), "Index out of range!");
entry.fields[idx] = string(bytes64ToBytes(value));
return 0;
}
function setKey(string memory value) public {
entry.key = value;
}
function getKey() public view returns (string memory) {
return entry.key;
}
function bytes64ToBytes(bytes1[64] memory src) private pure returns(bytes memory) {
bytes memory dst = new bytes(64);
for(uint32 i = 0; i < 64; i++) {
dst[i] = src[i][0];
}
return dst;
}
function bytesToBytes64(bytes memory src) private pure returns(bytes1[64] memory) {
bytes1[64] memory dst;
for(uint32 i = 0; i < 64; i++) {
dst[i] = src[i];
}
return dst;
}
}
Cast.sol
js
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.6.10 <0.8.20;
pragma experimental ABIEncoderV2;
abstract contract Cast {
function stringToS256(string memory) public virtual view returns (int256);
function stringToS64(string memory) public virtual view returns (int64);
function stringToU256(string memory) public virtual view returns (uint256);
function stringToAddr(string memory) public virtual view returns (address);
function stringToBytes32(string memory) public virtual view returns (bytes32);
function s256ToString(int256) public virtual view returns (string memory);
function s64ToString(int64) public virtual view returns (string memory);
function u256ToString(uint256) public virtual view returns (string memory);
function addrToString(address) public virtual view returns (string memory);
function bytes32ToString(bytes32) public virtual view returns (string memory);
}
编写TableTest
这里我直接用官网的例子代码进行测试,不过官网例子与实际的table合约有出入,需要进行修改
js
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.6.10 <0.8.20;
pragma experimental ABIEncoderV2;
import "./Table.sol";
contract TestTable{
// 创建TableManager对象,其在区块链上的固定地址是0x1002
TableManager constant tm = TableManager(address(0x1002));
Table table;
string constant TABLE_NAME = "t_test";
constructor () public{
// 创建t_test表,表的主键名为id,其他字段名为name和age
string[] memory columnNames = new string[](2);
columnNames[0] = "name";
columnNames[1] = "age";
KeyOrder keyOrder;
TableInfo memory tf = TableInfo(KeyOrder.Numerical,"id", columnNames);
tm.createTable(TABLE_NAME, tf);
// 获取真实的地址,存在合约中
address t_address = tm.openTable(TABLE_NAME);
require(t_address!=address(0x0),"");
table = Table(t_address);
}
function insert(string memory id,string memory name,string memory age) public returns (int32){
string[] memory columns = new string[](2);
columns[0] = name;
columns[1] = age;
Entry memory entry = Entry(id, columns);
int32 result = table.insert(entry);
// emit InsertResult(result);
return result;
}
function update(string memory id, string memory name, string memory age) public returns (int32){
UpdateField[] memory updateFields = new UpdateField[](2);
updateFields[0] = UpdateField("name", name);
updateFields[1] = UpdateField("age", age);
int32 result = table.update(id, updateFields);
return result;
}
function remove(string memory id) public returns(int32){
int32 result = table.remove(id);
return result;
}
function select(string memory id) public view returns (string memory,string memory)
{
Entry memory entry = table.select(id);
string memory name;
string memory age;
if(entry.fields.length==2){
name = entry.fields[0];
age = entry.fields[1];
}
return (name,age);
}
// enum ConditionOP {GT, GE, LT, LE, EQ, NE, STARTS_WITH, ENDS_WITH, CONTAINS}
// struct Condition {
// ConditionOP op;
// string field;
// string value;
// }
function selectMore(string memory age)
public
view
returns (Entry[] memory entries)
{
Condition[] memory conds = new Condition[](1);
Condition memory eq= Condition({op: ConditionOP.EQ, field: "age",value: age});
conds[0] = eq;
Limit memory limit = Limit({offset: 0, count: 100});
entries = table.select(conds, limit);
return entries;
}
}
TableInfo的问题
实际的TableInfo 结构如下
js
// KeyOrder指定Key的排序规则,字典序和数字序,如果指定为数字序,key只能为数字
enum KeyOrder {Lexicographic, Numerical}
struct TableInfo {
KeyOrder keyOrder;
string keyColumn;
string[] valueColumns;
}
是需要三个参数的,而官网的例子只用两个参数,缺少的第一个参数是枚举类,意思是你的主键是string类型还是数字类型,需要标明。
Condition的问题
实际的Condition结构如下
js
// 筛选条件,大于、大于等于、小于、小于等于
enum ConditionOP {GT, GE, LT, LE, EQ, NE, STARTS_WITH, ENDS_WITH, CONTAINS}
struct Condition {
ConditionOP op;
string field;
string value;
}
官网的只有两个参数,缺少的那个参数是field,也就是此条件是要用在表的哪个字段时安个。
关于主键的问题
在fisco2的table表合约开发时候,因其的设计,导致主键是可以重复的。
但在测试fisco3的table表合约开发的时候,我用重复的主键添加多个数据,发现它遵守了主键唯一
的规则,有兴趣的读者可以去测试一下,特别是用TestTable的selectMore
,把field改成主键字段,可以测试到返回不了多个数据
结语
这段时间会继续开发v3的table合约,在开发的时候遇到的坑和新发现会持续更新到博客上