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之间的 “关注“ 关系。
应用内要实现的功能包括:
> 以上所有设计都只是作为掌握Multi-model API的辅助,不推荐直接应用在实际开发过程
整个工程采用Spring Boot + Sring MVC Framework搭建,使用Maven作为构建工具,jdk8,以上工具在这里不再过多介绍,有兴趣的童鞋可以到文末查看项目的github地址,自行取阅。
类比于常见的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,通常这类操作应该交由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();
}
}
V
,将Profile和Account都创建成Vertex(同理, E
是Edge的父类),这也体现了Mutil-Model的理念,一种API可以同时实现文档和图的操作。createVertexClass()和createEdgeClass()可以实现同样的功能。本应用通过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语句:
这里采用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);
}
}
//报文样例:关注
{"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
}
}
//报文样例:我的关注
{"@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));
}
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 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!