vesoft-inc / nebula-java

Client API and data importer of Nebula Graph in Java
Apache License 2.0
173 stars 121 forks source link

An ORM framework for nebula-java #347

Closed Anyzm closed 1 year ago

Anyzm commented 3 years ago

使用示例:

@GraphEdge(value = "e_call", srcVertex = VertexEntity.class, dstVertex = VertexEntity.class)
public class ECallEntity  {
    @GraphProperty(value = "user_no1", required = true, propertyTypeEnum = GraphPropertyTypeEnum.GRAPH_EDGE_SRC_ID)
    private String userNo1;
    @GraphProperty(value = "user_no2", required = true, propertyTypeEnum = GraphPropertyTypeEnum.GRAPH_EDGE_DST_ID)
    private String userNo2;
    @GraphProperty(value = "call_out_cnt", dataType = GraphDataTypeEnum.INT)
    private int callOutCnt;
    @GraphProperty(value = "call_in_cnt", dataType = GraphDataTypeEnum.INT)
    private int callInCnt;
    @GraphProperty(value = "call_out_len", dataType = GraphDataTypeEnum.INT)
    private int callOutLen;
    @GraphProperty(value = "call_in_len", dataType = GraphDataTypeEnum.INT)
    private int callInLen;
    @GraphProperty(value = "create_time", dataType = GraphDataTypeEnum.TIMESTAMP, formatter = DateGraphValueFormatter.class)
    private Date createTime;

    @GraphProperty(value = "update_time", dataType = GraphDataTypeEnum.TIMESTAMP, formatter = DateGraphValueFormatter.class)
    private Date updateTime;
}

业务代码层引入框架中的基础Mapper,可以直接操作实体类, 比如保存边:public <S, T, E> int saveEdgeEntities(List entities) throws NebulaException; 比如查询顶点:public List fetchVertexTag(Class vertexClazz, String... vertexIds); 另外查询支持API: public QueryResult executeQuery(GraphQuery query) throws NebulaException;

等等 当然也自己集成了spring-boot-starter 目前已经用于生产,没有明显大问题 不知大家有什么好的建议呢?

Anyzm commented 3 years ago
@GraphVertex(value = "user", keyPolicy = GraphKeyPolicy.string_key)
public class VertexEntity  {
    @GraphProperty(value = "user_no", required = true, propertyTypeEnum = GraphPropertyTypeEnum.GRAPH_VERTEX_ID)
    private String userNo;
    @SensitiveField
    private transient String mobileNo;
    @GraphProperty(value = "mobile_no_encryptx")
    private String mobileNoEncryptx;
    @GraphProperty(value = "mobile_no_md5x")
    private String mobileNoMd5x;
    @GraphProperty(value = "birth_date")
    private String birthDate;
    @GraphProperty(value = "gender")
    private String gender;
    @GraphProperty(value = "marital")
    private String marital;
}
Anyzm commented 3 years ago

类似这样,用注解的方式标注Entity,用基础的Mapper执行查询或者更新操作,查询封装了API的方式操作,基础的Mapper也支持自己手写ngql

jamieliu1023 commented 3 years ago

@klay-ke @Nicole00

Anyzm commented 3 years ago

这个需求看到issues里面很多人提过,类似的:查询结果直接转换为java bean,nebula支持springboot等,都是此类需求。 总结就是:1、ORM框架,2、spring-boot-starter,目前这两者我这边粗略实现了。 风险点:1、框架集成的是2.0.1的nebula-java-client,不知道其余nebula的版本是否适用, 2、查询API没有穷尽,所以版本升级相应的查询API也需要长期维护升级,带来了新的维护成本

Anyzm commented 3 years ago
    public GraphQuery getQueryForEdgeCount(Class labelClazz, LocalDate backtraceDate, int steps, int layer, String... userNos) {
        EdgeQuery edgeQuery = NebulaEdgeQuery.build().goFromSteps(labelClazz, EdgeDirectionEnum.BIDIRECT, steps, userNos);
        NebulaUtils.addBacktraceDate(labelClazz, edgeQuery, backtraceDate, null);
        return edgeQuery.yield(labelClazz, "userNo1", "userNo2").limit(getLayerLimit(layer));
    }

封装查询API的好处:1、面向对象,更加直观立体 2、代码可读性高,动态可复用性强 等等

 EdgeQuery edgeQuery = NebulaEdgeQuery.build().goFromSteps(labelClazz, EdgeDirectionEnum.BIDIRECT, 1, steps, userNos);
        GraphCondition baseCondition = getBaseCondition(userNo, userNos);
        NebulaUtils.addBacktraceDate(labelClazz, edgeQuery, backtraceDate, baseCondition);
        return edgeQuery.yieldDistinct("$$.", VertexEntity.class, fieldArray()).limit(limitSize).pipe().yield().connectAdd(getYieldShortQuery());

目前查询API存在的问题:1、支持的查询语法不是很全面(够我们的业务用了) 2、仅支持fetch prop 和go,不支持索引之类的查询

sydowma commented 3 years ago

@Anyzm 发布把

Nicole00 commented 3 years ago

感谢@Anyzm 提供的ORM方案,我们目前也在working on it. 目前您这边是包括了插入和查询类的语法封装,请教两个问题:有关于数据更新、删除的封装么?对于返回的查询结果有对应的封装么? 我们可以基于您提供的方案一起打造一个更齐全的ORM。@klay-ke

Anyzm commented 3 years ago

感谢@Anyzm 提供的ORM方案,我们目前也在working on it. 目前您这边是包括了插入和查询类的语法封装,请教两个问题:有关于数据更新、删除的封装么?对于返回的查询结果有对应的封装么? 我们可以基于您提供的方案一起打造一个更齐全的ORM。@klay-ke

查询返回结果有封装:

@ToString
public class QueryResult implements Iterable<QueryResult.Row>, Cloneable, Serializable {

    @Getter
    private List<Row> data = Collections.emptyList();

    public QueryResult() {
    }

    public QueryResult(List<Row> data) {
        if (!CollectionUtils.isEmpty(data)) {
            this.data = data;
        }
    }

    @Override
    public QueryResult clone() {
        List<Row> newList = Lists.newArrayListWithExpectedSize(data.size());
        for (Row datum : data) {
            Row clone = datum.clone();
            newList.add(clone);
        }
        return new QueryResult(newList);
    }

    /**
     * 将查询结果合并
     *
     * @param queryResult
     * @return
     */
    public QueryResult mergeQueryResult(QueryResult queryResult) {
        if (queryResult == null || queryResult.isEmpty()) {
            return this;
        }
        if (this.isEmpty()) {
            this.data = queryResult.getData();
        } else {
            this.data.addAll(queryResult.getData());
        }
        return this;
    }

    public <T> List<T> getEntities(Class<T> clazz) {
        List<T> list = new ArrayList<>();
        for (Row row : this.data) {
            list.add(row.getEntity(clazz));
        }
        return list;
    }

    public int size() {
        return this.data.size();
    }

    public boolean isEmpty() {
        return this.size() == 0;
    }

    public boolean isNotEmpty() {
        return this.size() != 0;
    }

    @Override
    public Iterator<Row> iterator() {
        return this.data.iterator();
    }

    public Stream<Row> stream() {
        Iterable<Row> iterable = this::iterator;
        return StreamSupport.stream(iterable.spliterator(), false);
    }

    @Data
    public static class Row implements Iterable<Map.Entry<String, Object>>, Cloneable, Serializable {

        private Map<String, Object> detail = Collections.emptyMap();

        public Row() {
        }

        public Row(Map<String, Object> detail) {
            if (MapUtils.isNotEmpty(detail)) {
                this.detail = detail;
            }
        }

        @Override
        public Row clone() {
            Map<String, Object> newMap = Maps.newHashMapWithExpectedSize(detail.size());
            newMap.putAll(detail);
            return new Row(newMap);
        }

        public int size() {
            return this.detail.size();
        }

        public Row setProp(String key, Object value) {
            this.detail.put(key, value);
            return this;
        }

        public Map<String, Object> getRowData() {
            return this.detail;
        }

        public Object get(String key) {
            return MapUtils.isEmpty(this.detail) ? null : this.detail.get(key);
        }

        public Date getDate(String key) {
            Long value = this.getLong(key);
            return new Timestamp(value * 1000);
        }

        public String getString(String columnLabel) {
            return getString(columnLabel, null);
        }

        public String getString(String columnLabel, String defaultValue) {
            return (String) this.detail.getOrDefault(columnLabel, defaultValue);
        }

        public Boolean getBoolean(String columnLabel) {
            return getBoolean(columnLabel, null);
        }

        public Boolean getBoolean(String columnLabel, Boolean defaultValue) {
            return (boolean) this.detail.getOrDefault(columnLabel, defaultValue);
        }

        public Short getShort(String columnLabel) {
            return getShort(columnLabel, null);
        }

        public Short getShort(String columnLabel, Short defaultValue) {
            return (short) this.detail.getOrDefault(columnLabel, defaultValue);
        }

        public Integer getInt(String columnLabel) {
            return getInt(columnLabel, null);
        }

        public Integer getInt(String columnLabel, Integer defaultValue) {
            Object obj = this.detail.get(columnLabel);
            if (obj instanceof Long) {
                long l = (Long) obj;
                if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) {
                    throw new IllegalArgumentException("Bad value for type int: " + l);
                }
                return (int) l;
            } else {
                return (int) this.detail.getOrDefault(columnLabel, defaultValue);
            }
        }

        public Long getLong(String columnLabel) {
            return getLong(columnLabel, null);
        }

        public Long getLong(String columnLabel, Long defaultValue) {
            return (long) this.detail.getOrDefault(columnLabel, defaultValue);
        }

        public Float getFloat(String columnLabel) {
            return getFloat(columnLabel, null);
        }

        public Float getFloat(String columnLabel, Float defaultValue) {
            return (float) this.detail.getOrDefault(columnLabel, defaultValue);
        }

        public Double getDouble(String columnLabel) {
            return getDouble(columnLabel, null);
        }

        public Double getDouble(String columnLabel, Double defaultValue) {
            Object value = this.detail.get(columnLabel);
            if (value instanceof Long) {
                long lValue = (Long) value;
                return (double) lValue;
            } else if (value instanceof Integer) {
                Integer lValue = (Integer) value;
                return (double) lValue;
            } else {
                return (double) this.detail.getOrDefault(columnLabel, 0.0);
            }
        }

        private static <T> T copyMapToBean(Map<String, ?> map, Class<T> clazz) {
            String json = JSONObject.toJSONString(map);
            return JSONObject.parseObject(json, clazz);
        }

        public <T> T getEntity(Class<T> clazz) {
            return copyMapToBean(this.detail, clazz);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || this.getClass() != o.getClass()) {
                return false;
            }
            Row row = (Row) o;
            return Objects.equals(this.detail, row.detail);
        }

        @Override
        public int hashCode() {
            return Objects.hash(this.detail);
        }

        @Override
        public Iterator<Map.Entry<String, Object>> iterator() {
            return this.detail.entrySet().iterator();
        }
    }
}
Anyzm commented 3 years ago

更新删除也有简单封装:

    @Override
    public int execute(String statement) throws NebulaExecuteException {
        ResultSet resultSet = null;
        try {
            log.debug("execute执行nebula,ngql={}", statement);
            resultSet = this.session.execute(statement);
        } catch (Exception e) {
            log.error("更新nebula异常 Thrift rpc call failed: {}", e.getMessage());
            throw new NebulaExecuteException(ErrorCode.E_RPC_FAILURE, e.getMessage(), e);
        }
        if (resultSet.getErrorCode() == ErrorCode.SUCCEEDED) {
            return ErrorCode.SUCCEEDED;
        }
        if (resultSet.getErrorCode() == ErrorCode.E_EXECUTION_ERROR
                && resultSet.getErrorMessage().contains(E_DATA_CONFLICT_ERROR)) {
            //版本冲突,session内部不再打印错误日志,直接抛出自定义的版本异常
            throw new NebulaVersionConflictException(resultSet.getErrorCode(), resultSet.getErrorMessage());
        }
        log.error("更新nebula异常 code:{}, msg:{}, nGql:{} ",
                resultSet.getErrorCode(), resultSet.getErrorMessage(), statement);
        throw new NebulaExecuteException(resultSet.getErrorCode(), resultSet.getErrorMessage());
    }
klay-ke commented 3 years ago

谢谢提供的方案,想问一下你这里是主要基于反射实现的吗?我们后续可能也会看重一下这个方案对于其他客户端的适用性,你是否觉得对于python,go客户端你的大体方案也同样适用?有时间可以一起沟通聊一聊

wey-gu commented 3 years ago

谢谢提供的方案,想问一下你这里是主要基于反射实现的吗?我们后续可能也会看重一下这个方案对于其他客户端的适用性,你是否觉得对于python,go客户端你的大体方案也同样适用?有时间可以一起沟通聊一聊

Go 的话社区也还没来得及做 ORM,不过 zhihu 做了一个 --> https://github.com/zhihu/norm Python 的话社区也还没来得及做哈

Anyzm commented 3 years ago

谢谢提供的方案,想问一下你这里是主要基于反射实现的吗?我们后续可能也会看重一下这个方案对于其他客户端的适用性,你是否觉得对于python,go客户端你的大体方案也同样适用?有时间可以一起沟通聊一聊

其实我对于python和go的了解程度很浅,据我个人的认知,我觉得go也可以按照类似的思路实现,因为go本身和java很类似,都是强类型语言,go的出现本就被预言是用来替代java的。至于python,我觉得可能不是特别适合,因为Python是弱类型语言,更像脚本,所以python可能需要另外的思路实现,可能更有利于用户方便使用。

Anyzm commented 3 years ago

周末我可以将代码提一个PR,或者发布到我的个人仓库

klay-ke commented 3 years ago

是的 主要是python 那先看一下你的PR吧 谢谢

Anyzm commented 3 years ago

PR提了,可能有两个问题:注释是中文,另外新加的模块没加代码chekStyle,其余基本ok,大佬们看下吧

https://github.com/vesoft-inc/nebula-java/pull/349

klay-ke commented 3 years ago

你好 你在写这套orm框架的时候有写设计文档吗?如果有的话可以提供一下吗,方便review一些,谢谢

Anyzm commented 3 years ago

你好 你在写这套orm框架的时候有写设计文档吗?如果有的话可以提供一下吗,方便review一些,谢谢

没有写设计文档,只是在脑海中设计了一下。回头我再补充一下设计文档

klay-ke commented 3 years ago

你好 你在写这套orm框架的时候有写设计文档吗?如果有的话可以提供一下吗,方便review一些,谢谢

没有写设计文档,只是在脑海中设计了一下。回头我再补充一下设计文档

再次感谢你对nebula社区做出的贡献,前面因为一些事情耽误了你这边的review,实在不好意思

Anyzm commented 3 years ago

你好 你在写这套orm框架的时候有写设计文档吗?如果有的话可以提供一下吗,方便review一些,谢谢

没有写设计文档,只是在脑海中设计了一下。回头我再补充一下设计文档

再次感谢你对nebula社区做出的贡献,前面因为一些事情耽误了你这边的review,实在不好意思

没事,反正不着急,设计文档已经补充到PR中了,请查看,如依然有任何问题可随时沟通。

CPWstatic commented 2 years ago

您好,非常感谢您的贡献。我们内部沟通了一下,java orm可以参考go orm,可以在自己的组织开源,也可以个人开源。

Anyzm commented 2 years ago

您好,非常感谢您的贡献。我们内部沟通了一下,java orm可以参考go orm,可以在自己的组织开源,也可以个人开源。

好的

Anyzm commented 2 years ago

在个人仓库已发布,过段时间上传中央仓库,开发者也可以下载源码自己改。 通过捐赠仓库的形式贡献ORM https://github.com/Anyzm/graph-ocean

Nicole00 commented 1 year ago

感谢Anyzm老师提供的orm:https://github.com/nebula-contrib/graph-ocean

Anyzm commented 1 year ago

感谢您的来件,邮件已收到。