Skip to content

Commit f76e884

Browse files
committed
Part 22: Add UserService.java
1 parent 074390a commit f76e884

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package spring.oldboy.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.SneakyThrows;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.security.core.userdetails.UserDetails;
8+
import org.springframework.security.core.userdetails.UserDetailsService;
9+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
import org.springframework.util.StringUtils;
13+
import org.springframework.web.multipart.MultipartFile;
14+
import spring.oldboy.database.entity.User;
15+
import spring.oldboy.database.repository.user_repository.UserRepository;
16+
import spring.oldboy.dto.UserCreateEditDto;
17+
import spring.oldboy.dto.UserFilterDto;
18+
import spring.oldboy.dto.UserReadDto;
19+
import spring.oldboy.mapper.UserCreateEditMapper;
20+
import spring.oldboy.mapper.UserReadMapper;
21+
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Optional;
25+
26+
/*
27+
Используем Lombok аннотации для создания конструктора с final
28+
полями. Если бы у нас были не final поля, то мы воспользовались
29+
бы @AllArgsConstructor
30+
*/
31+
@Service
32+
@RequiredArgsConstructor
33+
/*
34+
Указываем на то, что методы нашего класса транзакционны, но переданный
35+
параметр 'readOnly = true' позволяет оптимизировать те запросы к БД,
36+
которые read-only, т.е. не вносят изменений в БД.
37+
*/
38+
@Transactional(readOnly = true)
39+
public class UserService implements UserDetailsService {
40+
41+
private final UserRepository userRepository;
42+
private final UserReadMapper userReadMapper;
43+
private final UserCreateEditMapper userCreateEditMapper;
44+
private final ImageService imageService;
45+
46+
public Page<UserReadDto> findAll(UserFilterDto filter, Pageable pageable) {
47+
48+
return userRepository.findAllByFilterAndPage(filter, pageable)
49+
.map(userReadMapper::map);
50+
}
51+
52+
/*
53+
Lesson 87 - Метод передает на уровень репозиториев критерии фильтрации
54+
наших User-ов и получив список отфильтрованных записей User-ов возвращает
55+
его (их) на уровень контроллеров в виде списка UserReadDto. Никакой
56+
'пагинации' - разбития на страницы тут не происходит.
57+
*/
58+
public List<UserReadDto> findAll(UserFilterDto filter) {
59+
return userRepository.findAllByFilter(filter).stream()
60+
.map(userReadMapper::map)
61+
.toList();
62+
}
63+
/*
64+
Нам нужен список всех User-ов в определенном
65+
виде, который определяется классом UserReadDto.
66+
*/
67+
public List<UserReadDto> findAll() {
68+
return userRepository.findAll().stream()
69+
.map(userReadMapper::map)
70+
.toList();
71+
}
72+
/*
73+
Lesson 108: Перенесем аннотацию с уровня контролеров сюда.
74+
75+
@PreAuthorize("hasAuthority('ADMIN')")
76+
*/
77+
public Optional<UserReadDto> findById(Long id) {
78+
/*
79+
Метод нашего UserRepository - Optional<T> findById(ID id) возвращает
80+
'кота Шреденгера', т.е. мы не знаем есть ли User под таким ID или нет,
81+
у объектов класса Optional есть свой метод *.map(), который принимает
82+
в качестве параметра маппер, в нашем случае это UserReadMapper.
83+
84+
По факту первый метод *.map() если значение присутствует, возвращает
85+
Optional параметр, описывающий (как будто с помощью ofNullable) результат
86+
применения данной функции сопоставления к значению, в противном случае
87+
возвращает пустой Optional параметр.
88+
89+
Если функция сопоставления возвращает нулевой результат, этот метод
90+
возвращает пустой Optional параметр.
91+
92+
Параметры: Mapper – функция сопоставления, применяемая к значению, если
93+
оно присутствует.
94+
95+
Возвращает: Optional параметр, описывающий результат применения функции
96+
сопоставления к значению этого Optional параметра, если значение
97+
присутствует, и в противном случае пустой Optional параметр.
98+
99+
Профессионал с опытом сразу поймет, что тут происходит, но новичку нужна
100+
более подробная раскладка:
101+
1. Нам нужно вернуть Optional<UserReadDto>. Optional - это 'коробка' в
102+
которой может быть, а может и не быть объект класса UserReadDto.
103+
2. Метод userRepository.findById(id) возвращает Optional<User>.
104+
3. Первый метод *.map() при текущем переданном в него параметре возвращает
105+
Optional<UserReadDto>, поскольку он выглядит как:
106+
public <U> Optional<U> map(Function<? super T, ? extends U> mapper), т.е.
107+
то что нам и нужно.
108+
4. Второй метод *.map() тот, что принимает объект User по ID возвращает либо
109+
UserReadDto, либо ничего не возвращает.
110+
В итоге в 'коробке' Optional либо будет лежать найденный UserReadDto, либо
111+
'коробка' будет пуста.
112+
*/
113+
return userRepository.findById(id)
114+
.map(object -> userReadMapper.map(object));
115+
}
116+
117+
/*
118+
Первые два запроса на чтение данных из БД, являются read-only,
119+
а вот текущий вносит изменения в данные и значит его нужно
120+
отдельно пометить как @Transactional без параметра. Тоже будет
121+
и с другими методами, которые вносят изменения в БД.
122+
*/
123+
@Transactional
124+
/*
125+
Тут мы создаем сущность и должны вернуть именно нечто конкретное,
126+
например UserReadDto или пробросить ошибку если таковая возникла.
127+
*/
128+
public UserReadDto create(UserCreateEditDto userDto) {
129+
return Optional.of(userDto)
130+
/*
131+
Как и ранее, полученный userDto нам нужно преобразовать в User, а
132+
это отдельный маппер, у нас это UserCreateEditMapper. Но, так же
133+
нам нужно загрузить (в 'профиль') в соответствующее поле записи
134+
картинку-аватарку для конкретного user-a если она была передана в
135+
форме регистрации.
136+
*/
137+
.map(dto -> {
138+
uploadImage(dto.getImage());
139+
return userCreateEditMapper.map(dto);
140+
})
141+
/* Сохраняем полученную сущность */
142+
.map(userRepository::saveAndFlush)
143+
/* Из сохраненной сущности снова получаем UserReadDto */
144+
.map(userReadMapper::map)
145+
/*
146+
Если что-то пошло не так, на любом из этапов бросаем
147+
исключение и отлавливаем ее на уровне контроллеров
148+
*/
149+
.orElseThrow();
150+
}
151+
152+
@Transactional
153+
public Optional<UserReadDto> update(Long id, UserCreateEditDto userDto) {
154+
/*
155+
Поскольку мы работаем с конкретным user-ом определенным по
156+
ID, посему мы пытаемся его получить с уровня DAO/Repository
157+
*/
158+
return userRepository.findById(id)
159+
/* Если данные получены вносим изменения */
160+
.map(entity -> {
161+
uploadImage(userDto.getImage());
162+
return userCreateEditMapper.map(userDto, entity);
163+
})
164+
/* Сохраняем изменения и фиксируем их */
165+
.map(userRepository::saveAndFlush)
166+
/* Возвращаем получившееся UserReadDto */
167+
.map(object -> userReadMapper.map(object));
168+
}
169+
170+
/* В случае с удалением мы захотим узнать успешно ли прошла операция, отсюда boolean */
171+
@Transactional
172+
public boolean delete(Long id) {
173+
/* И снова, поиск user по ID, в случае успеха удаляем и возвертаем true */
174+
return userRepository.findById(id)
175+
.map(entity -> {
176+
/* Метод из интерфейса CrudRepository */
177+
userRepository.delete(entity);
178+
/*
179+
Метод из интерфейса JpaRepository. Здесь необходим, чтобы тесты на данный метод отработали
180+
действительно корректно, поскольку при тестировании обычно происходит rollback, а не commit
181+
тестируемых операций. Т.е. если данный метод применять 'в бою' он отработает нормально, но
182+
вот для тестов, именно тут, мы явно прописываем *.flush()
183+
*/
184+
userRepository.flush();
185+
/*
186+
Недостаток метода *.delete() в том, что он возвращает void,
187+
а нам нужно булева переменная, поэтому возвращаем ее сами.
188+
*/
189+
return true;
190+
})
191+
/* В случае неудачи возвертаем false */
192+
.orElse(false);
193+
}
194+
195+
/* Обслуживающий метод, для загрузки изображения */
196+
@SneakyThrows
197+
private void uploadImage(MultipartFile image) {
198+
/*
199+
Если поле image не пустое, т.е. пользователь приложения пытается загрузить картинку
200+
через форму, то идет обращение к классу ImageService и через его метод *.upload()
201+
записывается в заранее определенное в ImageService место, в нашем случае, на
202+
локальной машине. Естественно нет аватарки - нет записи/перезаписи.
203+
*/
204+
if (!image.isEmpty()) {
205+
imageService.uploadAvatar(image.getOriginalFilename(), image.getInputStream());
206+
}
207+
}
208+
209+
/* Метод извлекающий аватарку из БД. Для наглядности разбит на шаги. */
210+
public Optional<byte[]> findAvatar(Long id) {
211+
/*
212+
Сначала находим user-a по ID, если такой user
213+
есть, в худшем случае дальше может пойти null
214+
завернутый в Optional.
215+
*/
216+
Optional<User> step_1 = userRepository.findById(id);
217+
/*
218+
Теперь достаем у user-a название аватарки, если она у
219+
user-a есть, и если user таки есть, в худшем случае
220+
дальше пойдет null завернутый в Optional.
221+
*/
222+
Optional<String> step_2 = step_1.map(user -> user.getImage());
223+
/*
224+
Проверяем есть ли текстовая ссылка на аватарку, если есть - true
225+
и обернутый в Optional String ссылки идет дальше, в худшем случае,
226+
дальше пойдет Optional.empty
227+
*/
228+
Optional<String> step_3 = step_2.filter(imagePath -> StringUtils.hasText(imagePath));
229+
/*
230+
Разница между map() и flatMap() в том, что map() изменяет только "распакованные"
231+
или простые-не-optional, значения "упакованных" в Optional контейнер объектов, а
232+
flatMap() перед изменением самостоятельно их "распаковывает", т.е. если у нас,
233+
например Optional<Optional<String>>, и проводит манипуляции, либо возвращает -
234+
Optional.empty.
235+
*/
236+
Optional<byte[]> final_step = step_3.flatMap(imagePath -> imageService.getAvatar(imagePath));
237+
return final_step;
238+
}
239+
240+
@Override
241+
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
242+
/* Находим user-a по имени */
243+
return userRepository.findByUsername(username)
244+
/*
245+
А вот тут, чуть хитрее, т.к. мы будем использовать
246+
Spring реализацию класса User, а не нашу.
247+
*/
248+
.map(user -> new org.springframework.security.core.userdetails.User(
249+
user.getUsername(),
250+
user.getPassword(),
251+
Collections.singleton(user.getRole())
252+
))
253+
.orElseThrow(() -> new UsernameNotFoundException("Failed to retrieve user: " + username));
254+
}
255+
256+
}

0 commit comments

Comments
 (0)