跳转至

数据库的操作

链上数据存储和读取是智能合约的一个重要功能。EOS链实现了一个内存数据库,支持以表的方式来存储数据,其中,每一个表的每一项数据都有唯一的主索引,称之为primary key,类型为uint64,表中存储的原始数据为任意长度的二进制数据,在智能合约调用存储数据的功能时,会将类的数据序列化后存进表中,在读取的时候又会通过反序列化的方式将原始数据转成类对象。并且还支持 u64, u128, u256, f64, Float128 类型的二重索引表,可以把二重索引表看作数据长度固定的特殊的表。主索引表和二重索引表可以配合起来使用,以实现多重索引的功能。二重索引表可以有多个。二重索引表的值是可以重复的,但是主索引表的主索引必须是唯一的。

下面结合示例来讲解下EOS的链上的内存数据库的使用。

store/find/update

存储,查找,更新三个功能是数据库最基本的功能了,下面的代码演示了如何通过这三个功能进行链上的计数。

完整代码

import {
    Name,
    Contract,
    print,
} from "asm-chain";

@table("counter")
class Counter {
    public key: u64;
    public count: u64;
    constructor(count: u64=0) {
        this.count = count;
        this.key = Name.fromString("counter").N;
    }

    @primary
    get primary(): u64 {
        return this.key;
    }
}

@contract
class MyContract extends Contract {
    constructor(receiver: Name, firstReceiver: Name, action: Name) {
        super(receiver, firstReceiver, action);
    }

    @action("inc")
    inc(): void {
        let mi = Counter.new(this.receiver);
        let it = mi.find(Name.fromString("counter").N);
        let count: u64 = 0;
        let payer: Name = this.receiver;

        if (it.isOk()) {
            let counter = mi.get(it)
            counter.count += 1;
            mi.update(it, counter, payer);
            count = counter.count;
        } else {
            let counter = new Counter(1);
            mi.store(counter, payer);
            count = 1;
        }
        print(`++++++++count:${count}`);
    }
}

解释一下上面的代码:

  • @primary指定了一个主索引成员变量为key, 类型为u64
  • @table("counter")这行代码定义了一个表,表的名称是counter,是一个name结构,table这个decorator指引编译器生成表相关的代码,生成的代码会对asm-chain代码中的MultiIndex结构相关的代码进行封装,以方便开发者进行调用
  • @action("inc")表示inc方法是一个action,会通过包含在Transaction中的Action结构来触发
  • let mi = Counter.new(this.receiver);指定创建一个表,self.receiver指定的是当前合约的账号名称,表示表是存储在当前合约账号。
  • let it = mi.find(Name.fromString("counter").N);用于查找主索引所在的值,返回的值是PrimaryIterator类型
  • let counter = mi.get(it)用于获取PrimaryIterator中的值,如果值不存在,则调用mi.store(counter, payer);来保存一个新值到数据库中,否则将count加1后调用mi.update(it, counter, payer);来更新数据库中的数据。其中的payer用于指定哪个账号支付RAM资源,并且需要在Transaction中已经用该账号的active权限签名。

编译:

cd examples/counter
yarn
yarn build

测试:

ipyeos -m pytest -s -x test.py -k test_inc

运行的测试代码如下:

@chain_test
def test_inc(tester):
    deploy_contract(tester, 'counter')
    args = {'account': 'hello'}
    r = tester.push_action('hello', 'inc', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", ret)

    r = tester.push_action('hello', 'inc', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", ret)

Remove

下面的代码演示了如何去删除数据库中的一项数据。

@action("testremove")
testRemove(account: Name): void {
    requireAuth(account);
    let mi = Counter.new(account);
    let it = mi.find(account.N);
    check(it.isOk(), "account not found");
    mi.remove(it);
}

上面的代码先调用let it = mi.find(account.N);方法来查找指定的数据,然后再调用remove删除,调用it.isOk()以检查指定的索引所在的数据存不存在。

注意:

这里的remove并不需要调用store或者update所指定的payer账号的权限即可删除数据,所以,在实际的应用中,需要通过调用asm_chain.requireAuth来确保指定账号的权限才可以删除数据,例如:

requireAuth(account);

测试代码:

@chain_test
def test_remove(tester):
    deploy_contract(tester, 'counter')
    args = {'account': 'hello'}
    r = tester.push_action('hello', 'inc', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", ret)

    r = tester.push_action('hello', 'inc', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()

    ret = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", ret)

    args = {'account': 'hello'}
    r = tester.push_action('hello', 'testremove', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", ret)

这里,先调用inc这个action来保证数据库中有存储数据,然后调用testremove来删除指定的数据,并且通过get_table_rows来确定数据是否已经添加或者被修改或者被删除,相关的get_table_rows的用法将在下面介绍。

编译:

cd examples/counter
yarn
yarn build

测试:

ipyeos -m pytest -s -x test.py -k test_remove
输出:

INFO     test:test.py:93 +++++++rows: {'rows': [{'account': 'hello', 'count': 1}], 'more': False, 'next_key': ''}
INFO     test:test.py:100 +++++++rows: {'rows': [{'account': 'hello', 'count': 2}], 'more': False, 'next_key': ''}
INFO     test:test.py:107 +++++++rows: {'rows': [], 'more': False, 'next_key': ''}

lowerBound/upperBound

这两个方法也是用来查找表中的元素的,不同于find方法,这两个函数用于模糊查找。其中,lowerBound方法返回>=指定id第一个元素的PrimaryIteratorupperBound方法返回>指定id的第一个元素的PrimaryIterator,下面来看下用法:

@action("testbound")
testBound(): void {
    let table = Counter.new(this.receiver);
    let payer = this.receiver;

    let value = new Counter(new Name(1), 1);
    table.store(value, payer);

    value = new Counter(new Name(3), 1);
    table.store(value, payer);

    value = new Counter(new Name(5), 1);
    table.store(value, payer);

    let it = table.lowerBound(1);
    check(it.isOk() && it.primary == 1, "bad value");
    print(`+++++db.lower_bound(1) return primary key: ${it.primary}\n`);

    it = table.upperBound(3);
    check(it.isOk() && it.primary == 5, "bad value");
    print(`+++++db.lower_bound(5) return primary key: ${it.primary}\n`);
}

测试代码:

@chain_test
def test_bound(tester: ChainTester):
    deploy_contract(tester, 'counter')
    args = {}
    r = tester.push_action('hello', 'testbound', args, {'hello': 'active'})

编译:

cd examples/counter
yarn
yarn build

运行测试:

ipyeos -m pytest -s -x test.py -k test_bound

输出:

+++++db.lower_bound(1) return primary key: 1
+++++db.upper_bound(3) return primary key: 5

利用API来对表进行查询

上面的例子都是讲的如何通过智能合约来操作链上的数据库的表,实际上,通过EOS提供的链下的get_table_rows的API的接口,也同样可以对链上的表进行查询工作。在ipyeosChainTester这个类中和pyeoskitChainApiAsyncChainApi这两个类,都提供了get_table_rows接口,以方便对表进行查询操作

在Python代码中,get_table_rows的定义如下

def get_table_rows(self, _json, code, scope, table,
                                lower_bound, upper_bound,
                                limit,
                                key_type='',
                                index_position='', 
                                encode_type='',
                                reverse = False,
                                show_payer = False):
    """ Fetch smart contract data from an account. 
    key_type: "i64"|"i128"|"i256"|"float64"|"float128"|"sha256"|"ripemd160"
    index_position: "2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"
    encode_type: "dec" or "hex", default to "dec"
    """

解释下这个接口的参数:

  • _json: True 返回json格式的数据库的记录,False返回16进制表示的原始数据
  • code: 表所在的账号,
  • scope: 一般设置为空字符串,当有相同的codetable时,不同的scope可以用来区别不同的表
  • table: 要查询的数据表名
  • lower_bound:起始主索引值或者二重索引值,类型由key_type指定,可以是数值类型,也可以是数值字符串,也可以是十六进制字符串。
  • upper_bound:结束主索引值或者二重索引值,类型由key_type指定,可以是数值类型,也可以是数值字符串,也可以是十六进制字符串。为空时表示没有设置上限,如果设置了一个非空的值,结果将返回>=lower_bound并且<=upper_bound的所有值
  • limit:用于限制返回的值的个数,如果查询的记录多于limit,返回的值中more将被设置成true, next_key: 指向下一个有效的索引。
  • key_type:值可以为:"name""i64""i128""i256""float64""float128""sha256""ripemd160"。对于主索引,也就是index_position1时,值只能是"name"或者"i64",对于index_position >= 2的二重索引,值可能是其中的任意一个。会在下面单独讲解各个取值时,lower_boundupper_bound的编码方式。
  • index_position:用于指定索引的相对位置,为空或者为1表示主索引,从2以上表示二重索引的位置
  • encode_type:值为"dec"或者"hex",默认为dec。指定lower_boundupper_bound的编码格式,以及返回值next_key的格式
  • reverse:指定是否要返回倒序表示的数据
  • show_payer:指定是否显示RAM资源的付费账号

key_type详解:

  • "name" 是一个name类型的字符串
  • "i64" 可以是数值类型,也可以是数值字符串,例如123, "123"
  • "i128" 可以是数值类型,或者数值字符串,或者十六进制字符串,例如:123, "123", "0xaabb", "aabb"
  • "i256" 当encode_type值为"dec"或者空字符串""时,编码格式为:小端模式表示的十六进制的字符串,长度为64个字符。例如: fb54b91bfed2fe7fe39a92d999d002c550f0fa8360ec998f4bb65b00c86282f5将被转换成二个小端模式的uint128_t类型的值:50f0fa8360ec998f4bb65b00c86282f5, fb54b91bfed2fe7fe39a92d999d002c5。当encode_type的值为"hex"时,采用和"sha256"类型一样的大端模式的编码方式。
  • "float64" 值为浮点字符串,如"123.456"
  • "float128" 当encode_type值为"dec"或者空字符串""时,值为浮点字符串,如"123.456",表示的范围只能是float64所允许的范围。当encode_type的值为"hex"时,encode_type值为小端模式表示的十六进制的数据。
  • "sha256" 大端模式表示的十六进制的字符串,长度为64个字符,会被转换成两个小端模式的uint128_t类型的值:如f58262c8005bb64b8f99ec6083faf050c502d099d9929ae37ffed2fe1bb954fb会被转换成50f0fa8360ec998f4bb65b00c86282f5fb54b91bfed2fe7fe39a92d999d002c5,可参考keytype_converter结构的代码。
  • "ripemd160" 十六进制的字符串,长度为64个字符,大端模式,会被转换成两个小端模式的uint128_t类型的值:如83a83a3876c64c33f66f33c54f1869edef5b5d4a000000000000000000000000会被转换成ed69184fc5336ff6334cc676383aa8830000000000000000000000004a5d5bef,可参考keytype_converter结构的代码。

get_table_rows接口的参数还是非常复杂的,作下总结:

  • 如果lower_boundupper_bound为空,表示查询不限范围
  • key_type的值为"i256""float128"时,lower_boundupper_bound的编码方式还受encode_type的影响

要通过get_table_rows来查询表,表的结构必须在ABI的描述中可见,在db_example1这个例子中,生成的test.abi中,包含如下信息即是对表的描述:

"tables": [
    {
        "name": "counter",
        "type": "Counter",
        "index_type": "i64",
        "key_names": [],
        "key_types": []
    }
]

测试代码:

@chain_test
def test_offchain_find(tester):
    deploy_contract(tester, 'counter')

    r = tester.push_action('hello', 'testbound', b'', {'hello': 'active'})
    tester.produce_block()

    r = tester.get_table_rows(False, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", r)

    r = tester.get_table_rows(True, 'hello', '', 'counter', '', '', 10)
    logger.info("+++++++rows: %s", r)

    r = tester.get_table_rows(True, 'hello', '', 'counter', '1', '3', 10)
    logger.info("+++++++rows: %s", r)

运行测试代码:

ipyeos -m pytest -s -x test.py -k test_offchain_find

输出:

INFO     test:test.py:125 +++++++rows: {'rows': ['01000000000000000100000000000000', '03000000000000000100000000000000', '05000000000000000100000000000000'], 'more': False, 'next_key': ''}
INFO     test:test.py:128 +++++++rows: {'rows': [{'account': '............1', 'count': 1}, {'account': '............3', 'count': 1}, {'account': '............5', 'count': 1}], 'more': False, 'next_key': ''}
INFO     test:test.py:131 +++++++rows: {'rows': [{'account': '............1', 'count': 1}, {'account': '............3', 'count': 1}], 'more': False, 'next_key': ''}

注意,这里的account由于是name结构,会将数值转换成字符串,所以输出看起来比较奇怪。

二重索引的存储,查询和更新

请先看下面的例子:

示例代码

import {
    Name,
    Table,
    U128,
    U256,
    printString,
    printHex,
    check,
    Contract,
    print,
} from "asm-chain";

@table("mydata")
class MyData extends Table {
    constructor(
        public a: u64=0,
        public b: u64=0,
        public c: U128=new U128()
    ) {
        super();
    }

    @primary
    get getPrimary(): u64 {
        return this.a;
    }

    @secondary
    get bvalue(): u64 {
        return this.b;
    }

    @secondary
    set bvalue(value: u64) {
        this.b = value;
    }

    @secondary
    get cvalue(): U128 {
        return this.c;
    }

    @secondary
    set cvalue(value: U128) {
        this.c = value;
    }
}

@contract
class MyContract extends Contract{
    @action("test")
    testSecondary(): void {
        let mi = MyData.new(this.receiver);

        let value = new MyData(1, 2, new U128(3));
        mi.store(value, this.receiver);

        value = new MyData(11, 22, new U128(33));
        mi.store(value, this.receiver);

        value = new MyData(111, 222, new U128(333));
        mi.store(value, this.receiver);


        let idx = mi.bvalueDB;    
        let idxIt = idx.find(2);
        printString(`+++++++++idx64.find: ${idxIt.i}, ${idxIt.primary}\n`);
        check(idxIt.primary == 1, "bad value");

        let ret = idx.lowerBound(2);
        check(ret.primary == 1, "bad value");

        ret = idx.upperBound(22);
        check(ret.primary == 111, "bad value");
    }

    @action("testupdate")
    testSecondaryUpdate(): void {
        let mi = MyData.new(this.receiver);
        let idx = mi.bvalueDB;
        let idxIt = idx.find(222);
        check(idxIt.isOk(), "value 222 not found");
        check(idxIt.primary == 111, "bad primary value");
        mi.updateBvalue(idxIt, 223, this.receiver);
        let ret = idx.find(22);
        check(ret.isOk(), "bad scondary value");
    }

    @action("testremove")
    testSecondaryRemove(): void {
        let table = MyData.new(this.receiver);
        let idx = table.bvalueDB;
        let idxIt = idx.find(222);
        check(idxIt.isOk(), "value 222 not found");
        check(idxIt.primary == 111, "bad primary value");
        let primaryIt = table.find(idxIt.primary);
        check(primaryIt.isOk(), "bad primary value");
        table.remove(primaryIt);
    }
}

在这个例子中,定义了两个二重索引:

@secondary
get bvalue(): u64 {
    return this.b;
}

@secondary
set bvalue(value: u64) {
    this.b = value;
}

@secondary
get cvalue(): U128 {
    return this.c;
}

@secondary
set cvalue(value: U128) {
    this.c = value;
}
  • test action 调用store方法存储了3组数据, 并演示了调用二重索引的lowerBound来查找二重索引,
  • testupdate action 演示了调用updateBvalue这个生成的方法来更新二重索引的数据。updateBvalue是一个生成的方法,规律是update + 二重索引的方法名。

测试代码:

@chain_test
def test_secondary_update(tester: ChainTester):
    deploy_contract(tester, 'secondaryindex')
    args = {}
    r = tester.push_action('hello', 'test', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()

    r = tester.push_action('hello', 'testupdate', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()

编译:

cd examples/secondaryindex
yarn
yarn build

运行测试:

ipyeos -m pytest -s -x test.py -k test_secondary_update

输出:

INFO     test:test.py:85 {'rows': [{'a': 1, 'b': 2, 'c': '3'}, {'a': 11, 'b': 22, 'c': '33'}, {'a': 111, 'b': 222, 'c': '333'}], 'more': False, 'next_key': ''}
INFO     test:test.py:92 {'rows': [{'a': 1, 'b': 2, 'c': '3'}, {'a': 11, 'b': 22, 'c': '33'}, {'a': 111, 'b': 223, 'c': '333'}], 'more': False, 'next_key': ''}

从输出中的:

{'a': 111, 'b': 223, 'c': '333'}

可以知道222已经被改成223了,其它的值保持不变

二重索引的删除

@action("testremove")
testSecondaryRemove(): void {
    let table = MyData.new(this.receiver);
    let idx = table.bvalueDB;
    let idxIt = idx.find(222);
    check(idxIt.isOk(), "value 222 not found");
    check(idxIt.primary == 111, "bad primary value");
    let primaryIt = table.find(idxIt.primary);
    check(primaryIt.isOk(), "bad primary value");
    table.remove(primaryIt);
}

解释一下上面的代码:

  • let idxIt = idx.find(222); 查找二重索引
  • let primaryIt = table.find(idxIt.primary); 通过idxIt.primary获取主索引,再通过主索引返回主索引的PrimaryIterator
  • table.remove(primaryIt) 删除表中的元素,包含主索引和所有二重索引

从上面的例子中可以看出,二重索引的删除是先通过二重索引找到主索引:,再通过主索引来删除所有包括二重索引的数据的。

测试代码:

@chain_test
def test_secondary_remove(tester: ChainTester):
    deploy_contract(tester, 'secondaryindex')
    args = {}
    r = tester.push_action('hello', 'test', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'mydata', '', '', 10)
    logger.info(ret)

    r = tester.push_action('hello', 'testremove', args, {'hello': 'active'})
    logger.info('++++++elapsed: %s', r['elapsed'])
    tester.produce_block()
    ret = tester.get_table_rows(True, 'hello', '', 'mydata', '', '', 10)
    logger.info(ret)

编译:

cd examples/secondaryindex
yarn
yarn build

运行测试:

ipyeos -m pytest -s -x test.py -k test_secondary_remove

输出:

INFO     test:test.py:102 {'rows': [{'a': 1, 'b': 2, 'c': '3'}, {'a': 11, 'b': 22, 'c': '33'}, {'a': 111, 'b': 222, 'c': '333'}], 'more': False, 'next_key': ''}
INFO     test:test.py:108 {'rows': [{'a': 1, 'b': 2, 'c': '3'}, {'a': 11, 'b': 22, 'c': '33'}], 'more': False, 'next_key': ''}

对比两次get_table_rows的返回值,会发现{'a': 111, 'b': 222, 'c': '333'}这组数据被删除了

利用API来对表进行二重索引查询

在上面的例子中定义了两个二重索引,类型分别为u64,u128get_table_rowsAPI还支持通过二重索引来查找对应的值

@chain_test
def test_offchain_find(tester: ChainTester):
    deploy_contract(tester, 'secondaryindex')

    args = {}
    r = tester.push_action('hello', 'test1', args, {'hello': 'active'})
    r = tester.get_table_rows(True, 'hello', '', 'mydata', '1', '', 10, key_type="i64", index_position="1")
    logger.info("+++++++rows: %s", r)

    r = tester.get_table_rows(True, 'hello', '', 'mydata', '11', '', 10, key_type="i64", index_position="2")
    logger.info("+++++++rows: %s", r)
    # 0x14d == 333
    r = tester.get_table_rows(True, 'hello', '', 'mydata', '0x14d', '', 10, key_type="i128", index_position="3")
    logger.info("+++++++rows: %s", r)

注意, 在查询c的时候,由于类型是u128,对于超出u64类型的范围时,可以用十六进制来表示数据,例如上面的0x14d的十进制数据为333

运行测试用例:

ipyeos -m pytest -s -x test.py -k test_offchain_find

上面的测试代码的运行结果如下:

INFO     test:test.py:117 +++++++rows: {'rows': [{'a': 1, 'b': 2, 'c': '3'}, {'a': 11, 'b': 22, 'c': '33'}, {'a': 111, 'b': 222, 'c': '333'}], 'more': False, 'next_key': ''}
INFO     test:test.py:120 +++++++rows: {'rows': [{'a': 11, 'b': 22, 'c': '33'}, {'a': 111, 'b': 222, 'c': '333'}], 'more': False, 'next_key': ''}
INFO     test:test.py:123 +++++++rows: {'rows': [{'a': 111, 'b': 222, 'c': '333'}], 'more': False, 'next_key': ''}

总结

EOS中的数据存储功能是比较完善的,并且有二重索引表的功能,使数据的查找变得非常的灵活。本章详细讲解了数据库表的增,删,改,查的代码。本章的内容较多,需要花点时间好好消化。可以在示例的基础上作些改动,并且尝试运行以增加对这章知识点的理解。

示例代码1 示例代码2

评论