HyperLedger Fabric Chaincode 高级查询 golang 实现

本文主要针对 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查看。

Mango查询

我们首先定义一下数据结构

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存入数据库中。这样实现这个功能的步骤就是:

  1. 首先通过GetStateByRange获取年份区间的迭代器
  2. 遍历每个年份通过stub.GetStateByPartialCompositeKey 对每个年份再根据索引获得一个该年份车号的迭代器。
  3. 根据车号迭代器(在这个例子中一共会有3个)获取最终的结果。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容