简体   繁体   English

Spring Rest 控制器返回特定字段

[英]Spring Rest Controller Return Specific Fields

I've been going through my head the best way to design a JSON API using Spring MVC.我一直在思考使用 Spring MVC 设计 JSON API 的最佳方法。 As we all know IO is expensive, and thus I don't want to make the client make several API calls to get what they need.众所周知,IO 很昂贵,因此我不想让客户端进行多次 API 调用来获得他们需要的东西。 However at the same time I don't necessarily want to return the kitchen sink.但与此同时,我不一定要归还厨房水槽。

As an example I was working on a game API similar to IMDB but for video games instead.例如,我正在开发一个类似于 IMDB 的游戏 API,但用于视频游戏。

If I returned everything connected to Game it would look something like this.如果我返回与 Game 相关的所有内容,它会看起来像这样。

/api/game/1 /api/游戏/1

{
    "id": 1,
    "title": "Call of Duty Advanced Warfare",
    "release_date": "2014-11-24",
    "publishers": [
        {
            "id": 1,
            "name": "Activision"
        }
    ],
    "developers": [
        {
            "id": 1,
            "name": "Sledge Hammer"
        }
    ],
    "platforms": [
        {
            "id": 1,
            "name": "Xbox One",
            "manufactorer": "Microsoft",
            "release_date": "2013-11-11"
        },
        {
            "id": 2,
            "name": "Playstation 4",
            "manufactorer": "Sony",
            "release_date": "2013-11-18"
        },
        {
            "id": 3,
            "name": "Xbox 360",
            "manufactorer": "Microsoft",
            "release_date": "2005-11-12"
        }
    ],
    "esrbRating": {
        "id": 1,
        "code": "T",
        "name": "Teen",
        "description": "Content is generally suitable for ages 13 and up. May contain violence, suggestive themes, crude humor, minimal blood, simulated gambling and/or infrequent use of strong language."
    },
    "reviews": [
        {
            "id": 1,
            "user_id": 111,
            "rating": 4.5,
            "description": "This game is awesome"
        }
    ]
}

However they may not need all this information, but then again they might.然而,他们可能不需要所有这些信息,但他们可能再次需要。 Making calls for everything seems like a bad idea from I/O and performance.从 I/O 和性能来看,调用所有内容似乎是个坏主意。

I thought about doing it by specifying include parameter in the requests.我考虑通过在请求中指定包含参数来做到这一点。

Now for example if you did not specify any includes all you would get back is the following.现在例如,如果您没有指定任何包含,那么您将得到以下内容。

{
    "id": 1,
    "title": "Call of Duty Advanced Warfare",
    "release_date": "2014-11-24"
}

However it you want all the information your requests would look something like this.但是,您希望您的请求的所有信息看起来像这样。

/api/game/1?include=publishers,developers,platforms,reviews,esrbRating

This way the client has the ability to specify how much information they want.这样,客户就可以指定他们想要多少信息。 However I'm kind of at a loss the best way to implement this using Spring MVC.但是,我对使用 Spring MVC 实现这一点的最佳方法感到不知所措。

I'm thinking the controller would look something like this.我在想控制器看起来像这样。

public @ResponseBody Game getGame(@PathVariable("id") long id, 
    @RequestParam(value = "include", required = false) String include)) {

        // check which include params are present

        // then someone do the filtering?
}

I'm not sure how you would optionally serialize the Game object.我不确定您将如何选择序列化 Game 对象。 Is this even possible.这甚至可能吗。 What is the best way to approach this in Spring MVC?在 Spring MVC 中解决这个问题的最佳方法是什么?

FYI, I am using Spring Boot which includes Jackson for serialization.仅供参考,我正在使用包含 Jackson 的 Spring Boot 进行序列化。

Instead of returning a Game object, you could serialize it as as a Map<String, Object> , where the map keys represent the attribute names.您可以将其序列化为Map<String, Object> ,而不是返回Game对象,其中映射键表示属性名称。 So you can add the values to your map based on the include parameter.因此,您可以根据include参数将值添加到地图中。

@ResponseBody
public Map<String, Object> getGame(@PathVariable("id") long id, String include) {

    Game game = service.loadGame(id);
    // check the `include` parameter and create a map containing only the required attributes
    Map<String, Object> gameMap = service.convertGameToMap(game, include);

    return gameMap;

}

As an example, if you have a Map<String, Object> like this:例如,如果您有一个Map<String, Object>像这样:

gameMap.put("id", game.getId());
gameMap.put("title", game.getTitle());
gameMap.put("publishers", game.getPublishers());

It would be serialized like this:它将像这样序列化:

{
  "id": 1,
  "title": "Call of Duty Advanced Warfare",
  "publishers": [
    {
        "id": 1,
        "name": "Activision"
    }
  ]
}

Being aware that my answer comes quite late: I'd recommend to look at Projections .意识到我的答案来得太晚了:我建议看一下Projections

What you're asking for is what projections are about.你要问的是预测是关于什么的。

Since you're asking about Spring I'd give this one a try: https://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts既然你问的是春天,我会试试这个: https ://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts

A very dynamic way for providing different projections on demand is offered by GraphQL . GraphQL提供了一种按需提供不同预测的非常动态的方式。 I just came across a very helpful article about how to use GraphQL with SpringBoot : https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/我刚刚看到一篇关于如何在SpringBoot中使用GraphQL的非常有用的文章: https ://www.graphql-java.com/tutorials/getting-started-with-spring-boot/

This can be done by Spring Projections .这可以通过 Spring Projections来完成。 Also works fine with Kotlin.也适用于 Kotlin。 Take a look here: https://www.baeldung.com/spring-data-jpa-projections看看这里: https ://www.baeldung.com/spring-data-jpa-projections

Looks there is always quite a lot of manual work.看起来总是有相当多的手工工作。 If you use some persistence abstraction you can have less work compared to plain SpringJDBC (JdbcTemplate).如果您使用一些持久性抽象,那么与普通的 SpringJDBC (JdbcTemplate) 相比,您可以减少工作量。 Also depends if your model is aligned with database column names.还取决于您的模型是否与数据库列名对齐。 There are nice series about Query Languages eg QueryDSL: https://www.baeldung.com/rest-search-language-spring-jpa-criteria .关于查询语言有很好的系列,例如 QueryDSL: https ://www.baeldung.com/rest-search-language-spring-jpa-criteria。

Using SpringRest & QueryDSL you can end up with something like this:使用 SpringRest 和 QueryDSL 你可以得到这样的结果:

Rest controller:休息控制器:

//...
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
//...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
//...

@ApiOperation("Returns list of all users")
@GetMapping(value = "/users", produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public Page<UsersRest> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "userId,desc") String[] sort,
        @RequestParam(required = false) Optional<String> search,
        @RequestParam(required = false) Optional<String> fields) {

    Sort sorting = parser.parseSortingParameters(sort);
    PageRequest pageable = PageRequest.of(page, size, sorting);
    // search
    BooleanExpression searchPredicate = parser.parseSearchParameter(search);
    // requested columns
    Path[] columns = parser.parseFieldsParameter(fields);

    Page<User> userPage = userService.getAllUsers(pageable, searchPredicate, columns);

    return new PageImpl<>(userPage, userPage.getPageable(), userPage.getTotalElements());
}

Repository class:存储库类:

//...
import com.querydsl.core.QueryResults;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.sql.Configuration;
import com.querydsl.sql.SQLQuery;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.spring.SpringConnectionProvider;
import com.querydsl.sql.spring.SpringExceptionTranslator;
//...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
//...

@Transactional(readOnly = true)
public Page<User> findAll(Pageable pageable, BooleanExpression searchPredicate, Path[] columns) {
    final var userTable = new QUser("USER");

    // Alternatively (if column names are aligned with field names - so manual mapping is not needed) can be used
    // Expressions.path constructor to dynamically create path:
    // http://www.querydsl.com/static/querydsl/latest/reference/html/ch03.html
    OrderSpecifier<?>[] order = convertToDslOrder(pageable.getSort());

    SQLQuery<Tuple> sql = queryFactory
            .select(columns)
            .from(userTable)
            .where(searchPredicate)
            .orderBy(order);

    sql.offset(pageable.getPageNumber());
    sql.limit(pageable.getPageSize());

    QueryResults<Tuple> queryResults = sql.fetchResults();

    final long totalCount = queryResults.getTotal();
    List<Tuple> results = queryResults.getResults();
    List<User> users = userRowMapper(userTable, results);

    return new PageImpl<>(users, pageable, totalCount);
}

Solution 1: Add @JsonIgnore to the variable you dont want to include in API response (in the model)解决方案 1:将 @JsonIgnore 添加到您不想包含在 API 响应中的变量(在模型中)

@JsonIgnore
    private Set<Student> students;

Solution 2: Remove the getters for the variables you don't want included.解决方案 2:删除您不想包含的变量的 getter。

If you need them else where, use different format for the getters so spring doesn't know about it.如果您在其他地方需要它们,请为 getter 使用不同的格式,以便 spring 不知道它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM