Итак, у нас есть DTO-класс UserDto, с соответствующими полями:
public class UserDto {
private Long id;
private String name;
private String login;
private String password;
private String email;
}
Я опускаю конструкторы и геттеры-сеттеры — уверен, вы умеете их создавать, а увеличивать в 3-4 раза код смысла не вижу — представим, что они уже есть.
Мы будем принимать DTO через контроллер с CRUD-методами. Опять же, я не буду писать все методы CRUD — для чистоты эксперимента нам хватит пары. Пусть это будут create и updateName.
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> create(@RequestBody UserDto dto) {
return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
}
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> updateName(@RequestBody UserDto dto) {
return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
}
Для наглядности их тоже пришлось упростить. Таким образом, мы получаем какой-то JSON, который преобразуется в UserDto, и возвращаем UserDto, который преобразуется в JSON и отправляется на клиент.
Теперь предлагаю ознакомиться с теми несколькими аннотациями валидации, с которыми мы будем работать.
@Null //значение должно быть null
@NotNull //значение должно быть не null
@Email //это должен быть e-mail
Со всеми аннотациями можно ознакомиться в библиотеке javax.validation.constraints. Итак, настроим наше DTO таким образом, чтобы сразу получать валидированый объект для дальнейшего перевода в сущность и сохранения в БД. Те поля, которые должны быть заполнены, мы пометим NotNull, также пометим e-mail:
public class UserDto {
@Null //автогенерация в БД
private Long id;
@NotNull
private String name;
@NotNull
private String login;
@NotNull
private String password;
@NotNull
@Email
private String email;
}
Мы задали настройки валидации для DTO — должны быть заполнены все поля, кроме id — он генерируется в БД. Добавим валидацию в контроллер:
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> create(@Validated @RequestBody UserDto dto) {
return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
}
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> updateName(@Validated @RequestBody UserDto dto) {
return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
}
Настроенная таким образом валидация подойдёт к созданию нового пользователя, но не подойдёт для обновления существующих — ведь для этого нам нужно будет получить id (который задан как null), а также, пропустить поля login, password и email, поскольку в updateName мы изменяем только имя. То есть, нам нужно получить id и name, и ничего больше. И здесь нам потребуются интерфейсы видимости.
Создадим прямо в классе DTO интерфейс (для наглядности, я рекомендую выносить такие вещи в отдельный класс, а лучше, в отдельный пакет, например, transfer). Интерфейс будет называться New, второй будет называться Exist, от которого мы унаследуем UpdateName (в дальнейшем мы сможем наследовать от Exist другие интерфейсы видимости, мы же не одно имя будем менять):
public class User {
interface New {
}
interface Exist {
}
interface UpdateName extends Exist {
}
@Null //автогенерация в БД
private Long id;
@NotNull
private String name;
@NotNull
private String login;
@NotNull
private String password;
@NotNull
@Email
private String email;
}
Теперь мы пометим наши аннотации интерфейсом New.
@Null(groups = {New.class})
private Long id;
@NotNull(groups = {New.class})
private String name;
@NotNull(groups = {New.class})
private String login;
@NotNull(groups = {New.class})
private String password;
@NotNull(groups = {New.class})
@Email(groups = {New.class})
private String email;
Теперь эти аннотации работают только при указании интерфейса New. Нам остаётся только задать аннотации для того случая, когда нам потребуется апдейтить поле name (напомню, нам нужно указать не-нулловвыми id и name, остальные нулловыми). Вот как это выглядит:
@Null(groups = {New.class})
@NotNull(groups = {UpdateName.class})
private Long id;
@NotNull(groups = {New.class, UpdateName.class})
private String name;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
private String login;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
private String password;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@Email(groups = {New.class})
private String email;
Теперь нам осталось задать необходимые настройки в контроллерах, прописать интерфейс, чтобы задать валидацию:
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> create(@Validated(UserDto.New.class) @RequestBody UserDto dto) {
return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
}
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> updateName(@Validated(UserDto.UpdateName.class) @RequestBody
UserDto dto) {
return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
}
Теперь для каждого метода будет вызываться свой набор настроек.
Итак, мы разобрались, как валидировать входные данные, теперь осталось валидировать выходные. Это делается при помощи аннотации @JsonView.
Сейчас в выходном DTO, который мы отдаём обратно, содержатся все поля. Но, предположим, нам не нужно никогда отдавать пароль (кроме исключительных случаев).
Для валидации выходного DTO добавим ещё два интерфейса, которые будут отвечать за видимость выходных данных — Details (для отображения пользователям) и AdminDetails (для отображения только админам). Интерфейсы могут наследоваться друг от друга, но для простоты восприятия сейчас мы делать этого не будем — достаточно примера со входными данными на этот счёт.
interface New {
}
interface Exist {
}
interface UpdateName extends Exist {
}
interface Details {
}
interface AdminDetails {
}
Теперь мы можем аннотировать поля так, как нам нужно (видны все, кроме пароля):
@Null(groups = {New.class})
@NotNull(groups = {UpdateName.class})
@JsonView({Details.class})
private Long id;
@NotNull(groups = {New.class, UpdateName.class})
@JsonView({Details.class})
private String name;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@JsonView({Details.class})
private String login;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@JsonView({AdminDetails.class})
private String password;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@Email(groups = {New.class})
@JsonView({Details.class})
private String email;
Осталось пометить нужные методы контроллера:
@JsonView(Details.class)
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> create(@Validated(UserDto.New.class) @RequestBody UserDto dto) {
return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
}
@JsonView(Details.class)
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> updateName(@Validated(UserDto.UpdateName.class) @RequestBody UserDto dto) {
return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
}
А когда-нибудь в другой раз мы пометим аннотацией @JsonView(AdminDetails.class) метод, который будет дёргать только пароль. Если же мы хотим, чтобы админ получал всю информацию, а не только пароль, аннотируем соответствующим образом все нужные поля:
@Null(groups = {New.class})
@NotNull(groups = {UpdateName.class})
@JsonView({Details.class, AdminDetails.class})
private Long id;
@NotNull(groups = {New.class, UpdateName.class})
@JsonView({Details.class, AdminDetails.class})
private String name;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@JsonView({Details.class, AdminDetails.class})
private String login;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@JsonView({AdminDetails.class})
private String password;
@NotNull(groups = {New.class})
@Null(groups = {UpdateName.class})
@Email(groups = {New.class})
@JsonView({Details.class, AdminDetails.class})
private String email;
Надеюсь, эта статья помогла разобраться с валидацией входных DTO и видимостью данных выходных.
Комментарии (22)
michael_vostrikov
06.12.2017 08:01Жуть какая. Можно было просто сделать 2 разных DTO. Без всяких магических аннотаций.
AstarothAst
06.12.2017 10:23Я опускаю конструкторы и геттеры-сеттеры — уверен, вы умеете их создавать, а увеличивать в 3-4 раза код смысла не вижу — представим, что они уже есть.
Для этого придумали lombok и аннотацию Data
krupt
06.12.2017 12:54А какие сообщения об ошибках будут уходить в ответе? Хотелось бы взглянуть
Leffchik
06.12.2017 22:09Плюсую. Также интересуют более сложные варианты валидации. К примеру, если ли пользователь с таким именем уже в БД, валидация завязанная на несколько полей сразу и проч. Будет ли продолжение?
P.S. Также не нашёл аннотацию @Validation. Есть @Validated. Возможно перепутали?AstarothAst
07.12.2017 11:16Не претендую на правоту, но «более сложные варианты валидации», наверное, все же лучше реализовывать махровой императивщиной, а не магическими аннотациями — чем сложнее будет маршрут валидации, тем сложнее будет понять, что, как и в каком порядке проверяется. Плюс это будет дебажится, что позволит найти ответ на рано или поздно возникнувший вопрос «а почему конкретно в этом случае не работает?»
Leffchik
07.12.2017 11:21Согласен. Тем не менее, интересуют варианты реализации, общий каркас, скажем так.
rraderio
07.12.2017 13:31Можно как-то сделать чтобы часть валидировались аннотациями а часть махровой императивщиной?
pmcode
08.12.2017 12:28JSR-303 неплох, но в сложных случаях декларативная логика так или иначе пасует, и то что по идее должно было упрощать код, вырождается в помойку из аннотаций. Неоднократно такое видел. Вот есть стандартный бин, и над каждым полем пачка аннотаций из 10+ строк JPA, JSR-303, Jackson, JAXB, Lombok… Хочется их уже просто в отдельный файл убирать или типа того.
symbix
Я не джавист, так что вопрос может показаться глупым, извините.
А почему геттеры-сеттеры, а не public final? DTO ведь по своей сути просто иммутабельная структура.
DSolodukhin
Так сложилось исторически ru.wikipedia.org/wiki/JavaBeans
xpendence Автор
В принципе, Вы правы, суть DTO заключается в простой передаче объекта, но в Java всё-таки лучше придерживаться спецификации Java Beans. Для DTO стандартным будет набор "конструктор со всеми полями + геттеры".
xpendence Автор
Также, допустим, Model Mapper именно сетит поля, поэтому, при его использовании нужны ещё и сеттеры.
mikaakim
Хотя бы для того, чтобы можно было поменять значение поле и сохранить тот же объект в базу, а не создавать новый этой операции.
symbix
Сохранить DTO в базу? Зачем?