前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[GraphDB普及系列]了解Multi-Model API

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

作者头像
曲水流觞
发布2019-10-27 21:15:15
8440
发布2019-10-27 21:15:15
举报
文章被收录于专栏:曲水流觞TechRill曲水流觞TechRill

简介

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应用,模型设计如下:

代码语言:javascript
复制
profile
 EDGE=HasFollowed  <------->  VERTEX=Account  <>-------->  VERTEX=Profile
   created_time                 id                           name
                                nickname                     phoneNum
                                created_time                 gender
                                                             address

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

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

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

工程说明

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

Maven依赖

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

代码语言:javascript
复制
<dependency>
   <groupId>com.orientechnologies</groupId>
   <artifactId>orientdb-client</artifactId>
   <version>${orientdb.version}</version>
</dependency>

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

管理数据库连接

代码语言:javascript
复制
@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的功能。

代码语言:javascript
复制
@Autowired
ODatabasePool pool;
代码语言:javascript
复制
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格式。

代码语言:javascript
复制
//报文样例:新增账号
{"nickName":"hello_orientdb","profile":{"name":"张三","address":"上海","gender":1,"phoneNum":"18600000000"}}

图相关CLASS实现

代码语言:javascript
复制
try (ODatabaseSession session = pool.acquire()) {
    session.begin();
    vertex = newVertex(session, clazz, params);
    session.commit();
}
代码语言:javascript
复制
/*
* 通过递归调用,处理像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实现

代码语言:javascript
复制
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);
     }
}
代码语言:javascript
复制
/**
* 通过递归生成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脚本样例见方法说明。

修改昵称

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

图相关CLASS实现

代码语言:javascript
复制
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实现

代码语言:javascript
复制
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,使用更方便。

关注

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

图相关CLASS实现

代码语言:javascript
复制
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实现

代码语言:javascript
复制
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。

我的关注

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

图相关CLASS创建

代码语言:javascript
复制
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创建

代码语言:javascript
复制
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,方便后续取关逻辑的实现。

取消关注

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

图相关CLASS实现

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

SQL实现

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 曲水流觞TechRill 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 业务场景
  • 工程说明
  • Maven依赖
  • 管理数据库连接
  • 创建相关的CLASS
  • 新增账号
  • 修改昵称
  • 关注
  • 我的关注
  • 取消关注
  • 总结
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档