Fabric链码开发——富查询
常见的链码使用PutState和GetState就可以进行简单的增加和查找了,再不济就加入DelState。因为fabric底层都是KV数据库,所以这三个接口就可以进行增删改查的基本需求了。但是,这都是单个数据的操作如果要进行功能较为强大的数据操作,只使用这个是完全不够的。查询时一切数据操作的开始,通过官方文档,找到了这两个支持富查询的函数。(富查询只支持在CouchDB 中使用)
源代码
func (s *ChaincodeStub) GetQueryResult(query string) (StateQueryIteratorInterface, error)
func (s *ChaincodeStub) GetQueryResultWithPagination(query string, pageSize int32,
bookmark string) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error)
第一个是一个简单的富查询函数,第二个是一个支持富查询的分页查询函数
// GetQueryResult documentation can be found in interfaces.go
func (s *ChaincodeStub) GetQueryResult(query string) (StateQueryIteratorInterface, error) {
// Access public data by setting the collection to empty string
collection := ""
// ignore QueryResponseMetadata as it is not applicable for a rich query without pagination
iterator, _, err := s.handleGetQueryResult(collection, query, nil)
return iterator, err
}
// GetQueryResultWithPagination ...
func (s *ChaincodeStub) GetQueryResultWithPagination(query string, pageSize int32,
bookmark string) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error) {
// Access public data by setting the collection to empty string
collection := ""
metadata, err := createQueryMetadata(pageSize, bookmark)
if err != nil {
return nil, nil, err
}
return s.handleGetQueryResult(collection, query, metadata)
}
以上是他们的源码,实际上可以发现,他们都是调用handleGetQueryResult
这个函数,分页查询函数知识多了一步创建metadata,将范围打包。之后可以和GetStateByRange(startKey, endKey string)
的函数做比较。
bookmark是书签,内容是一个无序的字符数组,如果为空则会重头查询,如果不为空,则会从会从它所代表的位置开始查询,在每次分页查询的时候都可以得到一个bookmark,也就是说,下一页的查询需要带上一页的bookmark。
func (s *ChaincodeStub) handleGetQueryResult(collection, query string,
metadata []byte) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error) {
//获取查询结果,这一步就使用了metadata进行分页
response, err := s.handler.handleGetQueryResult(collection, query, metadata, s.ChannelID, s.TxID)
if err != nil {
return nil, nil, err
}
//创建迭代器
iterator := s.createStateQueryIterator(response)
//进行再次封装
responseMetadata, err := createQueryResponseMetadata(response.Metadata)
if err != nil {
return nil, nil, err
}
return iterator, responseMetadata, nil
}
和这两个富查询函数一起使用handleGetQueryResult
的,还有一个GetPrivateDataQueryResult
函数。这个函数看起来是查询私有数据,但私有数据是什么暂且不知道,按下不提。这四个函数都是在shim/stub.go中。
索引及数据结构
富查询目前只有couchDB支持,这是一个比较依赖数据库的功能,那就必须要有考虑一个问题:索引。参考Fabric的中文文档可以建立索引。
特别注意两点:
1.索引是依据需求来的,如果太多的索引会影响查询速率
2.索引的目录是固定的,链码同目录下的META-INF/statedb/couchdb/indexes
一个完整的索引如下:
{
"index":{
//要查询的字段名称,一般情况下要包含docType,对于docType的解释在后面
"fields":["docType","owner"] // Names of the fields to be queried
},
// (可选)将创建索引的设计文件的名称。如果是一个文件包含一个索引可以不使用,官方建议也是单个索引
"ddoc":"indexOwnerDoc",
"name":"indexOwner",
"type":"json"
}
我在官方文档中还看到了一个数据结构的规范写法
type marble struct {
//docType用于区分状态数据库中的各种类型的对象
ObjectType string `json:"docType"`
//以下的正常的数据结构
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
docType
用来识别这个文档的数据类型。 同时在链码数据库中也可能存在其他文档。数据库中的文档对于这些属性值来说都是可查询的。
常用函数
以下是我根据文档中给出的示例总结的富查询常用函数,主要使用getQueryResultForQueryString
和getQueryResultForQueryStringWithPagination
,后两个函数是为分页富查询函数使用的。可以写在智能合约中调用。
=========================================================================================
// getQueryResultForQueryString执行传入的查询字符串。
// 结果集被建立并作为一个字节数组返回,其中包含JSON结果。
=========================================================================================
func getQueryResultForQueryString(APIstub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {
fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString)
resultsIterator, err := APIstub.GetQueryResult(queryString)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
buffer, err := constructQueryResponseFromIterator(resultsIterator)
if err != nil {
return nil, err
}
fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String())
return buffer.Bytes(), nil
}
=========================================================================================
// getQueryResultForQueryStringWithPagination执行传入的查询字符串,其中包括
// 分页信息。结果集被建立并作为一个字节数组返回,其中包含JSON结果。
=========================================================================================
func getQueryResultForQueryStringWithPagination(APIstub shim.ChaincodeStubInterface, queryString string, pageSize int32, bookmark string) ([]byte, error) {
fmt.Printf("- getQueryResultForQueryStringWithPagination queryString:\n%s\n", queryString)
resultsIterator, responseMetadata, err := APIstub.GetQueryResultWithPagination(queryString, pageSize, bookmark)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
buffer, err := constructQueryResponseFromIterator(resultsIterator)
if err != nil {
return nil, err
}
bufferWithPaginationInfo := addPaginationMetadataToQueryResults(buffer, responseMetadata)
fmt.Printf("- getQueryResultForQueryStringWithPagination queryResult:\n%s\n", bufferWithPaginationInfo.String())
return buffer.Bytes(), nil
}
=========================================================================================
// constructQueryResponseFromIterator构建一个JSON数组,其中包含来自给定结果迭代器的查询结果。
// 一个给定的结果迭代器=========================================================================================
func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) (*bytes.Buffer, error) {
// buffer is a JSON array containing QueryResults
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record is a JSON object, so we write as-is
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
return &buffer, nil
}
=========================================================================================
// addPaginationMetadataToQueryResults 将包含分页的QueryResponseMetadata
// 的信息,添加到构建的查询结果中。
=========================================================================================
func addPaginationMetadataToQueryResults(buffer *bytes.Buffer, responseMetadata *pb.QueryResponseMetadata) *bytes.Buffer {
buffer.WriteString("[{\"ResponseMetadata\":{\"RecordsCount\":")
buffer.WriteString("\"")
buffer.WriteString(fmt.Sprintf("%v", responseMetadata.FetchedRecordsCount))
buffer.WriteString("\"")
buffer.WriteString(", \"Bookmark\":")
buffer.WriteString("\"")
buffer.WriteString(responseMetadata.Bookmark)
buffer.WriteString("\"}}]")
return buffer
}
下面是我写的一个调用分页函数的合约函数(我把可以在SDK中调用的智能合约函数叫做合约函数)示例
func (s *SmartContract) queryCourseWithPagination(APIstub shim.ChaincodeStubInterface, args []string) pb.Response {
fmt.Println("func queryCourseWithPagination start")
fmt.Printf("args:%v\n", args)
if len(args) != 3 {
return shim.Error("arguments num not enough," + strconv.Itoa(len(args)))
}
queryString := args[0]
//return type of ParseInt is int64
pageSize, err := strconv.ParseInt(args[1], 10, 32)
if err != nil {
return shim.Error(err.Error())
}
bookmark := args[2]
//之前是获取参数,在这一步调用分页富查询函数,查询出结果后转为字节流返回
queryResults, err := getQueryResultForQueryStringWithPagination(APIstub, queryString, int32(pageSize), bookmark)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(queryResults)
}