Reputation: 1521
I have an entity Users
, which contains id
and emailAddress
. I want to write a controller which has 3 get methods simultaneously:
http://localhost:8080/api/users?pageNumber=0&pageSize=10
http://localhost:8080/api/users/209347293875928375928
http://localhost:8080/api/users/fireball%40email.com
or http://localhost:8080/api/users?emailAddress="fireball%40email.com"
NOTE: The difference in the two pieces of code is the arguments of the last function in both(The first is @PathVariable
and the second is a @RequestParam
.
I have tried two ways to accomplish this(both run into a separate issue), the first being:
@GetMapping
@ApiOperation(value = "List all Users")
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class, responseContainer = "List"),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
@Transactional(readOnly = true)
public ResponseEntity<Page<User>> getAll(
@RequestParam(required = false, defaultValue = "0") int pageNumber,
@RequestParam(required = false, defaultValue = "10") int pageSize) {
return ResponseEntity.ok(UserService.getUsers(pageNumber, pageSize));
}
@GetMapping(path = "/{id}")
@ApiOperation(value = "Get User by ID")
@Transactional(readOnly = true)
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
public ResponseEntity<?> get(@PathVariable("id") final UUID id) {
return UserService.get(id)
.map(ResponseEntity::ok)
.map(ResponseEntity.class::cast)
.orElse(
ResponseEntity.status(NOT_FOUND).body(new Error(format(USER_NOT_FOUND_MESSAGE, id))));
}
@GetMapping
@ApiOperation(value = "Get User by Email Address")
@Transactional(readOnly = true)
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
public ResponseEntity<?> get(@RequestParam("email") final String email) {
return UserService.get(email)
.map(ResponseEntity::ok)
.map(ResponseEntity.class::cast)
.orElse(
ResponseEntity.status(NOT_FOUND).body(new Error(format(USER_NOT_FOUND_MESSAGE, email))));
}
The above fails at compilation with Ambiguous mapping. Cannot map 'userController' method
as error. Essentially getAll()
and get(@RequestParam("email") final String email)
have the same URL path - /users
.
The second being:
@GetMapping
@ApiOperation(value = "List all Users")
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class, responseContainer = "List"),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
@Transactional(readOnly = true)
public ResponseEntity<Page<User>> getAll(
@RequestParam(required = false, defaultValue = "0") int pageNumber,
@RequestParam(required = false, defaultValue = "10") int pageSize) {
return ResponseEntity.ok(UserService.getUsers(pageNumber, pageSize));
}
@GetMapping(path = "/{id}")
@ApiOperation(value = "Get User by ID")
@Transactional(readOnly = true)
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
public ResponseEntity<?> get(@PathVariable("id") final UUID id) {
return UserService.get(id)
.map(ResponseEntity::ok)
.map(ResponseEntity.class::cast)
.orElse(
ResponseEntity.status(NOT_FOUND).body(new Error(format(USER_NOT_FOUND_MESSAGE, id))));
}
@GetMapping(path = "/{email}")
@ApiOperation(value = "Get User by Email Address")
@Transactional(readOnly = true)
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
public ResponseEntity<?> get(@PathVariable("email") final String email) {
return UserService.get(email)
.map(ResponseEntity::ok)
.map(ResponseEntity.class::cast)
.orElse(
ResponseEntity.status(NOT_FOUND).body(new Error(format(USER_NOT_FOUND_MESSAGE, email))));
}
Here I run into the issue of the controller not being able to resolve between get(@PathVariable("email") final String email
and get(@PathVariable("id") final UUID id)
with the following error:
Ambiguous handler methods mapped for '/api/gems/users/fireball%40email.com': {public org.springframework.http.ResponseEntity com.personal.project.controllers.UserController.get(java.util.UUID), public org.springframework.http.ResponseEntity com.personal.project.controllers.UserController.get(java.lang.String)}
Upvotes: 0
Views: 2790
Reputation: 130927
In line with my previous answer, you only need two controller methods. Then, in the method that handles GET /users
requests, the presence or absence of the name
query parameter will determine whether the server will fetch all users or filter the user by email.
See below the example below. For simplification, I have not added the OpenAPI annotations and also removed pagination, but you can easily add them as you need:
@RestController
@RequestMapping(path = "/users",
produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
@GetMapping
public ResponseEntity<List<User>> findUsers(@RequestParam("email") final String email) {
// If email is null/empty/blank, then fetch all users
// Otherwise filter users by email
}
@GetMapping(path = "/{id}")
public ResponseEntity<User> findUserById(@PathVariable("id") final UUID id) {
// Find user with the given ID
}
}
In case you plan to support multiple filters (or simply want to avoid if
-'else's or custom repository methods), it may be a good idea to use Query by Example (QBE), which is supported by Spring Data:
Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require you to write queries that contain field names. In fact, Query by Example does not require you to write queries by using store-specific query languages at all.
The documentation also states the following:
The Query by Example API consists of three parts:
- Probe: The actual example of a domain object with populated fields.
ExampleMatcher
: TheExampleMatcher
carries details on how to match particular fields. It can be reused across multipleExample
s.Example
: AnExample
consists of the probe and theExampleMatcher
. It is used to create the query.Query by Example is well suited for several use cases:
- Querying your data store with a set of static or dynamic constraints.
- Frequent refactoring of the domain objects without worrying about breaking existing queries.
- Working independently from the underlying data store API.
Query by Example also has several limitations:
- No support for nested or grouped property constraints, such as
firstname = ?0 or (firstname = ?1 and lastname = ?2)
.- Only supports starts/contains/ends/regex matching for strings and exact matching for other property types.
See below how to use Query by Example in your case:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserFilter {
private String email;
}
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/users",
produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<User>> findUsers(@RequestParam("email") final String email) {
UserFilter filter = UserFilter.builder().name(email).build();
List<User> users = userService.findUsers(filter);
return ResponseEntity.ok(users);
}
@GetMapping(path = "/{id}")
public ResponseEntity<User> findUserById(@PathVariable("id") final UUID id) {
User user = userService.findUserById(filter).orElseThrow(SomeSortOfException::new);
return ResponseEntity.ok(user);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public List<User> findUsers(UserFilter filter) {
User user = new User();
user.setEmail(filter.getEmail());
Example<User> example = Example.of(user);
return userRepository.findAll(example);
}
public Optional<User> findUserById(UUID id) {
return userRepository.findById(id);
}
}
Upvotes: 1
Reputation: 4502
You can solve it by merging these two Ambiguous path into one. For the first case:
@GetMapping
@ApiOperation(value = "List all Users")
@ApiResponses({
@ApiResponse(code = 200, message = "OK", response = User.class, responseContainer = "List"),
@ApiResponse(code = 401, message = "Unauthorized", response = Error.class),
@ApiResponse(code = 404, message = "Not Found", response = Error.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = Error.class)
})
@Transactional(readOnly = true)
public ResponseEntity<?> getUsers(
@RequestParam(required = false, defaultValue = "0") int pageNumber,
@RequestParam(required = false, defaultValue = "10") int pageSize,
// add email as param.
@RequestParam(required = false) String email,
) {
if(email ==null || StringUtils.isEmpty(email)){
return ResponseEntity.ok(UserService.getUsers(pageNumber, pageSize));
}else return UserService.get(email)
.map(ResponseEntity::ok)
.map(ResponseEntity.class::cast)
.orElse(
ResponseEntity.status(NOT_FOUND).body(new Error(format(USER_NOT_FOUND_MESSAGE, email))));
}
For the second one:
// change the type UUID id to string.
@GetMapping(path = "/{id}")
...
public ResponseEntity<?> get(@PathVariable("id") final String id) {
// check if id as an uuid or email, and based on that take action
}
Upvotes: 1