Фреймворки обещают ускорить темпы развития человека при условии, что он следует по основному пути. Путь может быть более или менее узким. Я большой поклонник экосистемы Spring, потому что ее дизайн расширяем и настраиваем на разных уровнях абстракции: таким образом, путь настолько велик, насколько вам нужно.
Функциональное программирование становится все более популярным. Spring предоставляет пару DSL для языка Kotlin. Например, DSL Beans и DSL Маршрутов обеспечивают более функциональный подход к конфигурации Spring. На стороне типа , Vavr (ранее Javaslang) довольно популярен в Java, в то время как в Kotlin есть Стрелка .
В этом посте я хотел бы описать, как можно использовать систему типов Arrow с данными Spring. В конечном счете, вы можете воспользоваться объяснениями для создания собственного хранилища данных Spring.
Исходная архитектура
Начальная архитектура для моего приложения довольно стандартная:
- Контроллер ОТДЫХА с двумя
GET
сопоставлениями - Интерфейс хранилища JDBC данных Spring Data
Поскольку это стандартно, Spring обрабатывает большую часть сантехники, и нам не нужно писать много кода. С Kotlin это еще более лаконично:
class Person(@Id val id: Long, var name: String, var birthdate: LocalDate?) interface PersonRepository : CrudRepository@RestController class PersonController(private val repository: PersonRepository) { @GetMapping fun getAll(): Iterable = repository.findAll() @GetMapping("/{id}") fun getOne(@PathVariable id: Long) = repository.findById(id) } @SpringBootApplication class SpringDataArrowApplication fun main(args: Array ) { runApplication (*args) }
К более функциональному подходу
Этот шаг не имеет ничего общего с данными Spring и не требуется, но он лучше соответствует функциональному подходу. Как упоминалось выше, мы можем извлечь выгоду из использования DSL маршрутов и компонентов. Давайте проведем рефакторинг приведенного выше кода, чтобы максимально удалить аннотации.
class PersonHandler(private val repository: PersonRepository) { // 1 fun getAll(req: ServerRequest) = ServerResponse.ok().body(repository.findAll()) // 2 fun getOne(req: ServerRequest): ServerResponse = repository .findById(req.pathVariable("id").toLong()) .map { ServerResponse.ok().body(it) } .orElse(ServerResponse.notFound().build()) // 3 } fun beans() = beans { // 4 bean() bean { val handler = ref () // 5 router { GET("/", handler::getAll) GET("/{id}", handler::getOne) } } } fun main(args: Array ) { runApplication (*args) { addInitializers(beans()) // 6 } }
- Создайте класс обработчика для организации функций маршрутизации
- Все функции маршрутизации должны принимать
Сервер запрашивает
параметр и возвращаетОтвет сервера
- Добавьте дополнительную возможность: если объект не найден, верните 404
- Используйте DSL маршрутов для сопоставления HTTP-глаголов и пути к функциям маршрутизации
ref()
извлекает компонент с настроенным типом из контекста приложения Spring- Явный вызов функции
beans()
, больше никакой магии!
Вводящая стрелка
Функциональный компаньон стандартной библиотеки Kotlin
— Стрела
Стрелка поставляется с четырьмя различными компонентами:
- Ядро
- FX: Фреймворк функциональных эффектов, дополняющий сопрограммы KotlinX
- Оптика: Глубокий доступ и преобразования к неизменяемым данным
- Мета: Библиотека метапрограммирования для компилятора Kotlin плагины
Основная библиотека предлагает Либо
введите. Стрелка советует использовать Либо<Единица измерения,T>
для моделирования необязательного значения. С другой стороны, Spring Data JDBC findById()
возвращает java.util. Необязательно
.
Преодоление разрыва
Как нам преодолеть разрыв между Необязательным
и Либо
?
Вот первая попытка:
repository .findById(req.pathVariable("id").toLong()) // 1 .map { Either.fromNullable(it) } // 2 .map { either -> either.fold( { ServerResponse.notFound().build() }, // 3 { ServerResponse.ok().body(it) } // 3 ) }.get() // 4
Необязательно<Человек>
Необязательно<Либо<Единица измерения, Человек>>
Необязательно<Ответ сервера>
- На этом этапе мы можем безопасно вызвать
get()
, чтобы получитьОтвет сервера
Я считаю, что использование Необязательно<Либо<Единица измерения, человек>>
не очень хорошо. Однако Kotlin может помочь нам в этом отношении с помощью функций расширения:
private funOptional .toEither() = if (isPresent) Either.right(get()) else Unit.left()
С помощью этой функции мы можем улучшить существующий код:
repository .findById(req.pathVariable("id").toLong()) // 1 .toEither() // 2 .fold( { ServerResponse.notFound().build() }, // 3 { ServerResponse.ok().body(it) } // 3 )
-
Необязательно<Человек>
-
Либо<Единица Измерения, Человек>
-
Ответ сервера
Так выглядит приятнее, но было бы намного лучше, если бы репозиторий возвращал Либо<Единица измерения, Человек>
напрямую.
Настройка весенних данных
Давайте проверим, как мы можем настроить данные Spring для достижения этой цели.
По умолчанию хранилище данных Spring предлагает все общие функции, которые вы можете ожидать, .например. :
Я считаю, что один приходит к Spring Data для простоты использования, но этот остается из-за его возможностей расширения.
На базовом уровне можно добавлять функции, которые следуют определенному шаблону именования, например , Порядок Поиска первого имени И последнего Имени По Фамилии()
. Spring Data сгенерирует код реализации без необходимости написания одной строки SQL. Когда вы достигнете пределов этого подхода, вы можете аннотировать функцию с помощью SQL, который вы хотите запустить.
В обоих случаях вам необходимо задать тип возвращаемого значения. Хотя количество возможных типов возврата довольно велико, оно все еще ограничено. Фреймворк не может учитывать все возможные типы, и, в частности, список не содержит Либо
.
Следующий уровень расширяемости заключается в добавлении любой функции с желаемой подписью с помощью пользовательской реализации. Для этого нам нужно:
- Интерфейс, который объявляет требуемую функцию
- Класс, реализующий интерфейс
interface CustomPersonRepository { // 1 fun arrowFindById(id: Long): Either// 2 } class CustomPersonRepositoryImpl(private val ops: JdbcAggregateOperations) // 3 : CustomPersonRepository { // 4 override fun arrowFindById(id: Long) = Either.fromNullable(ops.findById(id, Person::class.java)) // 5 } interface PersonRepository : CrudRepository , CustomPersonRepository // 6
- Новый пользовательский интерфейс
- Объявить требуемую функцию
- Новый класс реализации…
- … который реализует родительский интерфейс
- Реализовать функцию
- Просто расширьте пользовательский интерфейс
Теперь мы можем позвонить:
repository.arrowFindById(req.pathVariable("id").toLong()) .fold( { ServerResponse.notFound().build() }, { ServerResponse.ok().body(it) } )
Этот подход работает, но имеет один существенный недостаток. Чтобы избежать столкновения в сигнатуре функций, мы должны придумать оригинальное имя для нашей функции, которая возвращает Либо
т.е. стрелка findById()
.
Изменение базового репозитория по умолчанию
Чтобы преодолеть это ограничение, мы можем использовать еще один пункт расширения: изменить базовый репозиторий по умолчанию.
Приложения Spring Data определяют интерфейсы, но реализация должна откуда-то исходить. Фреймворк предоставляет его по умолчанию, но его можно переключить с помощью нашего собственного.
Вот обзор диаграммы классов:
Подробный процесс довольно сложен: важной частью является Простой репозиторий Jdbc
класс. Spring Data найдет класс через компонент Jdbc RepositoryFactoryBean
, создаст его новый экземпляр и зарегистрирует экземпляр в контексте.
Давайте создадим базовый репозиторий, который использует Либо
:
@NoRepositoryBean interface ArrowRepository: Repository { // 1 fun findById(id: Long): Either // 2 fun findAll(): Iterable // 3 } class SimpleArrowRepository ( // 4 private val ops: JdbcAggregateOperations, private val entity: PersistentEntity ) : ArrowRepository { override fun findById(id: Long) = Either.fromNullable( ops.findById(id, entity.type) // 5 ) override fun findAll(): Iterable = ops.findAll(entity.type) }
- Наш новый репозиторий интерфейсов…
- …с подписью, которую мы выбираем, без какого-либо риска столкновения.
- Мне было слишком лень все реализовывать.
- Базовая реализация интерфейса репозитория. Конструктор должен принять эти два параметра.
- Не изобретайте велосипед заново; используйте существующий экземпляр
Jdbc Aggregate Operations
.
Нам нужно аннотировать основной класс приложения с помощью @EnableJdbcRepositories
и настроить последний для переключения на этот базовый класс.
@SpringBootApplication @EnableJdbcRepositories(repositoryBaseClass = SimpleArrowRepository::class) class SpringDataArrowApplication
Чтобы упростить использование клиентского кода, мы можем создать аннотацию, которая переопределяет значение по умолчанию:
@EnableJdbcRepositories(repositoryBaseClass = SimpleArrowRepository::class) annotation class EnableArrowRepositories
Теперь использование простое:
@SpringBootApplication @EnableArrowRepositories class SpringDataArrowApplication
На этом этапе мы можем переместить код репозитория Arrow в его проект и распространить его для использования другими “клиентскими” проектами. Дальнейшее расширение не требуется, хотя Spring Data предлагает гораздо больше, например, , переключение заводского компонента.
Вывод
Spring Data предоставляет готовую к использованию реализацию репозитория “из коробки”. Когда этого недостаточно, его гибкий дизайн позволяет расширять код на разных уровнях абстракции.
В этом посте показано, как заменить базовый репозиторий по умолчанию нашим собственным, в котором используется тип стрелки в подписи функции.
Спасибо Марку Палучу за его отзыв.
Полный исходный код этого поста можно найти на Github в формате Maven.
Идти дальше:
- Работа с хранилищами данных Spring
- Пользовательские реализации для хранилищ данных Spring
- Настройка базового репозитория
Первоначально опубликовано на Фанат Java 11 апреля th , 2021
Оригинал: “https://dev.to/nfrankel/your-own-custom-spring-data-repository-140g”