Web接口输出方式选型

Web输出方式选型

DTO

DTO分层

必要性

DTO与PO的不对称关系决定了二者不能互相代替

DTO与PO存在在映射关系,可能是多对一,也可能是一对多,最特殊的关系就是上面大家说的这种情况“一对一”。也就是在“一对一”的情况下可以实现DTO与PO的混用,而其他情况下,如果混用都需要PO进行冗余设计,考虑这些冗余设计会比直接的、简单的造一个新的DTO出现要耗费更多的脑细胞,同时这个东西又不利于后期维护,可以说“牵一发,动从上到下”。但在项目初期,业务模型不确定的情况下,直接PO穿透到WEB层,能快速迭代出功能

性能上决定了PO代替DTO是个蹩脚的设计

PO是与数据库直接交互的对象,比如我要在页面上显示数据库中的数据,如果用PO来实现那么这个PO就得一直保持与数据库的连接,直到数据显示到页面上来。这样如果在service层有复杂运算的话,那么数据库方面的延时是非常可观的,而如果转换成DTO之后,数据库连接就可以尽快释放。所以从性能上来说应该使用DTO--当然对于性能不是很苛刻的情况下不用DTO也行

缺点

  • 最极端的情况下,每个接口输出都配套有一个DTO,那么长期维护是很痛苦的
  • 程序员很习惯使用bean copy的方式将PO的数据拷贝到DTO上,如果PO增加了字段,而DTO忘记补充,是不会出现报错的

模板语言(Mustache为例)

使用模板输出JSON数据
教程地址

Mustache 的模板语法很简单

{{keyName}}
{{#keyName}} {{/keyName}}
{{^keyName}} {{/keyName}}
{{.}}
{{<partials}}
{{{keyName}}}
{{!comments}}

对比DTO

  • 最极端的情况下,每个接口输出都配套有一个模板,在java开发中,丧失了类的继承,编译校验等特性,应该比DTO还更痛苦
  • DTO通常会把PO引用过去,PO是持久层,在json输出时会触发如hibernate的延迟加载,导致递归输出等问题,反而模板语言就没有此烦恼

GraphQL

** 一个GraphQL查询可以包含一个或者多个操作(operation),类似于一个RESTful API。操作(operation)可以使两种类型:查询(Query)或者修改(mutation)。

query {
  client(id: 1) {
    id 
    name
  }
}

注意上面的例子有三个不同的部分组成:
client是查询的operation
(id: 1)包含了传入给Query的参数
查询包含id和name字段,这些字段也是我们希望查询可以返回的

{
  "data": {
    "client": {
      "id": "1",
      "name": "Uncle Charlie"
    }
  }
}

graphql-java

Schema相当于一个数据库,它有很多GraphQLFieldDefinition组成,Field相当于数据库表/视图,每个表/视图又由名称、查询参数、数据结构、数据组成.

1) 先定义一个数据结构(GraphQLOutputType)字段,然后定义一个初始化方法
定义一个user的规格字段,此类可以考虑使用lombok来帮我们生成
lombok传送门

private GraphQLOutputType userType;

private void initOutputType() {
      /**
       * 会员对象结构
       */
      userType = newObject()
              .name("User")
              .field(newFieldDefinition().name("id").type(GraphQLInt).build())
              .field(newFieldDefinition().name("age").type(GraphQLInt).build())
              .field(newFieldDefinition().name("sex").type(GraphQLInt).build())
              .field(newFieldDefinition().name("name").type(GraphQLString).build())
              .field(newFieldDefinition().name("pic").type(GraphQLString).build())
              .build();
}

2)再定义两个表/视图,它包括名称,查询参数,数据结构,以及数据检索器

/**
     * 查询单个用户信息
     * @return
     */
    private GraphQLFieldDefinition createUserField() {
        return GraphQLFieldDefinition.newFieldDefinition()
                .name("user")
                .argument(newArgument().name("id").type(GraphQLInt).build())
                .type(userType)
                .dataFetcher(environment -> {
                    // 获取查询参数
                    int id = environment.getArgument("id");

                    // 执行查询, 这里随便用一些测试数据来说明问题
                    User user = new User();
                    user.setId(id);
                    user.setAge(id + 15);
                    user.setSex(id % 2);
                    user.setName("Name_" + id);
                    user.setPic("pic_" + id + ".jpg");
                    return user;
                })
                .build();
    }

    /**
     * 查询多个会员信息
     * @return
     */
    private GraphQLFieldDefinition createUsersField() {
        return GraphQLFieldDefinition.newFieldDefinition()
                .name("users")
                .argument(newArgument().name("page").type(GraphQLInt).build())
                .argument(newArgument().name("size").type(GraphQLInt).build())
                .argument(newArgument().name("name").type(GraphQLString).build())
                .type(new GraphQLList(userType))
                .dataFetcher(environment -> {
                    // 获取查询参数
                    int page = environment.getArgument("page");
                    int size = environment.getArgument("size");
                    String name = environment.getArgument("name");

                    // 执行查询, 这里随便用一些测试数据来说明问题
                    List<User> list = new ArrayList<>(size);
                    for (int i = 0; i < size; i++) {
                        User user = new User();
                        user.setId(i);
                        user.setAge(i + 15);
                        user.setSex(i % 2);
                        user.setName(name + "_" + page + "_" + i);
                        user.setPic("pic_" + i + ".jpg");
                        list.add(user);
                    }
                    return list;
                })
                .build();
    }


3)接着定义一个Schema,并将其初始化,它包含一个名称,以及一个或多个表/视图(Field)

 private GraphQLSchema schema;

    public GraphSchema() {
        initOutputType();
        schema = GraphQLSchema.newSchema().query(newObject()
                .name("GraphQuery")
                .field(createUsersField())
                .field(createUserField())
                .build()).build();
    }

4)之后写一个main方法,来测试一下

public static void main(String[] args) {
        GraphQLSchema schema = new GraphSchema().getSchema();

        String query1 = "{users(page:2,size:5,name:\"john\") {id,sex,name,pic}}";
        String query2 = "{user(id:6) {id,sex,name,pic}}";
        String query3 = "{user(id:6) {id,sex,name,pic},users(page:2,size:5,name:\"john\") {id,sex,name,pic}}";

        Map<String, Object> result1 = (Map<String, Object>) new GraphQL(schema).execute(query1).getData();
        Map<String, Object> result2 = (Map<String, Object>) new GraphQL(schema).execute(query2).getData();
        Map<String, Object> result3 = (Map<String, Object>) new GraphQL(schema).execute(query3).getData();

        // 查询用户列表
        System.out.println(result1);
        // 查询单个用户
        System.out.println(result2);
        // 单个用户、跟用户列表一起查
        System.out.println(result3);

}

5)最后把main方法里面的代码放到web层,只需要定义一个query参数,很容易就把查询服务搭建好了,dataFetcher 里面还是调用原来的查询接口

总结

在项目开发过程中,为了快速迭代,采用PO直接穿透到WEB层输出,遇到一些懒加载穿透过多JSON递归输出的问题。DTO有它客观存在性,但与PO共存也会导致冗余字段过多,基本上是PO加一个字段,DTO也必须加一个字段,维护起来也麻烦;模板语言会从根本上消除了懒加载的问题,但模板文件也会多起来;GraphQL可以认为是在模板的基础上,增加了查询的灵活度,是值得引入的框架。

推荐阅读更多精彩内容