数据库的操作
链上数据存储和读取是智能合约的一个重要功能。EOS链实现了一个内存数据库,支持以表的方式来存储数据,其中,每一个表的每一项数据都有唯一的主索引,称之为primary key,类型为u64,表中存储的原始数据为任意长度的二进制数据,在智能合约调用存储数据的功能时,会将类的数据序列化后存进表中,在读取的时候又会通过反序列化的方式将原始数据转成类对象。并且还支持u64, u128, u256, double, long double类型的二重索引表,可以把二重索引表看作数据长度固定的特殊的表。主索引表和二重索引表可以配合起来使用,以实现多重索引的功能。二重索引表可以有多个。二重索引表的值是可以重复的,但是主索引表的主索引必须是唯一的。
下面结合示例来讲解下EOS的链上的内存数据库的使用。
store
存储功能是数据库最简单的功能了,下面的代码即演示了该功能。
# db_example1.codon
from chain.database import primary
from chain.contract import Contract
@table("mytable")
class A(object):
a: primary[u64]
b: str
def __init__(self, a: u64, b: str):
self.a = primary[u64](a)
self.b = b
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('teststore')
def test_store(self):
print('db_test')
item = A(123u64, 'hello, world')
table = A.new_table(n'hello', n'')
table.store(item, n'hello')
解释一下上面的代码:
- 其中,@table("mytable")
这行代码表示要创建一个表名称为mytable
的表类型。注意,mytable
是一个name
类型。@table
告诉编译器生成表操作相关的额外代码,像上面的new_table
方法即是通过编译器生成的。
-
A.new_table(n'hello', n'')
创建一个新的表对象,n'hello'
用于指定表存储在那个账号中,第二个参数表示scope,一般可以置空,配合账号名,表名,用于区分不同的表。 -
table.store(item, n'hello')
这行代码将数据存储到链上的数据库中。其中的n'hello'
指定通过hello
这个账号支付RAM资源,并且需要在Transaction中已经用账号hello
的active
权限签名。
编译:
python-contract build db_example/db_example1.codon
ipyeos -m pytest -s -x test.py -k test_store
def test_store():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
注意在这个示例中,如果表中已经存在以123u64
为key的数据,那么该函数会抛出异常。
如将上面的测试用例修改成下面的代码:
def test_example1():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
# will raise exception
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
用同样的命令运行测试,在第二次调用push_action
时,该函数就会抛出像下面的异常:
could not insert object, most likely a uniqueness constraint was violated
为了不抛出异常,在要更新表中的数据时,则要用到update
方法。
在调用store
之前要先对表中是否存在主索引进行判断,如果已经存在,则不能调用store
方法,而必须调用update
方法。
以下的示例展示了用法:
find/update
这一节演示了数据库的查找和更新功能。
# db_example1.codon
...
@contract(main=True)
class MyContract(Contract):
...
@action('testupdate')
def test_update(self, value: str):
print('db_test')
table = A.new_table(n'hello', n'')
key = 123u64
it = table.find(key)
if it.is_ok():
print('+++++update value:', value)
item = A(key, value)
table.update(it, item, n'hello')
else:
print('+++++store value:', value)
item = A(key, value)
table.store(item, n'hello')
以下为测试代码:
def test_update():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'testupdate', {'value': 'hello, bob'}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
ret = t.push_action('hello', 'testupdate', {'value': 'hello, alice'}, {'hello': 'active'})
t.produce_block()
编译:
python-contract build db_example/db_example1.codon
用下面的命令来运行测试代码:
ipyeos -m pytest -s -x test.py -k test_update
在调用
t.push_action('hello', 'testupdate', {'value': 'hello, bob'}, {'hello': 'active'})
会输出:
+++++store value: hello, bob
再次调用 testupdate
:
t.push_action('hello', 'testupdate', {'value': 'hello, alice'}, {'hello': 'active'})
会输出:
+++++update value: hello, alice
可以看出,上面的代码稍微有点复杂,首先要调用find
判断和主索引对应的值存不存在,再决定是调用store
还是update
。需要注意的是,在更新的过程中,主索引的值是不能变的,否则会抛出异常。
可以试着将update的代码修改成:
item = A(key+1u64, value)
table.update(it, item, n'hello')
你将会看到到智能合约里抛出的异常
remove
下面的代码演示了如何去删除数据库中的一项数据。
# db_example/db_example1.codon
@action('testremove')
def test_remove(self):
print('test remove')
item = A(123u64, 'hello, world')
table = A.new_table(n'hello', n'')
table.store(item, n'hello')
it = table.find(123u64)
assert it.is_ok()
table.remove(it)
it = table.find(123u64)
assert not it.is_ok()
测试代码:
def test_remove():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'testremove', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
编译:
python-contract build db_example/db_example1.codon
测试:
ipyeos -m pytest -s -x test.py -k test_remove
上面的代码先调用store
方法来存储索引为123u64
的数据,然后再再调用remove
删除,调用assert
以检查结果。如果一切正常,程序将不会抛出任何的异常。
lowerbound/upperbound
这两个方法也是用来查找给中的元素的,不同于find
方法,这两个函数用于模糊查找。其中,lowerbound
方法返回>=
指定id
的Iterator
,upperbound
方法返回>
指定id
的Iterator
,下面来看下用法:
# db_example/db_example1.codon
...
@contract(main=True)
class MyContract(Contract):
...
@action('testbound')
def test_bound(self):
print('db_test')
table = A.new_table(n'hello', n'')
payer = n'hello'
value = A(1u64, "alice")
table.store(value, payer)
value = A(3u64, "bob")
table.store(value, payer)
value = A(5u64, "john")
table.store(value, payer)
it = table.lowerbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a == 1u64 and value2.b == 'alice'
it = table.upperbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a == 3u64 and value2.b == 'bob'
测试代码:
def test_bound():
t = init_db_test('db_example1)
ret = t.push_action('hello', 'testbound', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
编译:
python-contract build db_example/db_example1.codon
运行测试:
ipyeos -m pytest -s -x test.py -k test_bound
输出:
+++++: 1 alice
+++++: 3 bob
利用API来对表进行主索引查询
上面的例子都是讲的如何通过智能合约来操作链上的数据库的表,实际上,通过EOS提供的链下的get_table_rows
的API的接口,也同样可以对链上的表进行查询工作。
在测试代码中,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的描述中可见,可以通过下面的代码来让表在生成相应的ABI文件中有描述:
# db_example5.codon
from chain.database import primary
from chain.contract import Contract
@table("mytable")
class A(object):
a: primary[u64]
b: str
def __init__(self, a: u64, b: str):
self.a = primary[u64](a)
self.b = b
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
print('db_test')
table = A.new_table(n'hello', n'')
payer = n'hello'
value = A(1u64, "alice")
table.store(value, payer)
value = A(3u64, "bob")
table.store(value, payer)
value = A(5u64, "john")
table.store(value, payer)
it = table.lowerbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a() == 1u64 and value2.b == 'alice'
it = table.upperbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a() == 3u64 and value2.b == 'bob'
这里,通过table
这个内置的decorator
来让编译器在ABI中加入表的结构。
给类加了这个table
,编译器会自动给类添加get_primary
以及new_table
函数:
同时,类的成员变量也要满足相应的要求:
首先,必须声明一个主索引变量,类型必须是database.primary
, primary
类的实现如下:
class primary[T](object):
value: T
def __init__(self, value: T):
self.value = value
def get_primary(self) -> u64:
if isinstance(self.value, u64):
return self.value
return self.value.get_primary()
def __pack__(self, enc: Encoder):
self.value.__pack__(enc)
def __unpack__(dec: Decoder) -> primary[T]:
return primary[T](T.__unpack__(dec))
def __call__(self) -> T:
return self.value
def __size__(self) -> int:
return self.value.__size__()
primary
类是一个模版类,如果primary
的value
的类型不为u64
,则类型必须实现get_primary
方法,primary
类还有一个__call__
方法,以方便获取value
。在后面会讲到的多重索引中,还会用到二重索引,二重索引的类型必须是database.secondary
编译:
python-contract build db_example/db_example5.codon
你将在生成的db_example5.abi
中看到下面的描述:
"tables": [
{
"name": "mytable",
"type": "A",
"index_type": "i64",
"key_names": [],
"key_types": []
}
]
再看下测试代码:
def test_example5():
t = init_db_test('db_example5')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
rows = t.get_table_rows(True, 'hello', '', 'mytable', 1, '', 10)
logger.info('++++++=rows: %s', rows)
运行测试:
ipyeos -m pytest -s -x test.py -k test_example5
输出:
++++++=rows: {'rows': [{'a': 1, 'b': 'alice'}, {'a': 3, 'b': 'bob'}, {'a': 5, 'b': 'john'}], 'more': False, 'next_key': ''}
二重索引的操作
请先看下面的例子:
# db_example7.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.name import Name
@table("mytable")
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it = idx_table_b.find(2u64)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
idx_table_c = table.get_idx_table_by_c()
it = idx_table_c.find(3u128)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
在这个例子中,定义了两个二重索引:
b: secondary[u64]
c: secondary[u128]
在代码里,通过get_idx_table_by_b
和get_idx_table_by_c
来获取二重索引的表,返回的对象类型分别为IdxTable64
和IdxTable128
。
二重索引的表和主索引的表有类似的方法名称,也可以执行二重索引的查找的功能。
测试代码:
# test.py
def test_example7():
t = init_db_test('db_example7')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
编译:
python-contract build db_example/db_example7.codon
运行测试:
ipyeos -m pytest -s -x test.py -k test_example7
输出:
++++++it.primary: 1
++++++it.primary: 1
二重索引的的更新
在实际的应用中,有时候需要更新二重索引。请先看下面的代码
# db_example8.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.name import Name
@table("mytable")
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
item = A(111u64, 222u64, 333u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
table.update_b(it_sec, 22u64, payer)
it_sec = idx_table_b.find(22u64)
assert it_sec.is_ok()
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
注意上面代码中的这段代码:
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
table.update_b(it_sec, 22u64, payer)
it_sec = idx_table_b.find(22u64)
assert it_sec.is_ok()
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
简述下过程:
it_sec = idx_table_b.find(2u64)
查找二重索引的值2u64
,返回的SecondaryIterator
类型的it_sec
。table.update_b(it_sec, 22u64, payer)
这行代码即是实现了更新的功能,更新b
的值为22u64
it_sec = idx_table_b.find(22u64)
查找新的二重索引assert assert it_sec.is_ok()
用于确认二重索引是否更新成功assert it_sec.primary == 1u64
用于确认主索引是否正确
而update_b
是由编译器生成的代码,生成的代码如下:
def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
# 更新`b`的二重索引
self.idx_b.update(it, b, payer)
# 查找主索引
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
# 获取主索引对应的值
value: A = it_primary.get_value()
# 更新主索引对应的值
value.b = secondary[u64](b)
self.table.update(it_primary, value, payer)
从代码看出,更新二重索引的时候,还会更新主索引的对应的值
二重索引的删除
@action('testremove')
def test_remove(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
assert it_sec.primary == 1u64
it = table.find(it_sec.primary)
table.remove(it)
it_sec = idx_table_b.find(2u64)
assert not it_sec.is_ok()
print('done!')
在这个例子中,首先调用store
存储主索引为1u64
,以及第一个二重索引的值为2u64
的对象A,然后查询2u64
,确认it_sec.primary == 1u64
,然后调用remove
删除主索引为1u64
的数据,最后再次查询2u64
,确认元素已经被删除。
# test.py
def test_remove_secondary():
t = init_db_test('db_example8')
ret = t.push_action('hello', 'testremove', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
编译:
python-contract build db_example/db_example8.codon
运行测试:
ipyeos -m pytest -s -x test.py -k test_remove_secondary
利用API来对表进行二重索引查询
在例子db_example8.codon
中,定义了两个二重索引,类型分别为u64
,u128
,get_table_rows
API还支持通过二重索引来查找对应的值
def test_example9():
t = init_db_test('db_example8')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
# find by secondary u64
rows = t.get_table_rows(True, 'hello', '', 'mytable', 22, '', 10, 'i64', '2')
logger.info("++++++++++%s", rows['rows'])
assert rows['rows'][0]['b'] == 22
# find by secondary u128
rows = t.get_table_rows(True, 'hello', '', 'mytable', '3', '', 10, 'i128', '3')
logger.info("++++++++++%s", rows['rows'])
assert rows['rows'][0]['c'] == '3'
下面对代码作下解释
通过二重索引b
来查找表中的值:
rows = t.get_table_rows(True, 'hello', '', 'mytable', 22, '', 10, 'i64', '2')
这里的i64
即是b
的索引类型,2
是索引对应的序号,注意一下这里不是从1
开始算起的。
通过二重索引c
来查找表中的值:
rows = t.get_table_rows(True, 'hello', '', 'mytable', '3', '', 10, 'i128', '3')
这里的i128
即是c
的索引类型,注意这里lowerbound参数的值3
是二重索引的值,由于u128已经超过了64位整数的表示范围,所以用数字字符串表示,最后一个参数3
是索引对应的序号。
上面的测试代码的运行结果如下:
++++++++++[{'a': 1, 'b': 22, 'c': '3'}, {'a': 111, 'b': 222, 'c': '333'}]
++++++++++[{'a': 1, 'b': 22, 'c': '3'}, {'a': 111, 'b': 222, 'c': '333'}]
数据库的实现原理
上面的代码演示了数据库的基本操作,但是实际上在编译的过程中,有些方法和类是由编译器生成的,下面的代码把这些由编译器生成的代码展示了出来。
# db_example6.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.mi import MultiIndexBase
from chain.name import Name
@packer
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
def get_primary(self) -> u64:
return self.a()
class MultiIndexA(MultiIndexBase[A]):
idx_b: IdxTable64
idx_c: IdxTable128
def __init__(self, code: Name, scope: Name, table: Name):
MultiIndexBase[A].__init__(code, scope, table)
idx_table_base = table.value & 0xfffffffffffffff0u64
self.idx_b = IdxTable64(0, code, scope, Name(idx_table_base | u64(0)))
self.idx_c = IdxTable128(1, code, scope, Name(idx_table_base | u64(1)))
def store(self, item: A, payer: Name) -> Iterator[A]:
id: u64 = item.get_primary()
it = self.table.store(item, payer)
self.idx_b.store(id, item.b(), payer)
self.idx_c.store(id, item.c(), payer)
return it
def update(self, it: Iterator[A], item: A, payer: Name):
self.table.update(it, item, payer)
primary = item.get_primary()
secondary = item.b()
it_secondary, old_secondary = self.idx_b.find_by_primary(primary)
if not secondary == old_secondary:
self.idx_b.update(it_secondary, secondary, payer)
secondary = item.c()
it_secondary, old_secondary = self.idx_c.find_by_primary(primary)
if not secondary == old_secondary:
self.idx_c.update(it_secondary, secondary, payer)
def remove(self, it: Iterator[A]):
sec_it, _ = self.idx_b.find_by_primary(it.get_primary())
self.idx_b.remove(sec_it)
sec_it, _ = self.idx_c.find_by_primary(it.get_primary())
self.idx_c.remove(sec_it)
self.table.remove(it)
def remove(self, primary: u64):
it = self.table.find(primary)
if it.is_ok():
self.remove(it)
def get_idx_table_by_b(self) -> IdxTable64:
return self.idx_b
def get_idx_table_by_c(self) -> IdxTable128:
return self.idx_c
def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
self.idx_b.update(it, b, payer)
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
value: A = it_primary.get_value()
value.b = secondary[u64](b)
self.table.update(it_primary, value, payer)
def update_c(self, it: SecondaryIterator, c: u128, payer: Name) -> None:
self.idx_c.update(it, c, payer)
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
value: A = it_primary.get_value()
value.c = secondary[u128](c)
self.table.update(it_primary, value, payer)
@extend
class A:
def new_table(code: Name, scope: Name):
return MultiIndexA(code, scope, n"mytable")
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it = idx_table_b.find(2u64)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
idx_table_c = table.get_idx_table_by_c()
it = idx_table_c.find(3u128)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
这个例子展示了有二个二重索引的例子,只是把table
改成了packer
,编译器即不再会生成数据库相关的代码。
对比可知,编译器在编译的时候会生成MultiIndexA
这个继承自mi.codon
中定义的MultiIndexBase
类,
这个类有以下几个方法:
- def store(self, item: A, payer: Name) -> Iterator[A]
- def update(self, it: Iterator[A], item: A, payer: Name)
- def remove(self, it: Iterator[A])
- def get_idx_table_by_b(self) -> IdxTable64:
- def get_idx_table_by_c(self) -> IdxTable128:
- def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
- def update_c(self, it: SecondaryIterator, c: u128, payer: Name) -> None:
同时会为类A
生成额外的方法:
get_primary
获取主索引get_idx_table_by_b
, 用于获取二重索引b
的表,返回的类是IdxTable64
get_idx_table_by_c
,用于获取二重索引c
的表,返回的类为IdxTable128
。new_table
测试代码:
def test_example6():
t = init_db_test('db_example6')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
编译:
python-contract build db_example/db_example6.codon
运行测试:
ipyeos -m pytest -s -x test.py -k test_example6
输出:
++++++it.primary: 1
++++++it.primary: 1
总结
EOS中的数据存储功能是比较完善的,并且有二重索引表的功能,使数据的查找变得非常的灵活。本章详细讲解了数据库表的增,删,改,查的代码。本章的内容较多,需要花点时间好好消化。可以在示例的基础上作些改动,并且尝试运行以增加对这章知识点的理解。