圖資料庫 NebulaGraph 的 Java 資料解析實踐與指導

NebulaGraph發表於2023-04-19

如何快速、即時、符合直覺地去處理 Nebula Java Client 中的資料解析?讀這一篇就夠了。

圖資料庫 NebulaGraph 的論壇和微信群裡,有不少使用者問及了 Java 客戶端資料解析的問題。在本文教你一種簡單的方式同返回結果互動,快速、即時地拿到解析資料。

愉快、乾淨的 Java 互動環境

本文最為關鍵步驟之一,便是用幾行程式碼,準備一個乾淨的互動式 NebulaGraph Java REPL 環境。

多虧了 Java-REPL,我們可以很方便地(像 iPython 那樣)去實時互動地除錯、分析 NebulaGraph Java 客戶端。

下面,開始實操。

先用 Docker 映象準備環境:

docker pull albertlatacz/java-repl
docker run --rm -it \
    --network=nebula-net \
    -v ~:/root \
    albertlatacz/java-repl \
    bash
apt update -y && apt install ca-certificates -y
wget https://dlcdn.apache.org/maven/maven-3/3.8.6/binaries/apache-maven-3.8.6-bin.tar.gz --no-check-certificate

tar xzvf apache-maven-3.8.6-bin.tar.gz

wget https://github.com/vesoft-inc/nebula-java/archive/refs/tags/v3.0.0.tar.gz
tar xzvf v3.0.0.tar.gz
cd nebula-java-3.0.0/
../apache-maven-3.8.6/bin/mvn dependency:copy-dependencies
../apache-maven-3.8.6/bin/mvn -B package -Dmaven.test.skip=true

java -jar ../javarepl/javarepl.jar

在執行完上面的 java -jar ../javarepl/javarepl.jar 之後,我們就進入了互動式的 Java Shell(REPL)。我們不用再做編譯、執行、print 這樣的慢反饋來除錯和研究我們的程式碼了,是不是很方便?

root@a2e26ba62bb6:/javarepl/nebula-java-3.0.0# java -jar ../javarepl/javarepl.jar

Welcome to JavaREPL version 428 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_111)
Type expression to evaluate, :help for more options or press tab to auto-complete.
Connected to local instance at http://localhost:43707

java> System.out.println("Hello, World!");
Hello, World!
java>

首先我們在 java> 提示符下,這些來把必須的類路徑和匯入:

:cp /javarepl/nebula-java-3.0.0/client/target/client-3.0.0.jar
:cp /javarepl/nebula-java-3.0.0/client/target/dependency/fastjson-1.2.78.jar
:cp /javarepl/nebula-java-3.0.0/client/target/dependency/slf4j-api-1.7.25.jar
:cp /javarepl/nebula-java-3.0.0/client/target/dependency/slf4j-log4j12-1.7.25.jar
:cp /javarepl/nebula-java-3.0.0/client/target/dependency/commons-pool2-2.2.jar
:cp /javarepl/nebula-java-3.0.0/client/target/dependency/log4j-1.2.17.jar

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.vesoft.nebula.ErrorCode;
import com.vesoft.nebula.client.graph.NebulaPoolConfig;
import com.vesoft.nebula.client.graph.data.CASignedSSLParam;
import com.vesoft.nebula.client.graph.data.HostAddress;
import com.vesoft.nebula.client.graph.data.ResultSet;
import com.vesoft.nebula.client.graph.data.SelfSignedSSLParam;
import com.vesoft.nebula.client.graph.data.ValueWrapper;
import com.vesoft.nebula.client.graph.net.NebulaPool;
import com.vesoft.nebula.client.graph.net.Session;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.*;

我們可以從這 Java 環境連線到 NebulaGraph。在下面的例子中,我用了自己的 graphd 的 IP 和埠作為例子:

NebulaPoolConfig nebulaPoolConfig = new NebulaPoolConfig();
nebulaPoolConfig.setMaxConnSize(10);
List<HostAddress> addresses = Arrays.asList(new HostAddress("192.168.8.127", 9669));
NebulaPool pool = new NebulaPool();
pool.init(addresses, nebulaPoolConfig);
Session session = pool.getSession("root", "nebula", false);

透過呼叫 execute 方法獲得不太容易懂的 ResultSet 物件

剛接觸 NebulaGraph Java 客戶端的大家一定對這個 ResultSet 物件有些愁。別擔心,藉助我們的環境,十分鐘把它搞通。這裡我們執行一個簡單的返回 vertex 頂點的結果看看:

ResultSet resp = session.execute("USE basketballplayer;MATCH (n:player) WHERE n.name==\"Tim Duncan\" RETURN n");

我們可以參考 ResultSet 的程式碼:client/graph/data/ResultSet.java

其實可以先不看,跟著教程往下走。一般來說,查詢結果都是二維表,ResultSet 針對行和列提供了常見的處理方法。通常,我們會獲取每一行結果,再解析它,而關鍵的問題是每一個值要怎麼處理。

java> resp.isSucceeded()
java.lang.Boolean res9 = true

java> resp.rowsSize()
java.lang.Integer res16 = 1

java> rows = resp.getRows()
java.util.ArrayList rows = [Row (
  values : [
    <Value vVal:Vertex (
        vid : <Value sVal:70 6c 61 79 65 72 31 30 30>,
        tags : [
          Tag (
              name : 70 6C 61 79 65 72,
              props : {
                [B@5264a468 : <Value iVal:42>
                [B@496b8e10 : <Value sVal:54 69 6d 20 44 75 6e 63 61 6e>
              }
            )
        ]
      )>
  ]
)]
    
java> row0 = resp.rowValues(0)
java.lang.Iterable<com.vesoft.nebula.client.graph.data.ValueWrapper> res10 = ColumnName: [n], Values: [("player100" :player {name: "Tim Duncan", age: 42})]

回到本次 query 語句,它其實是在返回一個 vertex 頂點:

(root@nebula) [basketballplayer]> match (n:player) WHERE n.name == "Tim Duncan" return n
+----------------------------------------------------+
| n                                                  |
+----------------------------------------------------+
| ("player100" :player{age: 42, name: "Tim Duncan"}) |
+----------------------------------------------------+
Got 1 rows (time spent 2116/44373 us)

透過上面的幾個方法,我們其實能夠獲得這個頂點的值:

v = Class.forName("com.vesoft.nebula.Value")
v.getDeclaredMethods()

然而,這個 com.vesoft.nebula.Value 的值的類提供的方法特別原始,這也是讓大家犯愁資料解析的原因。所以,在這個教程中最重要的一個帶走的經驗(除了利用 REPL 之外)就是:非必要不要去取這個原始的類,我們應該去取得 ValueWrapper 封裝之後的值!!!

注意:其實我們有更輕鬆地方法,就是用 executeJson 直接獲得 JSON string。別擔心,會在後面提到,不過這個方法要 2.6 之後才支援。

那麼問題來了,如何使用 ValueWrapper 封裝呢?其實答案已經在上面了,大家可以回去看看,resp.rowValues(0) 的型別正是 ValueWrapper 的可迭代物件!

所以,正確開啟方式是迭它!迭它!迭它!其實這個就是程式碼庫裡的 GraphClientExample 的一部分例子了,我們把它迭代取出來,放到 wrappedValueList 裡慢慢把玩:

import java.util.ArrayList;
import java.util.List;
List<ValueWrapper> wrappedValueList = new ArrayList<>();

for (int i = 0; i < resp.rowsSize(); i++) {
    ResultSet.Record record = resp.rowValues(i);
    for (ValueWrapper value : record.values()) {
        wrappedValueList.add(value);
        if (value.isLong()) {
            System.out.printf("%15s |", value.asLong());
        }
        if (value.isBoolean()) {
            System.out.printf("%15s |", value.asBoolean());
        }
        if (value.isDouble()) {
            System.out.printf("%15s |", value.asDouble());
        }
        if (value.isString()) {
            System.out.printf("%15s |", value.asString());
        }
        if (value.isTime()) {
            System.out.printf("%15s |", value.asTime());
        }
        if (value.isDate()) {
            System.out.printf("%15s |", value.asDate());
        }
        if (value.isDateTime()) {
            System.out.printf("%15s |", value.asDateTime());
        }
        if (value.isVertex()) {
            System.out.printf("%15s |", value.asNode());
        }
        if (value.isEdge()) {
            System.out.printf("%15s |", value.asRelationship());
        }
        if (value.isPath()) {
            System.out.printf("%15s |", value.asPath());
        }
        if (value.isList()) {
            System.out.printf("%15s |", value.asList());
        }
        if (value.isSet()) {
            System.out.printf("%15s |", value.asSet());
        }
        if (value.isMap()) {
            System.out.printf("%15s |", value.asMap());
        }
    }
    System.out.println();
}

上邊這些很醜的 if 就是關鍵了,我們知道 query 的返回值可能是多種型別的,他們分為:

  • 圖語義的:點、邊、路徑
  • 資料型別:String,日期,列表,集合…等等

這裡的關鍵是,我們要使用 ValueWrapper 為我們準備好 asXxx 方法。如果這個值是一個頂點,那麼這個 Xxx 就是 Node。同理如果是邊的話,這個 Xxx 就是 Relationship。

所以,我給大家看看我們們這個返回點結果的情況下的 asNode() 方法:

java> v = wrappedValueList.get(0)
com.vesoft.nebula.client.graph.data.ValueWrapper v = ("player100" :player {name: "Tim Duncan", age: 42})
java> v.asNode()
com.vesoft.nebula.client.graph.data.Node res16 = ("player100" :player {name: "Tim Duncan", age: 42})
java> node = v.asNode()
com.vesoft.nebula.client.graph.data.Node node = ("player100" :player {name: "Tim Duncan", age: 42})

順便說一下,藉助於 Java 的反射 reflection,我們可以在這個互動程式裡做類似於 Python 裡 dir() 的事情:實時地去獲取一個類支援的方法。像這樣,省去了查程式碼的時間。

java> rClass=Class.forName("com.vesoft.nebula.client.graph.data.ResultSet")
java.lang.Class r = class com.vesoft.nebula.client.graph.data.ResultSet
java> rClass.getDeclaredMethods()
java.lang.reflect.Method[] res20 = [public java.util.List com.vesoft.nebula.client.graph.data.ResultSet.getColumnNames(), public int com.vesoft.nebula.client.graph.data.ResultSet.rowsSize(), public com.vesoft.nebula.client.graph.data.ResultSet$Record com.vesoft.nebula.client.graph.data.ResultSet.rowValues(int), public java.util.List com.vesoft.nebula.client.graph.data.ResultSet.colValues(java.lang.String), public java.lang.String com.vesoft.nebula.client.graph.data.ResultSet.getErrorMessage(), public boolean com.vesoft.nebula.client.graph.data.ResultSet.isSucceeded(), public int com.vesoft.nebula.client.graph.data.ResultSet.getErrorCode(), public java.lang.String com.vesoft.nebula.client.graph.data.ResultSet.getSpaceName(), public int com.vesoft.nebula.client.graph.data.ResultSet.getLatency(), public com.vesoft.nebula.graph.PlanDescription com.vesoft.nebula.client.graph.data.ResultSet.getPlanDesc(), public java.util.List com.vesoft.nebula.client.graph.data.ResultSet.getRows(), public java.lang.String com.vesoft.nebula.client.graph.data.ResultSet.getComment(), public java.lang.String com.vesoft.nebula.client.graph.data.ResultSet.toString(), public boolean com.vesoft.nebula.client.graph.data.ResultSet.isEmpty(), public java.util.List com.vesoft.nebula.client.graph.data.ResultSet.keys()]

這樣:

java> nodeClass=Class.forName("com.vesoft.nebula.client.graph.data.Node")
java.lang.Class nodeClass = class com.vesoft.nebula.client.graph.data.Node
java> nodeClass.getDeclaredMethods()
java.lang.reflect.Method[] res20 = [public boolean com.vesoft.nebula.client.graph.data.Node.hasTagName(java.lang.String), public boolean com.vesoft.nebula.client.graph.data.Node.hasLabel(java.lang.String), public java.util.List com.vesoft.nebula.client.graph.data.Node.tagNames(), public java.util.HashMap com.vesoft.nebula.client.graph.data.Node.properties(java.lang.String) throws java.io.UnsupportedEncodingException, public java.util.List com.vesoft.nebula.client.graph.data.Node.labels(), public boolean com.vesoft.nebula.client.graph.data.Node.equals(java.lang.Object), public java.lang.String com.vesoft.nebula.client.graph.data.Node.toString(), public java.util.List com.vesoft.nebula.client.graph.data.Node.values(java.lang.String), public int com.vesoft.nebula.client.graph.data.Node.hashCode(), public com.vesoft.nebula.client.graph.data.ValueWrapper com.vesoft.nebula.client.graph.data.Node.getId(), public java.util.List com.vesoft.nebula.client.graph.data.Node.keys(java.lang.String) throws java.io.UnsupportedEncodingException]

看到這裡,大家應該體會到封裝了 ValueWrapper 的好處了吧?它提供了方便的符合直覺的方法,對於 Node 型別來說,它提供了 tagNames()properties()labels() 等等非常好用的方法:

java> node.properties("player")
java.util.HashMap res11 = {name="Tim Duncan", age=42}
java> node.tagNames()
java.util.ArrayList res12 = [player]
java> node.labels()
java.util.ArrayList res13 = [player]
java> node.values("player")
java.util.ArrayList res14 = [42, "Tim Duncan"]

我們這裡只展示了頂點資料型別的處理、解析方式(RETURN n),像其他的資料型別比如邊(edge)、路徑(path)或者地理資料、時間資料,用這種方式(看有什麼方法,再互動地去試試方法怎麼用)也是一樣的,對吧?

直接返回 JSON 的 executeJson 方法

最後,好訊息是:從 v2.6 開始,NebulaGraph 可以直接返回 JSON 的 String 了,我們上面的糾結也都不是必要的了:

java> String resp_json = session.executeJson("USE basketballplayer;MATCH (n:player) WHERE n.name==\"Tim Duncan\" RETURN n");

java.lang.String resp_json = "
{
   "errors":[
      {
         "code":0
      }
   ],
   "results":[
      {
         "spaceName":"basketballplayer",
         "data":[
            {
               "meta":[
                  {
                     "type":"vertex",
                     "id":"player100"
                  }
               ],
               "row":[
                  {
                     "player.age":42,
                     "player.name":"Tim Duncan"
                  }
               ]
            }
         ],
         "columns":[
            "n"
         ],
         "errors":{
            "code":0
         },
         "latencyInUs":4761
      }
   ]
}
"

我相信大家肯定比我更擅長處理 JSON 的結果了哈~~

結論

  • 如果你有條件(v2.6 及其以上版本)用 JSON,情況會很容易,甚至你都不太需要本文的方法,不過本文可能會讓你的互動環境更容易;
  • 如果你不得不和 ResultSet 打交道,記得用 ValueWrapper。因為我們可以用 asNode()asRelationship()asPath(),封裝之後的值比原始的值可愛太多了!

    • 透過 REPL 工具,結合 Java 的 reflection 加上原始碼本身,分析資料的處理將變得異常順滑。

Happy Graphing!


謝謝你讀完本文 (///▽///)

NebulaGraph Desktop,Windows 和 macOS 使用者安裝圖資料庫的綠色通道,10s 拉起搞定海量資料的圖服務。通道傳送門:http://c.nxw.so/9ShUq

想看原始碼的小夥伴可以前往 GitHub 閱讀、使用、(^з^)-☆ star 它 -> GitHub;和其他的 NebulaGraph 使用者一起交流圖資料庫技術和應用技能,留下「你的名片」一起玩耍呢~

相關文章