Spring Data的MongoTemplate和MongoRepository有什么区别?

136

我需要编写一个应用程序,使用spring-data和MongoDB可以进行复杂查询。我开始使用MongoRepository,但在查找示例或理解语法方面遇到了复杂查询的困难。

我说的是像这样的查询:

@Repository
public interface UserRepositoryInterface extends MongoRepository<User, String> {
    List<User> findByEmailOrLastName(String email, String lastName);
}

或者使用基于JSON的查询,我尝试了一些试错方法,因为我没有正确理解语法。即使在阅读了MongoDB文档之后(由于错误的语法而导致的非工作示例)。

@Repository
public interface UserRepositoryInterface extends MongoRepository<User, String> {
    @Query("'$or':[{'firstName':{'$regex':?0,'$options':'i'}},{'lastName':{'$regex':?0,'$options':'i'}}]")
    List<User> findByEmailOrFirstnameOrLastnameLike(String searchText);
} 

在阅读了所有文档后,看起来 mongoTemplateMongoRepository 更好地得到记录。 我是指以下文档:

http://static.springsource.org/spring-data/data-mongodb/docs/current/reference/html/

你能告诉我使用哪个更方便和强大吗?mongoTemplate还是MongoRepository?它们都一样成熟吗,还是其中一个缺少更多功能?
3个回答

177
“方便”和“易于使用”在某种程度上是矛盾的目标。仓库比模板更方便,但后者当然可以更精细地控制执行内容。
由于仓库编程模型适用于多个Spring Data模块,因此您将在Spring Data MongoDB 参考文档的一般部分中找到更详细的文档。
我们通常建议采用以下方法:
1. 从仓库抽象开始,只声明使用查询派生机制或手动定义的查询的简单查询。 2. 对于更复杂的查询,请向存储库添加手动实现的方法(如此处所述)。对于实现,请使用MongoTemplate
对于您的示例,这将类似于:
  1. Define an interface for your custom code:

    interface CustomUserRepository {
    
      List<User> yourCustomMethod();
    }
    
  2. Add an implementation for this class and follow the naming convention to make sure we can find the class.

    class UserRepositoryImpl implements CustomUserRepository {
    
      private final MongoOperations operations;
    
      @Autowired
      public UserRepositoryImpl(MongoOperations operations) {
    
        Assert.notNull(operations, "MongoOperations must not be null!");
        this.operations = operations;
      }
    
      public List<User> yourCustomMethod() {
        // custom implementation here
      }
    }
    
  3. Now let your base repository interface extend the custom one and the infrastructure will automatically use your custom implementation:

    interface UserRepository extends CrudRepository<User, Long>, CustomUserRepository {
    
    }
    
这种方式基本上给了你选择:所有易于声明的内容都存放在UserRepository中,而手动实现更好的内容则存放在CustomUserRepository中。自定义选项已经记录在这里

2
嗨,Oliver,这实际上不起作用。spring-data 尝试从自定义名称生成查询。yourCustomMethod()。它会说在域类中“your”不是有效字段。我按照手册并仔细检查了你在 spring-data-jpa-examples 中的做法。没有运气。只要我将自定义接口扩展到存储库类中,spring-data 就会尝试自动生成。唯一的区别是我正在使用 MongoRepository 而不是 CrudRepository,因为我现在不想使用迭代器。如果您有提示,将不胜感激。 - Christopher Armstrong
14
最常见的错误是将实现类的命名搞错了:如果你的基础存储库接口叫做 YourRepository,那么实现类必须被命名为 YourRepositoryImpl。你是这样做了吗?如果是的话,我可以在 GitHub 或类似网站上查看一个示例项目... - Oliver Drotbohm
5
嗨,Oliver,实现类的命名不正确,正如你所猜测的那样。我更改了名称,现在看起来它已经可以工作了。非常感谢你的反馈。能够以这种方式使用不同类型的查询选项真的很酷。这是一个经过深思熟虑的设计! - Christopher Armstrong
1
第二个实现类的命名有误:应该是 CustomUserRepository 而不是 CustomerUserRepository - Cotta
1
使用Spring Data Repository和MongoTemplate将创建2个到Mongo数据库服务器的连接。这是真的吗?这会对性能产生什么影响? - iamcrypticcoder
显示剩余8条评论

47

关于多线程环境下的更新:

  • MongoTemplate 提供了一些“原子”操作,如 updateFirstupdateMultifindAndModifyupsert 等等,可以让你在单个操作中修改文档。这些方法使用的 Update 对象还允许你仅针对相关字段进行修改。
  • MongoRepository 仅提供基本的 CRUD 操作,如 findinsertsavedelete,它们适用于包含所有字段的 POJOs。这迫使你要么分几步更新文档(1. 查找要更新的文档,2. 修改返回的 POJO 中的相关字段,然后 3. save 它),要么手动定义自己的更新查询使用 @Query

在一个多线程环境中,比如一个具有几个 REST 端点的 Java 后端,单个方法的更新是最好的选择,以减少两个并发更新互相覆盖对方更改的机会。

例如:给定这样一个文档:{ _id: "ID1", field1: "a string", field2: 10.0 } 和两个不同的线程并发更新它...

使用 MongoTemplate 可以像这样实现:

THREAD_001                                                      THREAD_002
|                                                               |
|update(query("ID1"), Update().set("field1", "another string")) |update(query("ID1"), Update().inc("field2", 5))
|                                                               |
|                                                               |

文档的最终状态始终是{ _id:“ID1”,field1:“another string”,field2:15.0 },因为每个线程仅访问DB一次仅更改指定的字段。

而使用MongoRepository的相同情况场景如下:

THREAD_001                                                      THREAD_002
|                                                               |
|pojo = findById("ID1")                                         |pojo = findById("ID1")
|pojo.setField1("another string") /* field2 still 10.0 */       |pojo.setField2(pojo.getField2()+5) /* field1 still "a string" */
|save(pojo)                                                     |save(pojo)
|                                                               |
|                                                               |

根据最后一个save操作命中数据库的不同,最终文档将是{ _id: "ID1", field1: "another string", field2: 10.0 }{ _id: "ID1", field1: "a string", field2: 15.0 }
(注意:即使我们按评论中建议使用@Version注释,情况也不会有太大变化:其中一个save操作会引发OptimisticLockingFailureException,最终文档仍然是上述之一,仅更新了一个字段而不是两个字段。)

所以我认为,除非您有非常复杂的POJO模型或出于某种原因需要MongoRepository的自定义查询功能,否则MongoTemplate是更好的选择。


好的观点/例子。然而,您提到的竞态条件示例和不良结果可以通过使用@Version来避免发生这种情况。 - Madbreaks
@Madbreaks,你能提供一些关于如何实现这个的资源吗?可能有官方文档吗? - Karthikeyan
Spring Data文档关于@Version注解:https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo-template.optimistic-locking - Karim Tawfik
2
@Madbreaks 感谢您指出这一点。是的,@Version将“避免”第二个线程覆盖第一个保存的数据——“避免”是指它会丢弃更新并抛出OptimisticLockingFailureException异常。因此,如果您想要更新成功,您必须实现重试机制。MongoTemplate允许您避免整个情况。 - walen
1
能够从不同的线程更新实体的部分,而不将实体作为一个整体考虑,从技术角度来看似乎很令人兴奋,但这是一种不良的实践,可能会带来更多问题而非答案。一些高并发要求的系统被设计成完全不进行更新,只进行插入操作。 - Vlad Barjovanu

33

这个回答可能有些晚,但我建议避免使用整个代码库的方法。你将得到很少有实际价值的已实现方法。为了让它工作,你会遇到Java配置上的麻烦,即使在文档中也没有太多帮助,你可能需要花费数天或几周时间。

相反,选择使用MongoTemplate路径并创建自己的数据访问层可以解救Spring程序员所面临的配置噩梦。由于有很大的灵活性,因此MongoTemplate真正成为了能够构建自己的类和交互的工程师的救星。结构可以是这样的:

  1. 创建一个MongoClientFactory类,它将在应用程序级别运行,并给你一个MongoClient对象。你可以将其实现为单例模式,也可以使用枚举单例模式(这是线程安全的)
  2. 从基类中创建一个数据访问基础类,您可以从中继承每个域对象的数据访问对象。基类可以实现一个创建MongoTemplate对象的方法,您的类特定的方法可以使用该方法进行所有DB访问
  3. 每个域对象的数据访问类可以实现基本方法,或者您可以在基类中实现它们
  4. 控制器方法随后可以根据需要调用数据访问类中的方法。

嗨@rameshpa,我可以在同一项目中同时使用MongoTemplate和repository吗?是否可行? - Gauranga
3
你可以这样做,但是你实现的MongoTemplate将与Repository使用的连接到数据库不同。原子性可能会成为一个问题。此外,如果您有排序需求,我不建议在一个线程上使用两个不同的连接。 - rameshpa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接