使用TinkerPop框架对GDB增删改查

Posted 阿里巴巴淘系技术团队官网博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用TinkerPop框架对GDB增删改查相关的知识,希望对你有一定的参考价值。

本文介绍了使用GDB作为存储,进行服务端开发中需要注意的点。并以TinkerPop框架实现了几个常用的例子,展示GDB操作增删查改时需要注意的地方,以及使用两种方式提交GDB操作的差异。

引言

作为比较出名比较通用的图数据库的服务端框架之一,TinkerPop (地址:https://tinkerpop.apache.org/)with Gremlin应该算是大家的常用选择。有同学关于阿里在用的几种图数据库进行了调研,总结了如下的优缺点:


查询语言

数据加载

商业化

管控运维

特点

GDB

部分Gremlin

支持Cypher

批量:MaxCompute,OSS

增量:SDK

阿里云产品

rds管控

阿里云图计算团队基于开源Gremlin实现的图引擎,完全依托于阿里云基础设施,作为云产品具有商业化应用。服务高可用、易运维,支持ACID事务,

GraphCompute

支持Gremlin

批量:MaxCompute

增量:

Blink,SDK

依托于Dataworks

基于Dataworks

Dataworks与达摩院合作产品,自建分布式执行引擎,专注于图计算中的单节点并行执行优化。

内置大量图查询及分析算法,对机器学习场景支持度高

与Dataworks无缝打通,操作界面好

TuGraph

自研图查询语言、部分Gremlin语法


蚂蚁对外开放试用中

自研IDE

针对金融领域特殊的巨型复杂网络和超大实时更新数据而研发的分布式图数据库,高性能高可用

Neo4j

支持Cypher


开源产品



可以看到上述数据库中大多数支持Gremlin语法。又由于业务需求中需要ACID事务保障写入一致性,因此选用了GDB作为数据存储。

在TinkerPop官网的友链中,可以找到一些关于tinkerpop-java的API Doc。但实际使用中,我们发现不少框架中提供的API都会在GDB上执行错误,引起如:

org.apache.tinkerpop.gremlin.driver.exception.ResponseException: Invalid OpProcessor requested

之类的错误。

而网上搜索GDB,其实很难获得比较完整的文献解决方案。所以,借此文把GDB相关的趟坑经验与大家分享。

背景

由于业务中需要处理、存储类图数据,因此选用了GDB作为存储库。GDB官方例子中大多数为直接提交字符串型的脚本(Script)来进行库的CRUD操作的。但:

  1. 不少业务逻辑的初始筛选条件类似,后续的操作分散,由于Gremlin语法类似链式操作,直接使用String拼接的话无法在编码中对上一步返回的数据类型进行预判,导致编写逻辑的缺失或错误;

  2. 节点写入和边增加的操作依赖复杂的业务判重逻辑,需要通过事务包裹,而官方文档并没有给出事务操作的例子;

  3. TinkerPop提供的部分API在GDB上无法使用,产生奇妙的报错(如SessionedClient、tx、Transaction等)。

另,本人的后端代码能力有待提高,例子的分层仅供参考,欢迎斧正和讨论[手动狗头]。

依赖引用

在综合了各种资料后,目前GDB的最佳maven依赖是:

https://github.com/aliyun/alibabacloud-gdb-java-sdk

使用git clone后maven install一下(maven公共repo没有可用发布包)。

因为:

  1. TinkerPop的官方maven包没有实现对GDB的事务支持

  2. GDB-Java-SDK的1.0.1正式版本没有source和doc

在alibabacloud-gdb-java-driver依赖中,引用了tinkerpop的gremlin-driver和gremlin-core这两个包,版本号是3.4.2;同时有junit.jupiter和slf4j的引用。请注意排包。

以下逻辑就按照GDB-JAVA-SDK内提供的API来进行介绍。

DAO层封装

由于GDB支持Script和Bytecode两种提交模式,因此DAO层有以下几种方式生成:

  1. 实现Mybatis的接口和模板,实现Script的模板拼接,有人把这个能力实现写了专利(地址:http://www.xjishu.com/zhuanli/55/202210251927.html);--这部分实现非常繁琐,后续补例子

  2. 通过StringBuffer拼接脚本,自己通过GDBClient的submitAsync方法提交脚本和参数——注意参数是通过HashMap键来获取的;

    public String insert(DataDo dataDO) throws CoreSystemException 
        //Param Checker...
        
        StringBuilder stringBuffer = new StringBuilder();
        //填充实体Label
        stringBuffer.append(String.format("g.addV('%s')", dataDO.getLabel()));
        
        //填充实体属性,注意For循环内填充的是根据property为Key的参数预占符
        for (String property : dataDO.getPropertyCreationParamList()) 
            stringBuffer.append(String.format(".property('%s', %s)", property, property));
        
        stringBuffer.append(String.format(".property('propId', '%s')", id));
        stringBuffer.append(String.format(".property('creatorId', %d)", dataDO.getCreatorId()));
        stringBuffer.append(String.format(".property('area', %d)", dataDO.getArea()));
        
        //生成实体id
        String id = UUID.randomUUID().toString();
        //设置id
        stringBuffer.append(String.format(".property(id, '%s')", id));
        //提交结果,注意带一个HashMap<String, Object>参数作为填充参数。用于最终替换数值
        List<Result> results = gdbClient.queryResultSync(stringBuffer.toString(),
                                                         dataDO.getPropertyCreationParamMap());
        
        //结果处理
        dataDO.setId(id);
        if (results.size() != 1) 
            return null;
        
        return id;
    
  3. 通过GraphTraversal拼接出操作流,再通过Client编译成Bytecode进行提交。操作流拼接

    public GraphTraversal insertTraversal(DataDO dataDO, GraphTraversal traversal) 
      //Param Checker...
      
      //填充实体Label
      traversal = traversal.addV(knowledgePropNodeDO.getLabel());
      
      //填充实体属性,注意For循环内填充的是具体的值
      for (String property : knowledgePropNodeDO.getPropertyCreationParamList()) 
        traversal = traversal.property(property, knowledgePropNodeDO.getPropertyCreationParamMap().get(property));
      
      traversal = traversal.property("propId", id)
        .property("creatorId", knowledgePropNodeDO.getCreatorId())
        .property("area", knowledgePropNodeDO.getArea());
      
      //生成id
      String id = UUID.randomUUID().toString();
      knowledgePropNodeDO.setId(id);
      traversal = traversal.property(T.id, id);
      
      //Do event log append...
      
      //返回拼接的操作流,不执行真正的插入
      return traversal;
    

操作流程提交就用GdbClient.submit方法(不带超时,内部调用submitAsync方法直接get)或exec方法(带超时参数,内部调用submitAsync方法死循环查询完成情况)。

  注意

在2/3两种情况中,通过String拼接的脚本,支持within、without之类的入参为可变参数的Predicate操作符;而在Bytecode下,此类操作都会报必要参数缺失的异常:actual identifier, but expect expect : boolean int long float double string 如下图

TinkerPop(3.6.0版本或以上)支持自定义DSL,通过注解可以把Gremlin操作二次打包成领域内的语义操作,这时可以通过直接调用DO的一些方法来实现操作流编排,如:

加引用:

<dependency>
  <groupId>org.apache.tinkerpop</groupId>
  <artifactId>gremlin-annotations</artifactId>
  <version>3.6.0</version><!--这里的版本号最好和GdbClient的依赖对齐-->
</dependency>

加接口:

@GremlinDsl
public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> 
    public default GraphTraversal<S, Vertex> knows(String personName) 
        return out("knows").hasLabel("person").has("name", personName);
    
    
        public default <E2 extends Number> GraphTraversal<S, E2> youngestFriendsAge() 
        return out("knows").hasLabel("person").values("age").min();
    
    
        public default GraphTraversal<S, Long> createdAtLeast(int number) 
        return outE("created").count().is(P.gte(number));
    

引入接口并使用(注意,这里可以使用knows方法了)

SocialTraversalSource social = traversal(SocialTraversalSource.class).withEmbedded(graph);
social.V().has("name","marko").knows("josh");

Service层封

在这里通过事务/非事务的业务操作把一次需要处理的DAO串起来。

在TinkerPop框架中,对client的一次no Session提交,就相当于一次事务处理,在计算中发生错误则整条语句都不生效。

而通过g.tx()方法,可以获取一个Transaction进行多条语句执行的手工管理,通过transaction的begin、commit、close、rollback操作,实现事务的开启、提交、关闭和回滚。

因此,在Service层就按照是否需要事务分为NoSession操作和Session操作。

一般情况下增改删操作以Session操作归类(因为需要根据上一步查询的条件判断操作是否成功,来确定后续是否需要执行),特别是插入操作,需要插入点以及对应的关系边,而在点插入成功以前,并不能确知点的ID,从而不能直接连续操作完成插边——只能通过两段操作来完成。

对于No Session的操作这里不进行赘述,只需要按完整的Script或Bytecode进行提交,获取执行结果即可。

  TinkerPop官方食用方法

根据官方实例,在数据库支持事务的情况下,我们可以使用g.tx()获取到一个Transaction对象。然后通过Transaction对象的begin方法得到一个新的GraphTraversalSource对象进行连贯操作。可以多次使用这个对象进行CRUD,并在其中获取返回结果进行逻辑判断。并在最后通过commit或者rollback选择确认或回滚操作。

如下例子:

GraphTraversalSource g = traversal().withRemote(conn);


Transaction tx = g.tx();
// spawn a GraphTraversalSource from the Transaction. Traversals spawned
// from gtx will be essentially be bound to tx
//开启事务
GraphTraversalSource gtx = tx.begin();
try 
    //添加点1
    List<Vertex> personResult = gtx.addV("person").property("name", "John").toList();
    Assert.isTrue(!CollectionUtils.isEmpty(personResult), "Failed in create PersonNode");
    //添加点2
    List<Vertex> softwareResult = gtx.addV("software").property("name", "YYDS").toList();
    Assert.isTrue(!CollectionUtils.isEmpty(personResult), "Failed in create SoftwareNode");
    //添加点1到点2的关系
    List<Edge> edgeCreateResult = gtx.addE("create")
      .from(gtx.V(personResult.get(0).id())).to(gtx.V(softwareResult.get(0).id())).toList();
    //提交结果
    tx.commit();
    log.info("Success create Record [ -create-> ]", "John", "YYDS");
    return edgeCreateResult.get(0);
 catch (Exception ex) 
    //回滚事务
    tx.rollback();
    throw ex;
 finally 
    tx.close();

在这个例子中,完整创建一个记录”John create YYDS“,需要对数据库执行两点一边的插入操作,最后的边关系依赖两个点的id,所以前步操作成功后才可执行后步,通过tx生成的gtx实现了一次提交,失败可回滚。

  GDB实际用法

但在GDB中,直接按照TinkerPop的官方例子会出现Invalid OpProcessor异常。原因在于GDB不支持g.tx()的API操作[见Gremlin(地址:https://help.aliyun.com/document_detail/102883.html)兼容性,其实提交Script脚本串是可以开启事务的,在SDK的GdbClient中也是这样来实现open、commit、rollback方法的],因此无法直接获取到Transaction对象。

事实上,官方框架中,Transaction对象包裹了一个SessionedClient对象。在这个Client进行操作提交时会加上SessionId以及额外的SessionSettings参数来实现同一事务的操作确认/回滚。

而在GDB的SDK中,实现了一个特别的GdbClient类,用于把Bytecode的事务等提交逻辑改换成部分Script的方式来提交。因此,在GDB中,既可以通过提交”g.tx().open()“这样的script来实现官网ScriptDemo一致的事务开关;也可以用GdbClient自己提供的方法提交事务Bytecode。

这里只举提交Bytecode的方法(Script模式操作逻辑与官方例子一致,只是把每次的操作流程改成用client.submit提交的String就可以)。

不支持g.tx()的API操作(见Gremlin兼容性,其实提交Script脚本串是可以开启事务的,在SDK的GdbClient中也是这样来实现open、commit、rollback方法的),因此无法直接获取到Transaction对象。

事实上,官方框架中,Transaction对象包裹了一个SessionedClient对象。在这个Client进行操作提交时会加上SessionId以及额外的SessionSettings参数来实现同一事务的操作确认/回滚。


而在GDB的SDK中,实现了一个特别的GdbClient类,用于把Bytecode的事务等提交逻辑改换成部分Script的方式来提交。因此,在GDB中,既可以通过提交”g.tx().open()“这样的script来实现官网ScriptDemo一致的事务开关;也可以用GdbClient自己提供的方法提交事务Bytecode。


这里只举提交Bytecode的方法(Script模式操作逻辑与官方例子一致,只是把每次的操作流程改成用client.submit提交的String就可以)。

//创建Cluster连接池,获取SessionedClient
GdbClient.SessionedClient client = GdbCluster.build(gdbAHost).port(gdbAPort).credentials(gdbAUsername, gdbAPassword)
        .serializer(Serializers.GRAPHBINARY_V1D0)
        .create().connect(UUID.randomUUID().toString()).init();
//用于存放执行结果
StringBuffer result = new StringBuffer();
try 
    //重点:调用batchTransaction方法实现事务提交
    client.batchTransaction((tx, gtx) -> 
        try 
            //重点:调用tx(其实是GdbClient)的exec方法提交Bytecode操作流,并获取执行结果
            //不能直接使用gtx的toList方法(会出现Invalid OpProcessor异常)
            List<Result> personResult = tx.exec(
                    gtx.addV("person").property("name", "John")
                    , GQL_TIMEOUNT
            );
            Assert.isTrue(!CollectionUtils.isEmpty(personResult), "Failed in create PersonNode");
            List<Result> softwareResult = tx.exec(
                    gtx.addV("software").property("name", "YYDS")
                    , GQL_TIMEOUNT
            );
            Assert.isTrue(!CollectionUtils.isEmpty(personResult), "Failed in create SoftwareNode");
            //重点:返回的结果与直接执行bytecode不同
            Vertex person = personResult.get(0).getVertex();
            Vertex software = softwareResult.get(0).getVertex();
            List<Result> edgeCreateResult = tx.exec(
                    gtx.addE("create").from(gtx.V(person.id())).to(gtx.V(software.id()))
                    , GQL_TIMEOUNT
            );
            log.info("Success create Record [ -create-> ]", "John", "YYDS");
            result.append(edgeCreateResult.get(0).getEdge().id());
         catch (Exception ex) 
            //log it
            throw ex;
        
    );
finally 
    //重点:一定要手工关闭client
    client.close();

return result.toString();

在该示例中的所有 重点 标记,都需要格外注意,因为这些写法并不能被TinkerPop的标准写法替代。

小结

本篇记录了GDB进行事务操作和常用的TinkerPop框架的差异性,并收集了有效的GDB帮助文档和相关信息。有机会再跟大家探讨下图数据库做图谱检索的常见用法和优化。

团队介绍

我们是大淘宝技术部新品平台技术团队, 依托于淘宝大数据正在建立一套完整的涵盖消费者洞察、宏观及细分市场分析、竞争分析、市场策略研究、产品创新机制等的新品研发和创新孵化平台, 为品牌、商家及行业提供规模化的新品孵化和运营能力, 沉淀新品孵化机制和运营策略, 建立起一套基于大数据驱动的从市场研究、新品研发到新品投放营销的全链路新品运营平台。

✿  拓展阅读

作者|猫貂(荆羽晨)

编辑|橙子君

以上是关于使用TinkerPop框架对GDB增删改查的主要内容,如果未能解决你的问题,请参考以下文章

guns框架实现增删改查体会

Go使用Gin+Redis实现增删改查

MyBatis框架入门小案例(关于用mybatis框架对数据库的增删改查)

用java的框架做一个小项目实现对一张表的增删改查功能可以在页面操作

Go使用Gin+mysql实现增删改查

一个php文件怎么写上增删改查 功能?