本文主要针对 Fabric 1.0 并使用 LevelDB为数据库的情况下进行的复合主键与区间查询的golang语言实现。
在Hyper Ledge的官方指南中有基础的Chaincode demo,通过该教程,我们可以完整的完成Chaincode的开发工作。但是由于篇幅限制,该文档中仅包括了简单的单主键查询。而这往往不能满足我们的实际需要。
在实际的环境中,对于查询功能除了简单的键值对查询,我们往往还有如下两个需求:
- 富查询 :对数据的某一个属性进行查询获取所有满足条件的数据,例如所有颜色为红色的汽车信息。
- 区间查询:对一个范围内的键值进行查询获取数据,例如获取单号在005至008之间的订单信息。
下面我们将具体针对这两个需求来说一说如何用golang 实现对应的功能。
富查询
不少文章在提到Chaincode 富查询时就会提到CouchDB,诚然使用CouchDB可以很方便的实现富查询的功能而不需要我们自己做额外的工作。但其实使用LevelDB同样可以实现富查询。
那首先我们先来看看如何在CouchDB的情况下实现富查询:
使用CouchDB 实现富查询
CouchDB通过对Value的内容进行查询来实现富查询的需求。更具体的内容我们可以查阅官方文档。
CouchDB 使用MangoDB API Layer 来实现Query语法,需要传入一个queryString查询字符串,其语法可以在Github查看。
我们首先定义一下数据结构
type Car struct {
Color string `json:"Color"`
ID string `json:"ID"` // key
Price string `json:"Price"`
LaunchDate string `json:"LaunchDate"`
}
现在我们就来看一下如何实现使用CouchDB当Key为ID的情况下对Value中的Color字段进行查询:
\\使用CouchDB数据库
func (t *CarchainCode) queryByColor(stub shim.ChaincodeStubInterface, args []string) Car.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
color := args[0]
queryString := fmt.Sprintf(`{"selector":{"Color":"%s"}}`, color) //Mongo Query string 语法见上文链接
resultsIterator, err := stub.GetQueryResult(queryString) // 富查询的返回结果可能为多条 所以这里返回的是一个迭代器 需要我们进一步的处理来获取需要的结果
if err != nil {
return shim.Error("Rich query failed")
}
defer resultsIterator.Close() //释放迭代器
var buffer bytes.Buffer
bArrayMemberAlreadyWritten := false
buffer.WriteString(`{"result":[`)
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next() //获取迭代器中的每一个值
if err != nil {
return shim.Error("Fail")
}
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString(string(queryResponse.Value)) //将查询结果放入Buffer中
bArrayMemberAlreadyWritten = true
}
buffer.WriteString(`]}`)
fmt.Print("Query result: %s", buffer.String())
return shim.Success(buffer.Bytes())
}
使用LevelDB 实现富查询
在刚刚的实现过程中,可以看到的是我们使用了CouchDB等状态数据库特有的对Value内容进行查询的功能。那么是不是我们就不能用LevelDB实现了呢?答案显然是否定的,只是需要我们做一些额外的工作。
我们继续上面的里,要想在使用LevelDB的情况下按颜色查询Car的需求,需要按如下建立索引:
indexName := "color~id"
colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{car.Color, car.ID}) //创建Color与ID的组合键
if err != nil {
return shim.Error("Fail to create Composite key")
}
value := []byte{0x00}
stub.PutState(colorNameIndexKey, value) // 将索引信息保保存在Key中
有了上面这个索引之后我们就可以通过它来实现富查询了
colorIdResultsIterator, err := stub.GetStateByPartialCompositeKey ("color~id", []string{color}) //返回包含给出颜色的组合键的迭代器
if err != nil {
return shim.Error(err.Error())
}
defer colorIdResultsIterator.Close()
for resultsIterator.HasNext() {
colorIdKey, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
objectType, compisiteKeys, err := stub.SplitCompositeKey(string(colorIdKey.Key)) //通过SplitCompositeKey 解析出Car的主键 ID
returnColor := compisiteKeys[0]
returnId := compisiteKeys[1]
fmt.Print("found a car from index %s color: %s id %s\n",objectType,returnColor,returnId)
carBytes, err := stub.GetState(returnId) // 根据解析出的ID获取数据
//之后的步骤与上一个例子想类似这里就不赘述了
}
一些补充
上面我们主要运用了shim包中关于组合键的方法这里放上他们的官方文档
// GetStateByPartialCompositeKey queries the state in the ledger based on
// a given partial composite key. This function returns an iterator
// which can be used to iterate over all composite keys whose prefix matches
// the given partial composite key. The `objectType` and attributes are
// expected to have only valid utf8 strings and should not contain
// U+0000 (nil byte) and U+10FFFF (biggest and unallocated code point).
// See related functions SplitCompositeKey and CreateCompositeKey.
// Call Close() on the returned StateQueryIteratorInterface object when done.
// The query is re-executed during validation phase to ensure result set
// has not changed since transaction endorsement (phantom reads detected).
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
// CreateCompositeKey combines the given `attributes` to form a composite
// key. The objectType and attributes are expected to have only valid utf8
// strings and should not contain U+0000 (nil byte) and U+10FFFF
// (biggest and unallocated code point).
// The resulting composite key can be used as the key in PutState().
CreateCompositeKey(objectType string, attributes []string) (string, error)
// SplitCompositeKey splits the specified key into attributes on which the
// composite key was formed. Composite keys found during range queries
// or partial composite key queries can therefore be split into their
// composite parts.
SplitCompositeKey(compositeKey string) (string, []string, error)
这里我们需要注意到一点GetStateByPartialCompositeKey
方法是采用一种前缀匹配的方法来进行键的匹配返回的。也就是说,我们虽然是部分复合键的查询,但是只能拿前面的复合键进行匹配,而不是后面部分。具体来说当你有一个 出场年份~颜色~车号的索引时只能使用 年份、年份与颜色来进行查询,而不能用颜色来进行查询。因此当我们有多键的复合主键时,各个键的顺序可能需要我们仔细思考一下。
区间查询
除了上文所说的富查询外区间查询也是一个常用的功能。下面我们就来掩饰一下如何在chaincode中实现区间查询。
通过查询官方的文档我们可以发现下面这个方法。
// GetStateByRange returns a range iterator over a set of keys in the
// ledger. The iterator can be used to iterate over all keys
// between the startKey (inclusive) and endKey (exclusive).
// The keys are returned by the iterator in lexical order. Note
// that startKey and endKey can be empty string, which implies unbounded range
// query on start or end.
// Call Close() on the returned StateQueryIteratorInterface object when done.
// The query is re-executed during validation phase to ensure result set
// has not changed since transaction endorsement (phantom reads detected).
GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
通过给出需要查询区间的开始键与结束键(开始键与结束键按字典顺序排序)获得区间查询的结果。(包含开始键、不包括结束键的半闭半开区间)我们依旧用上面的数据结构来举例:
func (t *SimpleChaincode) rangeQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response {
resultsIterator, err := stub.GetStateByRange("Car:1", "Car:3") //这里应为传入参数,但为了简化这里直接Hard code 为 car1 、 car3
if err != nil {
return shim.Error("Query by Range failed")
}
defer resultsIterator.Close() //释放迭代器
var buffer bytes.Buffer
bArrayMemberAlreadyWritten := false
buffer.WriteString(`{"result":[`)
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next() //获取迭代器中的每一个值
if err != nil {
return shim.Error("Fail")
}
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString(string(queryResponse.Value)) //将查询结果放入Buffer中
bArrayMemberAlreadyWritten = true
}
buffer.WriteString(`]}`)
fmt.Print("Query result: %s", buffer.String())
return shim.Success(buffer.Bytes())
}
有一点需要补充的是当你有一条键为Car:12
的记录时它也将出现在上面这个函数的返回值里。有经验的朋友肯定一下子就知道了这里发生了什么问题,没错GetStateByRange
是按字典顺序来决定返回的,所以我们将 Key设置为 Car:000001
就可以解决这个问题。
思考一下
通过上面两部分的介绍相信大家已经掌握了fabric chaincode 区间查询与富查询的方法。这里我们再来思考这样一个需求:
- 查询所有生产年份在 2016~2018年之间的汽车。
猛地一看好像无从下手,但只要仔细想一下就不能发现这只是一个区间查询套一个富查询而已。下面我们具体来设计一下如何满足这个需求。
首先因为要将生产年份作为查询条件所以我们需要创建一个 生产年份~车号的索引。到这一步我们已经可以解决在2016/2017/2018生产的汽车了,
那么怎么把他们合起来呢?其实非常简单我们只需要把年份单独作为一个Key存入数据库中。这样实现这个功能的步骤就是:
- 首先通过
GetStateByRange
获取年份区间的迭代器 - 遍历每个年份通过
stub.GetStateByPartialCompositeKey
对每个年份再根据索引获得一个该年份车号的迭代器。 - 根据车号迭代器(在这个例子中一共会有3个)获取最终的结果。