В новом релизе Spring Data 2024.1 улучшена возможность добавления кастомной функциональности, что значительно упростило создание переиспользуемых экстеншенов.
В новом переводе от команды Spring АйО вы узнаете, что теперь можно разрабатывать универсальные расширения, которые подходят для множества проектов, не привязываясь к конкретному доменному типу.
С самого начала Spring Data репозитории проектировались так, чтобы их можно было легко расширять — будь то настройка одного метода запроса или создание новой базовой имплементации.
В релизе 2024.1 возможности по добавлению кастомной функциональности в репозитории были значительно улучшены, что сделало процесс создания экстеншенов, пригодных для использования в различных проектах, проще, чем когда-либо.
Давайте рассмотрим пример, чтобы увидеть, как это работает на практике.
Представьте, что вы используете MongoDB для управления базой данных фильмов. Вы хотите внедрить функцию векторного поиска MongoDB Atlas через интерфейсы репозиториев для выполнения операций поиска. Обычно для этого создаётся фрагмент кастомного репозитория:
package io.movie.db;
interface AtlasMovieRepository {
List<Movie> vectorSearch(String index, String path, List<Double> vector, Limit limit);
}
Комментарий от команды Spring АйО
Под фрагментом имеется виду Custom Repository Implementations (https://docs.spring.io/spring-data/jpa/reference/repositories/custom-implementations.html)
Здесь, поскольку вы работаете с типом Movie
, коллекция уже известна. Параметр index
указывает векторный индекс для использования, а path
определяет поле, содержащее векторные embedding’и для сравнения. Функция схожести (например, евклидова, косинусная или скалярное произведение) задается при настройке индекса. Предположим, что мы работаем с косинусным векторным индексом.
В вашей реализации фрагмента вам потребуется создать стадию агрегации $vectorSearch
, которая является подходом для MongoDB к выполнению векторных поисков, и интегрировать её в API агрегации с использованием MongoOperations
:
package io.movie.db;
class AtlasMovieRepositoryFragment implements AtlasMovieRepository {
private final MongoOperations mongoOperations;
public AtlasMovieRepositoryFragment(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
@Override
public List<Movie> vectorSearch(String index, String path, List<Double> vector, Limit limit) {
Document $vectorSearch = createSearchDocument(index, path, vector, limit);
Aggregation aggregation = Aggregation.newAggregation(ctx -> $vectorSearch);
return mongoOperations.aggregate(aggregation, "movies", Movie.class).getMappedResults();
}
private static Document createSearchDocument(String index, String path, List<Double> vector, Limit limit) {
Document $vectorSearch = new Document();
$vectorSearch.append("index", index);
$vectorSearch.append("path", path);
$vectorSearch.append("queryVector", vector);
$vectorSearch.append("limit", limit.max());
return new Document("$vectorSearch", $vectorSearch);
}
}
Теперь просто интегрируйте фрагмент в ваш MovieRepository
:
package io.movie.db;
interface MovieRepository extends CrudRepository<Movie, String>, AtlasMovieRepository { }
Хотя этот подход работает, вы можете заметить, что он тесно связан с одним репозиторием, имеющим определённый доменный тип (Movie
). Это затрудняет повторное использование в других проектах, так как реализации фрагментов привязаны к пакету репозитория и специфичны для домена.
Однако векторный поиск не ограничивается только нашей базой данных фильмов. Что если мы захотим использовать эту функциональность в других проектах без копирования и изменения решения? Давайте рассмотрим способ сделать это более универсальным.
Сделаем это переиспользуемым
Чтобы обеспечить возможность повторного использования, мы переносим AtlasMovieRepository
и его реализацию в отдельный проект, чтобы их можно было использовать повторно. Затем мы регистрируем фрагмент в файле META-INF/spring.factories
, чтобы Spring Data узнал о расширении:
api.mongodb.atlas.AtlasMovieRepository=api.mongodb.atlas.AtlasMovieRepositoryFragment
Комментарий от команды Spring АйО
Несмотря на то, что автоконфигурация через spring.factories
является deprecated, этот файл (spring.factories
) до сих пор используют разные модули Spring'а для других фич, не связанных с автоконфигурацией
Однако текущая реализация всё ещё привязана к типу Movie
, что ограничивает её переиспользуемость. Чтобы исправить это, нужно сделать фрагмент более универсальным. Переименуйте AtlasMovieRepository
в AtlasRepository
и добавьте generic тип в параметры. Не забудьте также обновить файл spring.factories
.
package api.mongodb.atlas;
interface AtlasRepository<T> {
List<T> vectorSearch(String index, String path, List<Double> vector, Limit limit);
}
Далее мы обновляем реализацию, чтобы она соответствовала новому generic подходу, так как теперь нельзя предполагать, что мы работаем с коллекцией Movie
. С использованием недавно добавленного RepositoryMethodContext
мы можем получить доступ к метаданным репозитория и динамически определить соответствующее имя коллекции:
package api.mongodb.atlas;
class AtlasRepositoryFragment<T> implements AtlasRepository<T>, RepositoryMetadataAccess {
private MongoOperations mongoOperations;
public AtlasRepositoryFragment(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
@Override
public List<T> vectorSearch(String index, String path, List<Double> vector, Limit limit) {
RepositoryMethodContext methodContext = RepositoryMethodContext.getContext();
Class<?> domainType = methodContext.getMetadata().getDomainType();
Document $vectorSearch = createSearchDocument(index, path, vector, limit);
Aggregation aggregation = Aggregation.newAggregation(ctx -> $vectorSearch);
return (List<T>) mongoOperations.aggregate(aggregation, mongoOperations.getCollectionName(domainType), domainType).getMappedResults();
}
private static Document createSearchDocument(String indexName, String path, List<Double> vector, Limit limit) {
Document $vectorSearch = new Document();
//…
}
}
Предоставленный RepositoryMethodContext
позволяет не только получить общую информацию о репозитории, но также предоставляет доступ к generic’ам, методам и другим аспектам репозиториев. В приведённом выше примере предполагается, что доменный тип репозитория совпадает с нашим кастомным фрагментом, что может быть не так. Поэтому вместо этого мы могли бы определить тип компонента интерфейса с помощью ResolvableType.forClass(getRepositoryInterface()).as(AtlasRepository.class).getGeneric(0)
или даже проверить возвращаемый тип текущего метода для выполнения дополнительных манипуляций, таких как проекции и другие. Для упрощения в этом примере мы будем придерживаться доменного типа.
Чтобы избежать излишней нагрузки, мы включаем доступ к контексту только для тех репозиториев, которым это необходимо. Если внимательно посмотреть на приведённый выше код, вы заметите дополнительный интерфейс RepositoryMetadataAccess
в классе AtlasRepositoryFragment
. Этот маркерный интерфейс указывает инфраструктуре предоставлять необходимые метаданные при вызове метода.
С такой настройкой вы можете использовать кастомный экстеншн в любом проекте, просто добавив его в репозиторий:
package io.movie.db;
interface MovieRepository extends CrudRepository<Movie, String>, AtlasRepository<Movie> { }
Чтобы опробовать это, посетите проект Spring Data Examples, где вы найдёте готовый к запуску код.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.