智能合约的数据存储方案
一、简介
使用以太坊的智能合约来存储数据,使用数据缓存用以优化查询,同时利用程序自动监听日志并更新缓存,达到缓存和智能合约数据的一致。缓存程序将数据存储到数据库,可以支持使用复杂的 SQL 语句来做查询,达到智能合约难以实现的复杂查询和加速查询的效果。
二、合约设定
为了配合监听程序达到自动监听的效果,合约的实现需要规范化,以结构体定义数据库的表格结构,包括 INSERT、UPDATE、DETETE 事件。一个标准的实现如下所示:
pragma solidity ^0.6.6;
pragma experimental ABIEncoderV2;
contract Admin {
struct Info {
string id;
string name;
string extra;
}
Info[] infos;
mapping(string => uint256) idToIndex;
event INSERT(Info info);
event UPDATE(Info info);
event DELETE(string id);
modifier onlyExist(string memory _id) {
require(idToIndex[_id] > 0, "no exist");
_;
}
modifier onlyNoExist(string memory _id) {
require(idToIndex[_id] == 0, "exist");
_;
}
function exist(string calldata _id) external view returns (bool) {
return idToIndex[_id] > 0;
}
function name(string calldata _id) external view returns (string memory) {
if (idToIndex[_id] == 0) return "";
else return infos[idToIndex[_id] - 1].name;
}
function add(Info memory _info) public {
_add(_info);
}
function adds(Info[] memory _infos) public {
for (uint256 i = 0; i < _infos.length; i++) {
_add(_infos[i]);
}
}
function del(string calldata _id) external {
_del(_id);
}
function dels(string[] calldata _ids) external {
for (uint256 i = 0; i < _ids.length; i++) {
_del(_ids[i]);
}
}
function set(Info memory _info) public {
_set(_info);
}
function sets(Info[] memory _infos) public {
for (uint256 i = 0; i < _infos.length; i++) {
_set(_infos[i]);
}
}
function get(string calldata _id) external view returns (Info memory) {
if (idToIndex[_id] > 0) return infos[idToIndex[_id] - 1];
}
function getAll() external view returns (Info[] memory) {
return infos;
}
function total() external view returns (uint64) {
return uint64(infos.length);
}
function page(uint128 _index, uint128 _size)
external
view
returns (Info[] memory ret)
{
uint256 start = _index * _size;
uint256 end = start + _size;
if (end > infos.length) end = infos.length;
if (end > start) ret = new Info[](end - start);
for (uint256 i = 0; end > start; (i++, start++)) {
ret[i] = infos[start];
}
}
function _add(Info memory _info) internal onlyNoExist(_info.id) {
infos.push(_info);
idToIndex[_info.id] = infos.length;
emit INSERT(_info);
}
function _del(string memory _id) internal onlyExist(_id) {
infos[idToIndex[_id] - 1] = infos[infos.length - 1];
idToIndex[infos[idToIndex[_id] - 1].id] = idToIndex[_id];
infos.pop();
delete idToIndex[_id];
emit DELETE(_id);
}
function _set(Info memory _info) internal onlyExist(_info.id) {
infos[idToIndex[_info.id] - 1] = _info;
emit UPDATE(_info);
}
}
三、数据库表格创建
合约里一般需要使用结构体来作为数据存储的基本单位,利用结构体的字段结合 INSERT 事件(必须有,没有 INSERT 事件数据库表格就不能插入数据)来决定表的结构。如果有 id 字段,则以 id 作为主键,如果没有则使用以下划线开头的字段组合为主键。结构体的字段名称就是表的字段名称,结构体的字段类型就是表的字段数据类型,结构体字段的数据类型自动转换为数据库支持的数据类型。根据 INSERT 事件的参数,来确定表名,如果只有一个 INSERT 事件,则以合约名称作为表名,如果存在多个,可以加上结构体的名称作为表名。
四、事件的监听
主要监听三类事件:插入(INSERT)、更新(UPDATE)和删除(DELETE),插入数据则监听 INSERT 事件,根据 INSERT 事件来将数据插入到数据库中,如果一个智能合约存在多个 INSERT 事件,还需要根据事件参数的类型来选择数据库的表格,同样的 UPDATE 和 DELETE 事件可以是结构体类型的数据,根据结构体定义的主键自动更新删除数据。如果 UPDATE 事件的参数不是结构体,则以 indexed 的参数或者自动根据主键来作为更新或者删除的条件。另外需要记录处理到的区块号,以便监听程序重启时自动从上次停止的区块号开始监听事件。
五、缓存更新程序
启动时根据事件定义自动创建表格,分析合约事件,确定增删改语句,开始监听日志,根据事件匹配合适的 SQL 语句执行。为了配置和重启缓存更新程序,需要额外创建一个配置表,记录合约监听截止的区块号,重启程序时可以从指定的区块号开始监听合约日志,不必从头开始创建缓存。