扩展 Spring Data Repositories 变得更容易

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

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

2024.1 版本增强了您扩展 repository 的能力,通过 自定义功能,让任何人都可以轻松创建可在不同项目中共享的扩展。

让我们通过一个例子来了解它在实践中是如何运作的。

假设您使用 MongoDB 作为文档存储来管理电影数据库。 您希望通过您的 repository 接口利用 MongoDB Atlas 的 向量搜索 功能来实现 AI 驱动的搜索操作。 通常,您会创建像这样的自定义 repository 片段

package io.movie.db;

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

在这里,由于您正在使用 Movie 类型,因此您已经知道集合。 index 参数指定要使用的向量索引,path 定义保存用于比较的 向量嵌入 的字段。 相似度函数(例如,欧几里得、余弦或点积)在您设置索引时确定。 让我们假设已经有一个余弦向量索引。

在您的片段实现中,您需要创建 $vectorSearch 聚合阶段,这是 MongoDB 运行向量搜索的方法,并使用 MongoOperations 将其集成到 Aggregation 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) 的单个 repository 紧密耦合。 这使得它难以在其他项目中重用,因为片段实现与 repository 的包相关,并且是特定于域的。

但是向量搜索并不局限于我们的电影数据库。 如果我们想在其他项目中重用此功能,而无需复制和修改解决方案该怎么办? 让我们探索一种使其更通用的方法。

使其可重用

为了实现重用,我们将 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,我们可以访问 repository 元数据并动态确定适当的集合名称

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();
       //…
   }
}

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

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

通过该设置,您现在可以通过简单地扩展您的 repository 在任何项目中使用自定义扩展

package io.movie.db;

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

要试用它,请访问 Spring Data Examples 项目,您将在其中找到准备运行的代码。

获取 Spring 新闻资讯

订阅 Spring 新闻资讯,保持联系

订阅

抢先一步

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

了解更多

获得支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

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

查看所有