扩展 Spring Data Repositories 变得更容易了

工程 | Christoph Strobl | 2024年12月3日 | ...

自诞生以来,Spring Data Repositories 就被设计为可扩展的,无论您是想自定义单个查询方法,还是提供一个全新的基础实现。

2024.1 版本通过自定义功能增强了您扩展仓库的能力,使任何人都可以比以往更轻松地创建可在不同项目之间共享的扩展。

让我们通过一个示例来了解这在实践中是如何工作的。

假设您正在使用 MongoDB 作为文档存储来管理电影数据库。您希望通过您的仓库接口利用 MongoDB Atlas 向量搜索 功能,以实现 AI 驱动的搜索操作。通常,您会创建一个自定义仓库片段,如下所示:

package io.movie.db;

interface AtlasMovieRepository {
   List<Movie> vectorSearch(String index, String path, List<Double> vector, Limit limit);
}

在这里,由于您正在处理 Movie 类型,因此您已经知道集合。索引参数指定要使用的向量索引,path 定义了用于比较的 向量嵌入 字段。相似性函数(例如,欧几里得、余弦或点积)在设置索引时确定。假设余弦向量索引已经到位。

在您的片段实现中,您需要创建 $vectorSearch 聚合阶段,这是 MongoDB 运行向量搜索的方法,并使用 MongoOperations 将其集成到聚合 API 中。

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

然而,当前的实现仍然绑定到 Movie 类型,限制了其可重用性。为了解决这个问题,我们需要使片段更通用。将 AtlasMovieRepository 重命名为 AtlasRepository 并引入一个泛型类型参数。别忘了更新 spring.factories 文件。

package api.mongodb.atlas;

interface AtlasRepository<T> {
   List<T> vectorSearch(String index, String path, List<Double> vector, Limit limit);
}

接下来,我们更新实现以反映新的泛型方法,因为我们不能再假设我们正在针对 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();
       //…
   }
}

所提供的方法上下文不仅允许您访问有关仓库的常规信息,还允许您访问仓库的泛型、方法等。在上面的代码片段中,我们假设仓库领域类型与我们的自定义片段对齐,但这不一定是这种情况。因此,我们可以通过 ResolvableType.forClass(getRepositoryInterface()).as(AtlasRepository.class).getGeneric(0) 读取接口的组件类型,甚至检查当前方法的返回类型以应用额外的操作,如投影等。为简单起见,在此示例中我们坚持使用领域类型。

为了避免不必要的开销,我们只为需要上下文访问的仓库启用上下文访问。仔细查看上面的代码,您会发现 AtlasRepositoryFragment 类上有一个额外的 RepositoryMetadataAccess 接口。这个标记接口建议基础设施在方法调用时提供所需的元数据。

通过此设置,您现在可以通过简单地扩展您的仓库来在任何项目中使用自定义扩展。

package io.movie.db;

interface MovieRepository extends CrudRepository<Movie, String>, AtlasRepository<Movie> { }

要试用,请访问 Spring Data 示例 项目,您将找到可运行的代码。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有