暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

了解Multi-Model API

图数据库 2021-04-16
582


简介

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

  1.                                                profile

  2. EDGE=HasFollowed  <------->  VERTEX=Account  <>-------->  VERTEX=Profile

  3.   created_time                 id                           name

  4.                                nickname                     phoneNum

  5.                                created_time                 gender

  6.                                                             address

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

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

  • 创建上述模型中的CLASS(可以理解为sql中的建表)

  • 新增账号

  • 修改昵称

  • 关注

  • 查询我的关注

  • 取关

  1. > 以上所有设计都只是作为掌握Multi-model API的辅助,不推荐直接应用在实际开发过程

工程说明

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

Maven依赖

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

  1. <dependency>

  2.   <groupId>com.orientechnologies</groupId>

  3.   <artifactId>orientdb-client</artifactId>

  4.   <version>${orientdb.version}</version>

  5. </dependency>

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

管理数据库连接

  1. @Configuration

  2. public class OrientDbConfig {


  3.    @Bean

  4.    public ODatabasePool oDatabasePool() {

  5.        Map<String, Object> params = new HashMap();

  6.        params.put(DB_POOL_MIN.getKey(), 10);

  7.        params.put(DB_POOL_MAX.getKey(), 100);

  8.        params.put(DB_POOL_ACQUIRE_TIMEOUT.getKey(), 30000);

  9.        params.put(DB_POOL_IDLE_TIMEOUT.getKey(), 0);

  10.        params.put(DB_POOL_IDLE_CHECK_DELAY.getKey(), 0);

  11.        OrientDBConfig config =  OrientDBConfig.builder().fromMap(params).build();

  12.        return new ODatabasePool(orientDB(), "demodb", ADMIN, ADMIN, config);

  13.    }


  14.    @Bean

  15.    public OrientDB orientDB() {

  16.        return new OrientDB("remote:yourhost", ROOT, ROOT,

  17.                            OrientDBConfig.defaultConfig());

  18.    }

  19. }

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

创建相关的CLASS

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

  1. @Autowired

  2. ODatabasePool pool;

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    OSchema oSchema = session.getMetadata().getSchema();

  3.    //如果CLASS不存在,创建

  4.    if (!oSchema.existsClass(CLASS_PROFILE) && !oSchema.existsClass(CLASS_ACCOUNT)) {

  5.        OClass oProfile = session.createClass(CLASS_PROFILE, CLASS_V);

  6.        oProfile.createProperty(PROFILE_NAME, OType.STRING);

  7.        oProfile.createProperty(PROFILE_ADDRESS, OType.STRING);

  8.        oProfile.createProperty(PROFILE_GENDER, OType.SHORT);

  9.        oProfile.createProperty(PROFILE_PHONENUM, OType.STRING);

  10.        oProfile.createIndex(IDX_PROFILE_PHONENUM, OClass.INDEX_TYPE.UNIQUE, PROFILE_PHONENUM);

  11.        oProfile.setStrictMode(true);


  12.        OClass oAccount = session.createClass(CLASS_ACCOUNT, CLASS_V);

  13.        oAccount.createProperty(COMMON_ID, OType.STRING);

  14.        oAccount.createProperty(ACCOUNT_NICKNAME, OType.STRING);

  15.        oAccount.createProperty(COMMON_CREATEDTIME, OType.DATETIME);

  16.        oAccount.createProperty(ACCOUNT_PROFILE, OType.LINK, oProfile);

  17.        oAccount.createIndex(IDX_ACCOUNT_ID, OClass.INDEX_TYPE.UNIQUE_HASH_INDEX, COMMON_ID);

  18.        oAccount.createIndex(IDX_ACCOUNT_NICKNAME, OClass.INDEX_TYPE.UNIQUE_HASH_INDEX, ACCOUNT_NICKNAME);


  19.        session.commit();

  20.    }

  21. }

  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格式。

  1. //报文样例:新增账号

  2. {"nickName":"hello_orientdb","profile":{"name":"张三","address":"上海","gender":1,"phoneNum":"18600000000"}}

图相关CLASS实现

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    session.begin();

  3.    vertex = newVertex(session, clazz, params);

  4.    session.commit();

  5. }

  1. /*

  2. * 通过递归调用,处理像Account中关联Profile的情况

  3. */

  4. private OVertex newVertex(ODatabaseSession session, String clazz, JSONObject params) {

  5.    OVertex vertex = session.newVertex(clazz);

  6.    for (Map.Entry<String, Object> e: params.entrySet()) {

  7.        if (e.getValue() instanceof JSONObject) {

  8.            OVertex inner = newVertex(session, e.getKey(), (JSONObject) e.getValue());

  9.            vertex.setProperty(e.getKey(), inner);

  10.        } else {

  11.            vertex.setProperty(e.getKey(), e.getValue());

  12.        } // todo 只处理了1:1的关系,1:n或n:n的情况请自行完善

  13.    }

  14.    session.save(vertex);

  15.    return vertex;

  16. }

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

SQL实现


  1. StringBuilder sql = new StringBuilder("BEGIN;\n");

  2. try (ODatabaseSession session = pool.acquire()) {

  3.    newVertexSql(sql, clazz, params);

  4.    sql.append("COMMIT;\n").append("return $").append(clazz);

  5.    try (OResultSet rs = session.execute("sql", sql.toString())) {

  6.        rs.stream().findFirst().map
    (OResult::toJSON).orElse(null);

    }

  1. }

  1. /**

  2. * 通过递归生成batch脚本,处理像Account中关联Profile的情况

  3. * 最终执行的语句样例:

  4. * <pre>

  5. * BEGIN;

  6. * LET profile = CREATE VERTEX profile CONTENT {"address":"上海","gender":1,"name":"张三","phoneNum":"18622222222"};

  7. * 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};

  8. * COMMIT;

  9. * return $Account

  10. * </pre>

  11. */

  12. public StringBuilder newVertexSql(StringBuilder sql, String clazz, JSONObject params) {

  13.        Map<String, Object> temp = new HashMap<>();

  14.        StringBuilder inner = new StringBuilder();

  15.        for (Map.Entry<String, Object> e: params.entrySet()) {

  16.            if (e.getValue() instanceof JSONObject) {

  17.                newVertexSql(sql, e.getKey(), (JSONObject) e.getValue());

  18.                inner.append("\"").append(e.getKey()).append("\":")

  19.                    .append("$").append(e.getKey()).append(".@rid").append("}");

  20.            } else {

  21.                temp.put(e.getKey(), e.getValue());

  22.            }

  23.            // todo 目前只处理了1:1的关系,1:n或n:n的情况请自行完善

  24.        }

  25.        Map<String, String> args = new HashMap<>();

  26.        args.put(KEYWORD_CLASS, clazz);

  27.        String content = JSON.toJSONString(temp);

  28.        args.put(KEYWORD_CONTENT, inner.length() == 0

  29.                        ? content

  30.                        : content.substring(0, content.length() - 1) + "," + inner.toString());

  31.        sql.append("LET ").append(clazz).append(" = ")

  32.            .append(parseSql(CREATE_VERTEX_USE_CONTENT, args)).append("\n");

  33.        return sql;

  34.    }

以上代码展示了另一种方式,除了使用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脚本样例见方法说明。

修改昵称

  1. //报文样例:修改昵称

  2. {"id":"6d5f1625-e171-4ab7-be22-8fd1036e41fd","@rid":"#100:0","nickName":"hi_orientdb"}

图相关CLASS实现

  1. OVertex vertex;

  2. try (ODatabaseSession session = pool.acquire()) {

  3.    session.begin();

  4.    vertex = session.load(new ORecordId(rid));

  5.    for (Map.Entry<String, Object> e: properties.entrySet()) {

  6.        vertex.setProperty(e.getKey(), e.getValue());

  7.    }

  8.    vertex.save();

  9.    session.commit();

  10. }

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

SQL实现

  1. Map<String, Object> properties = new HashMap<>();

  2. properties.put(COMMON_ID, jo.getString(COMMON_ID));

  3. properties.put(ACCOUNT_NICKNAME, jo.getString(ACCOUNT_NICKNAME));

  4. String sql = "UPDATE Account SET nickname = :nickname WHERE id = :id;";

  5. try (ODatabaseSession session = pool.acquire()) {

       try (OResultSet rs = session.command(sql, properties)) {

  1.        return rs.stream().findFirst().map(OResult::toJSON).orElse(null);

  2.    }

  3. }

  1. 使用update语句执行修改逻辑,这样查询的条件就不仅限于@rid。

  2. 尽量使用参数化的查询语句,不要每次通过字符串连接而生成语句。每次接收到sql语句后,OrientDB会parse语句,生成AST,并缓存,如果使用字符串连接的形式每次都无法命中缓存,而需要重新parse(虽然parse过程不是非常消耗资源的动作,但是零消耗总好过低消耗)。

  3. OrientDB支持java Stream API,使用更方便。

关注

  1. //报文样例:关注

  2. {"HasFollowed":{"id":"cdcc93e8-20e3-4245-92ae-eed49c464994","@rid":"#261:0"},"id":"471c3b4f-0f8c-41a6-b5eb-594dd7ccf5f8","@rid":"#260:0"}

图相关CLASS实现

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    session.begin();

  3.    OEdge edge = session.newEdge(

  4.            session.load(new ORecordId(fromRid)),

  5.            session.load(new ORecordId(toRid)),

  6.            CLASS_HASFOLLOWED);

  7.    session.save(edge);

  8.    session.commit();

  9. }

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

SQL实现

  1. Map<String, Object> properties = new HashMap<>();

  2. properties.put("fromId", fromId);

  3. properties.put("toId", toId);

  4. String sql = "CREATE EDGE HasFollowed FROM " +

  5.            "(SELECT FROM Account WHERE id = :fromId) TO " +

  6.            "(SELECT FROM Account WHERE id = :toId);";

  7. try (ODatabaseSession session = pool.acquire()) {

  8.    try (OResultSet rs = session.command(sql, properties)) {

  9.        //your code

  10.    }

  11. }

  1. 通过CREATE EDGE语句可以实现同样的逻辑,同时也要注意两个Vertex的方向。

  2. OResultSet对象也需要close。

我的关注

  1. //报文样例:我的关注

  2. {"@rid":"#260:0", "id":"471c3b4f-0f8c-41a6-b5eb-594dd7ccf5f8"}

图相关CLASS创建

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    OVertex resultSet = session.load(new ORecordId(jo.getString(COMMON_RID)));

  3.    resultSet.toJSON("fetchPlan:profile:-1 out_HasFollowed.in:1");

  4. }

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

SQL创建

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    String sql = "match \n" +

  3.            "  {class:Account, as:self, where:(id = :id)}\n" +

  4.            "  .outE('HasFollowed'){as:hasFollowed}\n" +

  5.            "  .inV(){as:follow}\n" +

  6.            "return \n" +

  7.            "  follow.@rid as rid, follow.nickName as nickName, follow.id as id, " +

  8.            "  follow.profile.name as name, follow.profile.phoneNum as phoneNum, " +

  9.            "  follow.profile.gender as gender,  follow.profile.address as address, " +

  10.            "  hasFollowed.@rid as hasFollowRid;";

  11.    Map<String, Object> properties = new HashMap<>();

  12.    properties.put(COMMON_ID, jo.getString(COMMON_ID));

  13.    List<String> results = resultSet.stream().map(r -> r.toJSON()).collect(Collectors.toList());

  14. }

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

取消关注

  1. //报文样例:取消关注

  2. {"id":"6d5f1625-e171-4ab7-be22-8fd1036e41fd","@rid":"#100:0,"hasFollowed":"6d5f1625-e171-4ab7-be22-111111111111"}

图相关CLASS实现

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    session.delete(new ORecordId(rid));

  3. }

  1. delete()方法可以直接通过@rid删除相应的记录。

  2. 在OrientDB中大部分场景下,图的完整性是由数据库实例维护的。上述代码仅仅删除了边的记录,但是数据库引擎会同时把边两端的顶点中相关的link记录清除掉(即删除边的同时,顶点也会被更新)。

SQL实现

  1. try (ODatabaseSession session = pool.acquire()) {

  2.    Map<String, Object> properties = new HashMap<>();

  3.    properties.put("fromId", fromId);

  4.    properties.put("toId", toId);

  5.    String sql = "DELETE EDGE HasFollowed FROM " +

  6.                "(SELECT FROM Account WHERE id = :fromId) TO " +

  7.                "(SELECT FROM Account WHERE id = :toId);";

  8.    session.command(sql, properties);

  9. }

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

总结

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

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

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

温馨提示:如果在微信PC端浏览,请使用系统浏览器,效果最佳



文章转载自图数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论