智能合约的数据存储方案

admin
admin 2021年03月31日
  • 在其它设备中阅读本文章

一、简介

使用以太坊的智能合约来存储数据,使用数据缓存用以优化查询,同时利用程序自动监听日志并更新缓存,达到缓存和智能合约数据的一致。缓存程序将数据存储到数据库,可以支持使用复杂的 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 语句执行。为了配置和重启缓存更新程序,需要额外创建一个配置表,记录合约监听截止的区块号,重启程序时可以从指定的区块号开始监听合约日志,不必从头开始创建缓存。