JAVA 中的 OData REST API 带投影

OData REST API in JAVA with projection

在 JAVA 中可以制作类似的东西吗?如果是,如何? (有样品)

考虑使用 http://olingo.apache.org/ 或 odata4j

我在 .NET 中制作了一个示例,它公开了 OData 格式的 REST API(让我的消费者能够使用标准化协议进行过滤、select、排序等)。

注意事项:我公开的模型不同于来自 ORM 的模型(避免公开数据库并为 control/restrict 消费者的查询提供可能性)。 两个用户 class:Models.User 和 DAL.User

样本:

public IHttpActionResult GetUsers(ODataQueryOptions<Models.User> queryOptions)
{
    //Get the IQueryable from DB/Repository
    DAL.UsersDAO usersDAO = new DAL.UsersDAO();
    IQueryable<DAL.User> usersQueryable = usersDAO.GetUsers();

    //Make the projection from the 'User of Project DAL' to the 'User of ODataProject'
    IQueryable<Models.User> usersProjected = usersQueryable.Select(user => new Models.User()
    {
        Id = user.Id,
        Name = user.Name,
        Gender = user.Gender
    });

    //At this point, the query was not executed yet. And with that, it is possible to add new Filters
    //like the ones send by the client or even some rules from Business Logic
    //(ex: user only see other users from his country, etc)

    //Appling the queryOptions requested by the consumer
    IQueryable usersWithOptionsApplied = queryOptions.ApplyTo(usersProjected);
    IQueryable<Models.User> usersToExecute = usersWithOptionsApplied as IQueryable<Models.User>;

    //Execute the Query against the Database/Repository/Whatever with all the filters
    IEnumerable<Models.User> usersExecuted = usersToExecute.ToList();

    return Ok(usersExecuted);
}

关键点是:

1 - 构建查询的可能性(从 Database/Repository/Whatever 获取构建器)

2 - 将查询投影到公开的模型(不是来自 ORM 的模型)

3 - 将用户发送的过滤器应用到 OData REST API (queryOptions)

我在此处上传的示例(在 .NET 中):http://multiupload.biz/2meagoxw2boa

我真的很感激任何这样做的尝试。 尝试使用 OData 作为跨平台技术的标准方式的概念证明。

我真的很高兴看到你的问题,因为这是我现在已经研究了几个月的主题,我希望我可以作为一个开源项目提供。我会尽量给出一个简洁的答案,但有很多事情要讲;-)

我使用 Olingo 作为我的概念证明,使用 ElasticSearch 作为我的后端,但我想为任何后端(SQL 和没有 SQL)提供一个开放的解决方案。

有两个主要部分:

  • 元数据配置。 Olingo 提供了一个实体EdmProvider,负责向图书馆提供托管实体的元数据。这一个在请求处理期间被调用以将请求路由到正确的元素处理。

    这个级别有两个案例。您可以手动配置此元素,也可以尝试通过自动检测后端结构来自动配置。对于第一个,我们需要扩展抽象的 class EdmProvider。我介绍了自定义 EdmProvider 将基于的中间元数据,因为需要一些提示来确定实现请求的正确方法(例如使用 ElasticSearch、父/子关系等)。以下是手动配置中间元数据的示例:

    MetadataBuilder builder = new MetadataBuilder();
    builder.setNamespaces(Arrays.asList(new String[] { "odata" }));
    builder.setValidator(validator);
    
    TargetEntityType personDetailsAddressType
            = builder.addTargetComplexType("odata", 
                                 "personDetailsAddress");
    personDetailsAddressType.addField("street", "Edm.String");
    personDetailsAddressType.addField("city", "Edm.String");
    personDetailsAddressType.addField("state", "Edm.String");
    personDetailsAddressType.addField("zipCode", "Edm.String");
    personDetailsAddressType.addField("country", "Edm.String");
    
    TargetEntityType personDetailsType
            = builder.addTargetEntityType(
                               "odata", "personDetails");
    personDetailsType.addPkField("personId", "Edm.Int32");
    personDetailsType.addField("age", "Edm.Int32");
    personDetailsType.addField("gender", "Edm.Boolean");
    personDetailsType.addField("phone", "Edm.String");
    personDetailsType.addField(
                 "address", "odata.personDetailsAddress");
    

    第二种方法并不总是可行,因为后端不需要提供所有必要的元数据。对于 ElasticSearch,我们需要在类型映射中添加元数据来支持它。

    现在我们有了这个,我们可以专注于请求处理。

  • 请求处理 Olingo 允许基于处理器处理请求。事实上,库会将请求路由到可以处理请求的类型的处理器。例如,如果你想在一个实体上做一些事情,一个处理器实现了 EntityCollectionProcessorCountEntityCollectionProcessor 和/或 EntityProcessor 将被选中并使用。然后将调用接口的正确方法。属性也是一样,...

    所以我们需要实现处理器来适应请求并与目标后端交互。在这个级别,有很多管道(使用 Olingo 序列化器/反序列化器,构建上下文 URL,最终提取参数,...)并且一个好的方法似乎是实现一个通用层作为基础。后者负责在后台执行操作(读、写、查询等),也负责处理Olingo的类型(Entity, Property, )和所使用的元素之间的转换后端驱动程序(在 ElasticSearch 的情况下,原始对象,命中 - 参见 http://www.elastic.co/guide/en/elasticsearch/client/java-api/current/client.html)。

因此,如果我们需要公开的 OData 模型和后端模式之间的间接级别,我们需要在上述元数据和请求级别实现它们之间的映射。这允许实体及其属性的名称不完全相同。

关于过滤器,我们可以使用 class UriInfo 在 Olingo 中轻松访问它们(参见方法 getFilterOptiongetSelectOptiongetExpandOptiongetOrderByOption, getSkipOptiongetTopOption),如下所述在一个处理器中:

@Override
public void readEntityCollection(final ODataRequest request,
              ODataResponse response, final UriInfo uriInfo,
              final ContentType requestedContentType)
              throws ODataApplicationException, SerializerException {
    (...)
    EntitySet entitySet = dataProvider.readEntitySet(edmEntitySet,
                uriInfo.getFilterOption(), uriInfo.getSelectOption(),
                uriInfo.getExpandOption(), uriInfo.getOrderByOption(),
                uriInfo.getSkipOption(), uriInfo.getTopOption());

    (...)
}

然后可以将所有提示传递给负责在后端创建请求的元素。这是 ElasticSearch 的示例(注意 class QueryBuilder 是 ElasticSearch Java 的工厂来构建查询):

QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
if (topOption!=null && skipOption!=null) {
    requestBuilder.setFrom(skipOption.getValue())
                  .setSize(topOption.getValue());
}

带有查询参数 $filter 的查询有点乏味,因为我们需要将初始查询转换为到目标后端的查询。在 class FilterOption 上,我们可以访问允许访问表达式的 Expression 实例。以下代码描述了一种基于此 class:

构建 ElasticSearch 查询的简化方法
Expression expression = filterOption.getExpression();
QueryBuilder queryBuilder
           = expression.accept(new ExpressionVisitor() {
    (...)

    @Override
    public Object visitBinaryOperator(
              BinaryOperatorKind operator, Object left,
              Object right) throws ExpressionVisitException,
                                  ODataApplicationException {
        String fieldName = (String)left;
        // Simplification but not really clean code ;-)
        String value = ((String)right).substring(
                          1, right.length() - 1);
        return QueryBuilders.termQuery((String) left, right);
    }

    @Override
    public Object visitLiteral(String literal)
                throws ExpressionVisitException,
                ODataApplicationException {
        return literal;
    }

    @Override
    public Object visitMember(UriInfoResource member)
                throws ExpressionVisitException,
                ODataApplicationException {
        UriResourcePrimitiveProperty property
                   = (UriResourcePrimitiveProperty)
                          member.getUriResourceParts().get(0);
        return property.getProperty().getName();
    }
}

如果我们在查询参数 $filter 中使用值 description eq 'test',我们将得到类似的东西:

>> visitMember - member = [description]
>> visitLiteral - literal = 'test'
>> visitBinaryOperator - operator = eq, left = description, right = 'test'

另一个棘手的部分在于处理导航属性和最终在后端对数据进行非规范化的方式。我认为这有点超出你的问题范围。

如果有什么不明白或/以及您是否需要更多详细信息,请随时问我。

希望对你有帮助, 蒂埃里