С нуля реализовать CRUD-приложение для двух независимых сущностей (User, Dish) с использованием подхода Onion/Clean Architecture, а также двух источников данных:
- Mock-репозитории (in-memory).
- Репозитории на
Spring Data JPA+H2.
Важно: связи между сущностями в этой работе не делаем (они вынесены в ЛР-4).
Ссылку на PR в ваш репозиторий (созданный через этот шаблон).
H2 — это легковесная Java СУБД. Она может работать как in-memory или как файловая БД.
В этой работе используем файловый режим, чтобы данные сохранялись между перезапусками приложения.
Плюсы для ЛР:
- Быстрый старт.
- Простая интеграция со Spring Boot.
- Удобна для автотестов и локальной отладки.
Зависимость в pom.xml:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Обычно вместе с ней в проекте уже должны быть:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Пример конфигурации application.yml (файловая H2):
spring:
datasource:
url: jdbc:h2:file:./data/lab3db;MODE=PostgreSQL;AUTO_SERVER=TRUE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
path: /h2-consoleSpring Data JPA позволяет быстро реализовывать доступ к данным через интерфейсы репозиториев.
Spring сам генерирует SQL по имени метода.
interface UserJpaRepository : JpaRepository<UserEntity, Long> {
fun findByEmail(email: String): UserEntity?
fun existsByEmail(email: String): Boolean
fun findAllByActiveTrue(): List<UserEntity>
}Используется, когда нужна более гибкая выборка.
interface DishJpaRepository : JpaRepository<DishEntity, Long> {
@Query(
"""
select d
from DishEntity d
where d.isAvailable = true
and d.price <= :maxPrice
order by d.price asc
"""
)
fun findAvailableCheaperThan(maxPrice: BigDecimal): List<DishEntity>
}Это демонстрационный пример JPQL.
В решении ориентируйтесь на контракт из spec.yaml, а не копируйте этот фрагмент один в один.
Когда у нас есть mock и db, бизнес-логика не должна зависеть от конкретного источника.
Решение: зависимости направляем к абстракциям (портам).
interface UserRepositoryPort {
fun create(user: User): User
fun findById(id: Long): User?
// ...
}class UserMockRepository : UserRepositoryPort {
private val storage = mutableMapOf<Long, User>()
private var seq = 1L
override fun create(user: User): User {
val saved = user.copy(id = seq++)
storage[saved.id] = saved
return saved
}
// ...
}class UserJpaAdapter(
private val userJpaRepository: UserJpaRepository
) : UserRepositoryPort {
override fun create(user: User): User =
userJpaRepository.save(UserEntity.fromDomain(user)).toDomain()
// ...
}@Service
class UserService(
private val userRepositoryPort: UserRepositoryPort
) {
fun execute(cmd: CreateUserCommand): User {
return userRepositoryPort.create(
User(
id = 0,
email = cmd.email,
firstName = cmd.firstName,
lastName = cmd.lastName,
isActive = true
)
)
}
}Переключение можно сделать:
- Через
@Profile("mock")/@Profile("db"). - Через конфигурационное свойство
app.data-provider=mock|db.
Главный принцип: меняется только адаптер, service остается неизменным.
Сущности должны быть независимыми, без связей.
id: Longemail: StringfirstName: StringlastName: StringisActive: Boolean
id: Longname: Stringdescription: Stringprice: BigDecimalisAvailable: Boolean
Нужно реализовать ровно те endpoint'ы и контракты, которые описаны в спецификации (методы, URL, параметры, коды ответов, схемы JSON).
Кратко, что есть в спецификации:
- CRUD для
users. - CRUD для
dishes. - Для
GET /api/v1/dishesподдержка опционального фильтраnamePart(для JPQL-выборки).
- Создайте порты (
UserRepositoryPort,DishRepositoryPort) в domain/application слое. - Реализуйте mock-адаптеры (in-memory) для обоих портов.
- Поднимите API целиком на mock-реализации и проверьте CRUD.
- Подключите
H2и настройте datasource. - Создайте
Entity-классы и Spring Data репозитории. - Реализуйте JPA-адаптеры, которые реализуют те же порты.
- Переключите приложение на JPA-репозитории.
User-репозиторий: использовать derived methods.Dish-репозиторий: добавить минимум один JPQL@Queryметод.
Приложение должно быть организовано по Onion/Clean Architecture:
- HTTP-контроллеры не зависят от JPA-сущностей напрямую.
- Service-слой не зависит от Spring Data.
- Инфраструктура подключается как адаптеры к портам.
| Категория | Критерий | Баллы |
|---|---|---|
| Штраф | Не проходят автотесты | -7 |
| Архитектура | Реализована Onion/Clean структура: порты, services, адаптеры | 4 |
| CRUD: User | Полный CRUD для User работает корректно |
2 |
| CRUD: Dish | Полный CRUD для Dish работает корректно |
2 |
| Mock-слой | Есть рабочие mock-репозитории для двух сущностей | 2 |
| JPA-слой | Реализованы JPA-адаптеры и работа с H2 | 2 |
| Spring Data подходы | Один репозиторий на derived methods, второй с JPQL @Query |
2 |
| Качество решения | Чистота кода, структура пакетов, читаемость | 1 |
| Итого | 15 |
- Проект запускается локально.
- В коде есть и mock, и JPA реализации репозиториев.
- Для
Dishесть минимум один JPQL запрос. - CRUD для
UserиDishработает через HTTP.