专栏首页曲水流觞TechRill[GraphDB普及系列]了解Multi-Model API

[GraphDB普及系列]了解Multi-Model API

简介

OrientDB诞生之初是文档数据库,其中包含的无索引链接设计让它完美地具备了图数据库的能力,但彼时其核心API依然是Document API,随后,基于Apache TinkerPop 2.x 实现的Graph API,作为一个单独的组件加入其中。这种割裂的API设计,显然不符合OrientDB多模型数据库的定位。因此,v3.0版本之后,Multi-Model API作为新的核心出现在整个API体系中。

文目的就是带领大家体验新的API。仅仅通过其java doc来学习难免枯燥,因此借由一个简单的业务场景,构建一个web应用,让应用开发员能更切实地了解Multi-Model API,了解OrientDB。

业务场景

借鉴微博的一小部分功能,来构建一个简单的web应用,模型设计如下:

profile
 EDGE=HasFollowed  <------->  VERTEX=Account  <>-------->  VERTEX=Profile
   created_time                 id                           name
                                nickname                     phoneNum
                                created_time                 gender
                                                             address

Account表示账号,其中除了账号的基本属性外,通过profile属性关联到Profile类,该类中包含用户的基本信息,而HasFollowed作为边来表述Account之间的 “关注“ 关系。

应用内要实现的功能包括:

  • 创建上述模型中的CLASS(可以理解为sql中的建表)
  • 新增账号
  • 修改昵称
  • 关注
  • 查询我的关注
  • 取关
> 以上所有设计都只是作为掌握Multi-model API的辅助,不推荐直接应用在实际开发过程

工程说明

整个工程采用Spring Boot + Sring MVC Framework搭建,使用Maven作为构建工具,jdk8,以上工具在这里不再过多介绍,有兴趣的童鞋可以到文末查看项目的github地址,自行取阅。

Maven依赖

类比于常见的web应用,业务应用通过orientdb-client组件,以remote连接方式访问远端的OrientDB数据库实例,maven依赖如下(笔者使用3.0.7版本):

<dependency>
   <groupId>com.orientechnologies</groupId>
   <artifactId>orientdb-client</artifactId>
   <version>${orientdb.version}</version>
</dependency>

TinkerPop 2的API需要依赖 orientdb-graphdb模块,而想使用TinkerPop 3 API的话,需要依赖 orientdb-gremlin模块。

管理数据库连接

@Configuration
public class OrientDbConfig {

    @Bean
    public ODatabasePool oDatabasePool() {
        Map<String, Object> params = new HashMap();
        params.put(DB_POOL_MIN.getKey(), 10);
        params.put(DB_POOL_MAX.getKey(), 100);
        params.put(DB_POOL_ACQUIRE_TIMEOUT.getKey(), 30000);
        params.put(DB_POOL_IDLE_TIMEOUT.getKey(), 0);
        params.put(DB_POOL_IDLE_CHECK_DELAY.getKey(), 0);
        OrientDBConfig config =  OrientDBConfig.builder().fromMap(params).build();
        return new ODatabasePool(orientDB(), "demodb", ADMIN, ADMIN, config);
    }

    @Bean
    public OrientDB orientDB() {
        return new OrientDB("remote:yourhost", ROOT, ROOT, 
                            OrientDBConfig.defaultConfig());
    }
}

一切数据库操作,都依赖OrientDB实例( orientdb-client组件使用 Binary Protocol 协议,通过TCP/IP socket进行业务应用与数据库实例间的交互),同时其连接池实例为ODatabasePool。将orientDB和oDatabasePool都声明成spring的bean,方便依赖注入以及其生命周期的管理(OrientDB、ODatabasePool均重写了close方法,Spring会在应用退出时释放其占用的资源,实现优雅停机,kill -9除外)。

创建相关的CLASS

首先来创建相关的CLASS,通常这类操作应该交由DBA角色来实现,这里只是为了介绍API的功能。

@Autowired
ODatabasePool pool;
try (ODatabaseSession session = pool.acquire()) {
    OSchema oSchema = session.getMetadata().getSchema();
    //如果CLASS不存在,创建
    if (!oSchema.existsClass(CLASS_PROFILE) && !oSchema.existsClass(CLASS_ACCOUNT)) {
        OClass oProfile = session.createClass(CLASS_PROFILE, CLASS_V);
        oProfile.createProperty(PROFILE_NAME, OType.STRING);
        oProfile.createProperty(PROFILE_ADDRESS, OType.STRING);
        oProfile.createProperty(PROFILE_GENDER, OType.SHORT);
        oProfile.createProperty(PROFILE_PHONENUM, OType.STRING);
        oProfile.createIndex(IDX_PROFILE_PHONENUM, OClass.INDEX_TYPE.UNIQUE, PROFILE_PHONENUM);
        oProfile.setStrictMode(true);

        OClass oAccount = session.createClass(CLASS_ACCOUNT, CLASS_V);
        oAccount.createProperty(COMMON_ID, OType.STRING);
        oAccount.createProperty(ACCOUNT_NICKNAME, OType.STRING);
        oAccount.createProperty(COMMON_CREATEDTIME, OType.DATETIME);
        oAccount.createProperty(ACCOUNT_PROFILE, OType.LINK, oProfile);
        oAccount.createIndex(IDX_ACCOUNT_ID, OClass.INDEX_TYPE.UNIQUE_HASH_INDEX, COMMON_ID);
        oAccount.createIndex(IDX_ACCOUNT_NICKNAME, OClass.INDEX_TYPE.UNIQUE_HASH_INDEX, ACCOUNT_NICKNAME);

        session.commit();
    }
}
  1. 注入连接池,并通过acquire()方法获取连接对象:ODatabaseSession,通过try-with-resources来保证操作结束后其资源得到释放,同时注意ODatabaseSession 是非线程安全的
  2. getMetadata()可以获取数据库的元数据信息,包括Schemas、索引、调度器、函数库、安全信息等。代码中使用其判断对应的CLASS是否已经存在,避免重复创建的异常。
  3. createClass()用来创建CLASS,OrientDB支持继承,这里通过指定父类 V,将Profile和Account都创建成Vertex(同理, E是Edge的父类),这也体现了Mutil-Model的理念,一种API可以同时实现文档和图的操作。createVertexClass()和createEdgeClass()可以实现同样的功能。
  4. createProperty()用来创建CLASS所包含的属性,除基本属性外,也支持引用。代码中通过指定profile为OType.LINK类型,而建立了Account到Profile的1:1引用关系(这里设计成引用只是为了更多的展示API的功能,使用Edge来建立关系也是合理的)。
  5. OrientDB支持多种Schema模式,这个setStrictMode(true)指定使用Schema-Full模式,后续插入过程中不能再新增属性,这虽然牺牲了一些灵活性,但是提高了性能并节省了磁盘空间,结合业务场景酌情选择适合的模式。
  6. createIndex()用来创建索引,OrientDB中包含SB-Tree,Hash,Lucene等多种索引。代码中为phoneNum创建了默认的唯一索引(SB-Tree),因为考虑到手机号码可能需要范围查询(如like 186%),而为Account ID(本文采用UUID)添加UNIQUE_HASH_INDEX,因为其基本不可能范围查询,这样提高检索性能,并节省空间。
  7. 最后commit(),将变更提交到数据库实例。

新增账号

本应用通过json格式的报文进行前后端交互,业务层收到的参数均为json格式。

//报文样例:新增账号
{"nickName":"hello_orientdb","profile":{"name":"张三","address":"上海","gender":1,"phoneNum":"18600000000"}}

图相关CLASS实现

try (ODatabaseSession session = pool.acquire()) {
    session.begin();
    vertex = newVertex(session, clazz, params);
    session.commit();
}
/*
* 通过递归调用,处理像Account中关联Profile的情况
*/
private OVertex newVertex(ODatabaseSession session, String clazz, JSONObject params) {
    OVertex vertex = session.newVertex(clazz);
    for (Map.Entry<String, Object> e: params.entrySet()) {
        if (e.getValue() instanceof JSONObject) {
            OVertex inner = newVertex(session, e.getKey(), (JSONObject) e.getValue());
            vertex.setProperty(e.getKey(), inner);
        } else {
            vertex.setProperty(e.getKey(), e.getValue());
        } // todo 只处理了1:1的关系,1:n或n:n的情况请自行完善
    }
    session.save(vertex);
    return vertex;
}

以上代码通过API中与图相关的CLASS来实现逻辑:newVertex()用来新增一条顶点的记录,OVertex代表顶点(OEdge代表边),其中setProperty()用来设置属性。这里利用递归调用,将嵌套的json报文转化为Account和Profile的引用关系(方法仅供参考)。

SQL实现

StringBuilder sql = new StringBuilder("BEGIN;\n");
try (ODatabaseSession session = pool.acquire()) {
    newVertexSql(sql, clazz, params);
    sql.append("COMMIT;\n").append("return $").append(clazz);
    try (OResultSet rs = session.execute("sql", sql.toString())) {
        rs.stream().findFirst().map(OResult::toJSON).orElse(null);
     }
}
/**
* 通过递归生成batch脚本,处理像Account中关联Profile的情况
* 最终执行的语句样例:
* <pre>
* BEGIN;
* LET profile = CREATE VERTEX profile CONTENT {"address":"上海","gender":1,"name":"张三","phoneNum":"18622222222"};
* LET Account = CREATE VERTEX Account CONTENT {"created_time":"2018-10-25 12:14:08","nickName":"hello_orientdb","id":"aa5b2e47-f863-4afe-b38c-9fbf87d8bedd","profile":$profile.@rid};
* COMMIT;
* return $Account
* </pre>
*/
public StringBuilder newVertexSql(StringBuilder sql, String clazz, JSONObject params) {
        Map<String, Object> temp = new HashMap<>();
        StringBuilder inner = new StringBuilder();
        for (Map.Entry<String, Object> e: params.entrySet()) {
            if (e.getValue() instanceof JSONObject) {
                newVertexSql(sql, e.getKey(), (JSONObject) e.getValue());
                inner.append("\"").append(e.getKey()).append("\":")
                    .append("$").append(e.getKey()).append(".@rid").append("}");
            } else {
                temp.put(e.getKey(), e.getValue());
            }
            // todo 目前只处理了1:1的关系,1:n或n:n的情况请自行完善
        }
        Map<String, String> args = new HashMap<>();
        args.put(KEYWORD_CLASS, clazz);
        String content = JSON.toJSONString(temp);
        args.put(KEYWORD_CONTENT, inner.length() == 0 
                        ? content 
                        : content.substring(0, content.length() - 1) + "," + inner.toString());
        sql.append("LET ").append(clazz).append(" = ")
            .append(parseSql(CREATE_VERTEX_USE_CONTENT, args)).append("\n");
        return sql;
    }

以上代码展示了另一种方式,除了使用OVertex和OEdge这些CLASS来进行图操作,Multi-Model API也支持执行OrientDB的sql语句:

  • query(),执行幂等操作(SELECT, MATCH, TRAVERSE...)
  • command(),执行所有操作,幂等(SELECT, MATCH...),非幂等 (INSERT, UPDATE, DELETE...) and DDL (CREATE CLASS, CREATE PROPERTY...)
  • execute(),执行脚本(默认为SQL脚本)

这里采用execute()执行Batch脚本的方式,与command()相比,这种方式的好处是减少客户端与数据库实例的交互次数,最终执行的Batch脚本样例见方法说明。

修改昵称

//报文样例:修改昵称
{"id":"6d5f1625-e171-4ab7-be22-8fd1036e41fd","@rid":"#100:0","nickName":"hi_orientdb"}

图相关CLASS实现

OVertex vertex;
try (ODatabaseSession session = pool.acquire()) {
    session.begin();
    vertex = session.load(new ORecordId(rid));
    for (Map.Entry<String, Object> e: properties.entrySet()) {
        vertex.setProperty(e.getKey(), e.getValue());
    }
    vertex.save();
    session.commit();
}

在已知记录的@rid情况下,可以直接通过load()方式加载记录,之后通过修改相应的属性实现update的目的。

SQL实现

Map<String, Object> properties = new HashMap<>();
properties.put(COMMON_ID, jo.getString(COMMON_ID));
properties.put(ACCOUNT_NICKNAME, jo.getString(ACCOUNT_NICKNAME));
String sql = "UPDATE Account SET nickname = :nickname WHERE id = :id;";
try (ODatabaseSession session = pool.acquire()) {
    try (OResultSet rs = session.command(sql, properties)) {        
        return rs.stream().findFirst().map(OResult::toJSON).orElse(null);
    }
}
  1. 使用update语句执行修改逻辑,这样查询的条件就不仅限于@rid。
  2. 尽量使用参数化的查询语句,不要每次通过字符串连接而生成语句。每次接收到sql语句后,OrientDB会parse语句,生成AST,并缓存,如果使用字符串连接的形式每次都无法命中缓存,而需要重新parse(虽然parse过程不是非常消耗资源的动作,但是零消耗总好过低消耗)。
  3. OrientDB支持java Stream API,使用更方便。

关注

//报文样例:关注
{"HasFollowed":{"id":"cdcc93e8-20e3-4245-92ae-eed49c464994","@rid":"#261:0"},"id":"471c3b4f-0f8c-41a6-b5eb-594dd7ccf5f8","@rid":"#260:0"}

图相关CLASS实现

try (ODatabaseSession session = pool.acquire()) {
    session.begin();
    OEdge edge = session.newEdge(
            session.load(new ORecordId(fromRid)), 
            session.load(new ORecordId(toRid)), 
            CLASS_HASFOLLOWED);
    session.save(edge);
    session.commit();
}

关注相当于在两个Account间插入一条HasFollowed边,newEdge()方法可以实现这个动作,需要注意参数的顺序,OrientDB的边是有方向的。

SQL实现

Map<String, Object> properties = new HashMap<>();
properties.put("fromId", fromId);
properties.put("toId", toId);
String sql = "CREATE EDGE HasFollowed FROM " +
            "(SELECT FROM Account WHERE id = :fromId) TO " +
            "(SELECT FROM Account WHERE id = :toId);";
try (ODatabaseSession session = pool.acquire()) {
    try (OResultSet rs = session.command(sql, properties)) {
        //your code
    }
}
  1. 通过CREATE EDGE语句可以实现同样的逻辑,同时也要注意两个Vertex的方向。
  2. OResultSet对象也需要close。

我的关注

//报文样例:我的关注
{"@rid":"#260:0", "id":"471c3b4f-0f8c-41a6-b5eb-594dd7ccf5f8"}

图相关CLASS创建

try (ODatabaseSession session = pool.acquire()) {
    OVertex resultSet = session.load(new ORecordId(jo.getString(COMMON_RID)));
    resultSet.toJSON("fetchPlan:profile:-1 out_HasFollowed.in:1");
}

默认情况下,在remote连接模式下,query或者load都是延迟加载模式,client为了获取连接的记录需要发送多个网络请求来从服务端加载数据,在一些场景下,这非常消耗资源。通过Fetch Plan可以避免这种情况。代码中在toJSON方法中指定了fetchPlan,加载Account中的profile和其关注的Account记录。

SQL创建

try (ODatabaseSession session = pool.acquire()) {
    String sql = "match \n" +
            "  {class:Account, as:self, where:(id = :id)}\n" +
            "  .outE('HasFollowed'){as:hasFollowed}\n" +
            "  .inV(){as:follow}\n" +
            "return \n" +
            "  follow.@rid as rid, follow.nickName as nickName, follow.id as id, " +
            "  follow.profile.name as name, follow.profile.phoneNum as phoneNum, " +
            "  follow.profile.gender as gender,  follow.profile.address as address, " +
            "  hasFollowed.@rid as hasFollowRid;";
    Map<String, Object> properties = new HashMap<>();
    properties.put(COMMON_ID, jo.getString(COMMON_ID));
    List<String> results = resultSet.stream().map(r -> r.toJSON()).collect(Collectors.toList());
}

对于复杂的图遍历场景,Match是利器。上述代码展示了加载我的关注的Match语句,其中return除了返回Account相关字段,还返回了边的@rid,方便后续取关逻辑的实现。

取消关注

//报文样例:取消关注
{"id":"6d5f1625-e171-4ab7-be22-8fd1036e41fd","@rid":"#100:0,"hasFollowed":"6d5f1625-e171-4ab7-be22-111111111111"}

图相关CLASS实现

try (ODatabaseSession session = pool.acquire()) {
    session.delete(new ORecordId(rid));
}
  1. delete()方法可以直接通过@rid删除相应的记录。
  2. 在OrientDB中大部分场景下,图的完整性是由数据库实例维护的。上述代码仅仅删除了边的记录,但是数据库引擎会同时把边两端的顶点中相关的link记录清除掉(即删除边的同时,顶点也会被更新)。

SQL实现

try (ODatabaseSession session = pool.acquire()) {
    Map<String, Object> properties = new HashMap<>();
    properties.put("fromId", fromId);
    properties.put("toId", toId);
    String sql = "DELETE EDGE HasFollowed FROM " +
                "(SELECT FROM Account WHERE id = :fromId) TO " +
                "(SELECT FROM Account WHERE id = :toId);";
    session.command(sql, properties);
}

与创建边类似,通过DELETE EDGE语句可以实现同样的操作。

总结

本文旨在通过一个简单的web业务场景,带领大家了解OrientDB新的Multi-Model API的一些基础功能。较之前的版本,新的API更能体现“多模型”的产品定位,在提供丰富的图操作同时,兼顾的文档的特性,使用更平滑、更方便。

后续文章会带来更丰富、实用的相关实践经验,欢迎持续关注。

源码地址: https://github.com/xiang-me/OrientDB-tutorials

本文分享自微信公众号 - 曲水流觞TechRill(geniusiandev)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ElasticSearch 查询的秘密

    https://neway6655.github.io/elasticsearch/2015/09/11/elasticsearch-study-notes.h...

    JAVA葵花宝典
  • 超全面的 MySQL优化 面试解析

    price decimal(8,2)有2位小数的定点数,定点数支持很大的数(甚至是超过int,bigint存储范围的数)

    用户1516716
  • 2018年全球最受欢迎的30款数据可视化工具

    RAWGraphs是一个在线的开源工具和数据可视化框架,用来处理Excel表中的数据。你只需将数据导入到RAWGraphs中,设计你想要的图表,然后将其导出为S...

    iCDO互联网数据官
  • 硬货来了!轻松掌握 MongDB 流式聚合操作

    信息科学中的聚合是指对相关数据进行内容筛选、处理和归类并输出结果的过程。MongoDB 中的聚合是指同时对多个文档中的数据进行处理、筛选和归类并输出结果的过程。...

    崔庆才
  • 日均 5 亿查询量的京东订单中心,为什么舍 MySQL 用 ES ?

    京东到家订单中心系统业务中,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调用量都非常大,造成了订单数据读多写少的情况。

    芋道源码
  • 与我一起学习微服务架构设计模式1—逃离单体地狱

    软件架构对功能性需求影响并不大,它影响非功能性需求,即质量属性或者其他能力,如交付速度的可维护性、可扩展性和可测试性。

    java达人
  • 除了不要 SELECT * ,程序员使用数据库还应知道的11个技巧!

    应用程序慢如牛,原因多多,可能是网络的原因、可能是系统架构的原因,还有可能是数据库的原因。

    帅地
  • 建议收藏 | 专业的MySQL开发规范

    命名规范的对象是指数据库SCHEMA、表TABLE、索引INDEX、约束CONSTRAINTS等的命名约定

    Rare0716
  • 解读“OB登顶TPCC”

    作为十一期间数据库圈的一条刷屏新闻,“中国自研数据库超越Oracle登顶全球第一”,确实很吸引眼球。近几天来,又不断有后续消息放出。有热捧的、有唱衰的、有不以为...

    用户5548425
  • 写给工程师的 MySQL 面试高频 100 问!

    本文主要受众为开发人员,所以不涉及到MySQL的服务部署等操作,且内容较多,大家准备好耐心和瓜子矿泉水.

    帅地

扫码关注云+社区

领取腾讯云代金券