数据库的操作
链上数据存储和读取是智能合约的一个重要功能。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
第一个元素的PrimaryIterator
,upperBound
方法返回>
指定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的接口,也同样可以对链上的表进行查询工作。在ipyeos
的ChainTester
这个类中和pyeoskit
的ChainApiAsync
和ChainApi
这两个类,都提供了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
: 一般设置为空字符串,当有相同的code
,table
时,不同的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_position
为1
时,值只能是"name"
或者"i64"
,对于index_position >= 2
的二重索引,值可能是其中的任意一个。会在下面单独讲解各个取值时,lower_bound
和upper_bound
的编码方式。index_position
:用于指定索引的相对位置,为空或者为1
表示主索引,从2
以上表示二重索引的位置encode_type
:值为"dec"
或者"hex"
,默认为dec
。指定lower_bound
,upper_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
会被转换成50f0fa8360ec998f4bb65b00c86282f5
和fb54b91bfed2fe7fe39a92d999d002c5
,可参考keytype_converter结构的代码。 - "ripemd160" 十六进制的字符串,长度为64个字符,大端模式,会被转换成两个小端模式的uint128_t类型的值:如
83a83a3876c64c33f66f33c54f1869edef5b5d4a000000000000000000000000
会被转换成ed69184fc5336ff6334cc676383aa883
和0000000000000000000000004a5d5bef
,可参考keytype_converter结构的代码。
get_table_rows
接口的参数还是非常复杂的,作下总结:
- 如果
lower_bound
和upper_bound
为空,表示查询不限范围 - 当
key_type
的值为"i256"
和"float128"
时,lower_bound
和upper_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
,u128
,get_table_rows
API还支持通过二重索引来查找对应的值
@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中的数据存储功能是比较完善的,并且有二重索引表的功能,使数据的查找变得非常的灵活。本章详细讲解了数据库表的增,删,改,查的代码。本章的内容较多,需要花点时间好好消化。可以在示例的基础上作些改动,并且尝试运行以增加对这章知识点的理解。