问题描述
在我的项目中,我使用以下方法从数据库中查询数据:
In my project I am using the following approach to querying data from the database:
- 使用可以返回任何类型且不绑定到一种类型的通用存储库,即
IRepository.Get
而不是IRepository.Get
.NHibernatesISession
就是这样一个存储库的例子. 在带有特定
T
的IQueryable
上使用扩展方法来封装重复查询,例如
- Use a generic repository that can return any type and is not bound to one type, i.e.
IRepository.Get<T>
instead ofIRepository<T>.Get
. NHibernatesISession
is an example of such a repository. Use extension methods on
IQueryable<T>
with a specificT
to encapsulate recurring queries, e.g.
public static IQueryable<Invoice> ByInvoiceType(this IQueryable<Invoice> q,
InvoiceType invoiceType)
{
return q.Where(x => x.InvoiceType == invoiceType);
}
用法如下:
var result = session.Query<Invoice>().ByInvoiceType(InvoiceType.NormalInvoice);
现在假设我有一个使用此查询的公共方法要测试.我想测试三种可能的情况:
Now assume I have a public method I want to test that uses this query. I want to test the three possible cases:
- 查询返回 0 张发票
- 查询返回 1 张发票
- 查询返回多张发票
我现在的问题是:要模拟什么?
- 我不能模拟
ByInvoiceType
因为它是一个扩展方法,或者我可以吗? - 出于同样的原因,我什至无法模拟
Query
.
- I can't mock
ByInvoiceType
because it is an extension method, or can I? - I can't even mock
Query
for the same reason.
推荐答案
经过更多研究并基于此处和 这些 链接,我决定完全重新设计我的 API.
After some more research and based on the answers here and on these links, I decided to completely re-design my API.
基本概念是在业务代码中完全禁止自定义查询.这解决了两个问题:
The basic concept is to completely disallow custom queries in the business code. This solves two problems:
- 提高了可测试性
- Mark 的博文中概述的问题不再发生.业务层不再需要关于正在使用的数据存储的隐式知识来了解
IQueryable
上允许哪些操作,哪些不允许.
- The testability is improved
- The problems outlined in Mark's blog post can no longer happen. The business layer no longer needs implicit knowledge about the datastore being used to know which operations are allowed on the
IQueryable<T>
and which are not.
在业务代码中,查询现在如下所示:
In the business code, a query now looks like this:
IEnumerable<Invoice> inv = repository.Query
.Invoices.ThatAre
.Started()
.Unfinished()
.And.WithoutError();
// or
IEnumerable<Invoice> inv = repository.Query.Invoices.ThatAre.Started();
// or
Invoice inv = repository.Query.Invoices.ByInvoiceNumber(invoiceNumber);
实际上是这样实现的:
正如 Vytautas Mackonis 在 他的回答中所建议的,我不再直接依赖于 NHibernate 的 ISession
,相反,我现在依赖于 IRepository
.
As Vytautas Mackonis suggested in his answer, I am no longer depending directly on NHibernate's ISession
, instead I am now depending on an IRepository
.
这个接口有一个名为 Query
的属性,类型为 IQueries
.对于业务层需要查询的每个实体,IQeries
中有一个属性.每个属性都有自己的接口,用于定义实体的查询.每个查询接口都实现了通用的 IQuery
接口,后者又实现了 IEnumerable
,从而产生了非常干净的 DSL,类似于上面看到的语法.
This interface has a property named Query
of type IQueries
. For each entity the business layer needs to query there is a property in IQueries
. Each property has its own interface that defines the queries for the entity. Each query interface implements the generic IQuery<T>
interface which in turn implementes IEnumerable<T>
, leading to the very clean DSL like syntax seen above.
一些代码:
public interface IRepository
{
IQueries Queries { get; }
}
public interface IQueries
{
IInvoiceQuery Invoices { get; }
IUserQuery Users { get; }
}
public interface IQuery<T> : IEnumerable<T>
{
T Single();
T SingleOrDefault();
T First();
T FirstOrDefault();
}
public interface IInvoiceQuery : IQuery<Invoice>
{
IInvoiceQuery Started();
IInvoiceQuery Unfinished();
IInvoiceQuery WithoutError();
Invoice ByInvoiceNumber(string invoiceNumber);
}
这种流畅的查询语法允许业务层组合提供的查询,以充分利用底层 ORM 的功能,让数据库尽可能多地过滤.
This fluent querying syntax allows the business layer to combine the supplied queries to take full advantage of the underlying ORM's capabilities to let the database filter as much as possible.
NHibernate 的实现看起来像这样:
The implementation for NHibernate would look something like this:
public class NHibernateInvoiceQuery : IInvoiceQuery
{
IQueryable<Invoice> _query;
public NHibernateInvoiceQuery(ISession session)
{
_query = session.Query<Invoice>();
}
public IInvoiceQuery Started()
{
_query = _query.Where(x => x.IsStarted);
return this;
}
public IInvoiceQuery WithoutError()
{
_query = _query.Where(x => !x.HasError);
return this;
}
public Invoice ByInvoiceNumber(string invoiceNumber)
{
return _query.SingleOrDefault(x => x.InvoiceNumber == invoiceNumber);
}
public IEnumerator<Invoice> GetEnumerator()
{
return _query.GetEnumerator();
}
// ...
}
在我的实际实现中,我将大部分基础结构代码提取到了一个基类中,以便为新实体创建新的查询对象变得非常容易.向现有实体添加新查询也非常简单.
In my real implementation I extracted most of the infrastructure code into a base class, so that it becomes very easy to create a new query object for a new entity. Adding a new query to an existing entity is also very simple.
这样做的好处是业务层完全没有查询逻辑,因此可以轻松切换数据存储.或者可以使用标准 API 实现其中一个查询或从另一个数据源获取数据.业务层可能会忽略这些细节.
The nice thing about this is that the business layer is completely free of querying logic and thus the data store can be switched easily. Or one could implement one of the queries using the criteria API or get the data from another data source. The business layer would be oblivious to these details.
这篇关于使用扩展方法中定义的查询进行单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!