最近有一个项目采用了EntityFramework,对于基本的增删改查操作,我们采用了传统的仓储模式(IRepository),但对于项目中的仓储接口的定义及实现上我认为存在部分缺陷。这个创建模式是以前同事编写好的,然后我们在新项目中利用它。
分页查询接口。
接口定义:
IEnumerable<T> QueryByPage(Func<T,
bool> FunWhere, Func<T,
string> FunOrder,
int PageSize,
int PageIndex,
out
int recordsCount);
接口实现:
public IEnumerable<T> QueryByPage(Func<T,
bool> FunWhere, Func<T,
string> FunOrder,
int PageSize,
int PageIndex,
out
int recordsCount)
{
recordsCount = context.Set<T>().Where(FunWhere).OrderByDescending(FunOrder).Count();
return context.Set<T>().Where(FunWhere).OrderByDescending(FunOrder).Select(t => t).Skip((PageIndex -
1) * PageSize).Take(PageSize);
}
缺陷一:对于仓储接口,集合返回IEnumerable。
如果返回的是IEnumerable,那么实际上,系统会将表中的所有数据加载到内存中,然后再进行条件过滤,排序,再分页。如果表记录稍微多一点的话,性能可想而知。此种情况下应该推荐返回IQueryable,它才是真正适合和数据库打交道的对象。在客户端应用程序没有访问实际对象值之前,比如ToList()操作,它只是一个编译过程,根据用户传入的参数构建查询计划最终生成用于查询所用的SQLScript脚本。这种方式才是真正意义上的按需所取。
下面我们来定义一个新接口:
IQueryable<T> Query(Expression<Func<T,
bool>> filter);
注意这里是一个开放性特别大的查询接口,如果说不要轻易为客户端开放IQueryable这也没也问题,而且也不推荐将仓储接口直接开放给客户端应用程序,应该在仓储接口上为每个特定的应用系统提供全新的接口,比如可以这样:
public
interface Iaspnet_UsersRepository
{
List<aspnet_UsersModel> QueryByPage(Expression<Func<T,
bool>> filter, Expression<Func<T,
string>> FunOrder,
int PageSize,
int PageIndex,
out
int recordsCount);
}
这里为了演示方便,就直接调用仓储接口做测试。
IEnumerable调用代码:
var list = service.QueryByPage(p => p.UserId != Guid.Empty,p=>p.UserName,
1,
1,
out recordsCount).ToList();
IEnumerable情况下生成的脚本:
SELECT
[
Extent1
].
[
UserId
]
AS
[
UserId
],
[
Extent1
].
[
UserName
]
AS
[
UserName
],
[
Extent1
].
[
LoweredUserName
]
AS
[
LoweredUserName
],
[
Extent1
].
[
MobileAlias
]
AS
[
MobileAlias
],
[
Extent1
].
[
IsAnonymous
]
AS
[
IsAnonymous
],
[
Extent1
].
[
LastActivityDate
]
AS
[
LastActivityDate
]
FROM
[
dbo
].
[
aspnet_Users
]
AS
[
Extent1
]
IQueryable调用
var list
= service.Query(p
=>p.UserId
!=Guid.Empty).OrderBy(p
=>p.UserName).Take(
1).Skip(
1).ToList();
IQueryable情况下生成的脚本
View Code
小结:
只有返回IQueryable,才会实现按需所取。尽管这个方法在最上层的服务层没有进行封装,理论上客户端应用程序无法访问到此方法,但既然提供了,而且对外是Public,那么在一定程度上就会对程序员造成误导。
缺陷二:不太实用的接口定义
我们来看一下这个接口定义
IEnumerable<T> QueryByPage(List<KeyValuePair<
string,
object>> queryWhere, List<KeyValuePair<
string,
int>> funOrder,
int PageSize,
int PageIndex,
out
int recordsCount)
再看下它的实现,可以看出这个方法实现了按需所取,但这是多么的不协调啊(上面那个分页方法就不是按需所取)。就像有人做事一样,50%的工作可以打100分,其余的50%只能打60分,而且这40分很容易得到。
View Code
查询条件难以理解
List<KeyValuePair<string, object>>,用户根本不知道需要传递什么参数,提供的接口参数一定要让调用者容易理解。
我理解的仓储接口
如果还有哪些不够的,可以根据情况再决定增加哪些功能。创建接口不推荐直接开放给客户端,应该在此基础上重新为每个应用定义接口。
public
interface IMyRepository<T>
where T :
class,
new()
{
T Create();
T Update(T entity);
T Insert(T entity);
void Delete(T entity);
List<T> FindAll();
IQueryable<T> Query(Expression<Func<T,
bool>> filter);
}
前一阵我总结过一篇关于IRepository的文章:我所理解的IRepository,当时只是纯属从单一接口上来做分享,但毕竟接口并不是单独存在的,它需要很好的融入到项目中去,这就是一个员工需要融入到团队中一样。而要想让IRepositoyr在项目中发挥得当,最好根据自己项目的需求来做相应调整,毕竟适应自己公司大环境的解决方案才是最有可能成功的应用,比如我们是EntityFrame+MVC。
第一:在原来接口的基础上做些补充。
1:增加了一个支持分页的方法。这个其实是非常见的方法,只需要在原来查询接口中做些封装即可。接口定义如下:
IList<T> QueryByPage<TKey>(Expression<Func<T,
bool>> filter, Expression<Func<T, TKey>>
orderby,
int OrderType,
int PageSize,
int PageIndex,
out
int recordsCount);
客户可以这样调用:
var list=service.QueryByPage(p=>p.UserName=
"
张三
"&&p.RecordStatus==
0,p=>p.CreatedOn,
1,PageSize,PageIndex,
out recordCount);
注意:这个分页方法的参数,最好传表达式树,让其动态构建SQL,如果传Func这种泛型委托的话,一定要在调用Context时将其转换成表达式树,否则就无法在服务器端完成分页了。
2:为Update方法增加了一个重载方法,定义如下:
T Update(T entity, Expression<Func<T,
bool>> filter);
其实T Update(T entity)这个接口也能完成更新操作,但需要传入的实体参数自身包含主键相关查询字段信息,否则无法找到需要更新的记录是哪条。之所以增加一个重载方法,我个人认为新增加的方法更加符合平时的操作习惯,跟SQL的更新语法是比较符合的,第一步是确认需要更新的字段,第二步是选择更新的记录是哪条。调用示例如下:
Update(
new Sutduent{Name=
"
张三
"},p=>p.Id=
1);
第二:对IRepository的应用注入一些自定义的事件。
在应用EntityFramword对其表进行CRUD时,在某些环境下,表中的一部分字段属于系统字段(比如记录的创建日期CreatedOn,修改日期ModifiedOn),为了简化操作,我们可以在操作数据时将这些系统字段由人工处理变成自动处理,即客户端调用IRepository时不传递这些系统字段的信息,只传和业务逻辑紧密相关的字段(比如学生表中的姓名,学号,地址等),然后IRepository会自动的将这些系统字段的值填充上。这样从某种程度上可以使我们的领域模型变得更加简单,也可以减轻程序员对领域模型的维护成本。
以前听说过一句话:代码的行数和出错误的概率是成正比的,这里同样的道理,领域模型的字段数量与出错误的概率是成正比的。
解决方案:在Context上订阅自定义事件,这样在正式提交数据库之前会先执行我们定义的自定义事件之后再提交数据库,所以我们可以利用EntityObject提供的MarkCreated方法来自动的将我们定义的一些系统字段给加上去。
注意:这里之所以可以这样做,也需要取决于我们的数据库设计,数据表都遵守一定的约束,比如所有表都会具备相同的系统字段。只有这样我们才能放开手脚使用下面的解决方案。
context.SavingChanges +=
new EventHandler(ContextPersistent);
public
void ContextPersistent(
object sender, EventArgs e)
{
ObjectContext oc = sender
as ObjectContext;
ObjectStateManager osm = oc.ObjectStateManager;
IEnumerable<ObjectStateEntry> newEntities = osm.GetObjectStateEntries(EntityState.Added);
foreach (ObjectStateEntry ose
in newEntities)
{
EntityObject newAdded = (EntityObject)context.GetObjectByKey(ose.EntityKey);
newAdded.MarkCreated(CurrentUser);
}
}
public
static
void MarkCreated(
this EntityObject eo, Guid user)
{
Type type = eo.GetType();
PropertyInfo pInfo = type.GetProperty(
"
CreatedOn
");
if (pInfo !=
null)
pInfo.SetValue(eo, DateTime.UtcNow,
null);
PropertyInfo mInfo = type.GetProperty(
"
ModifiedOn
");
if (mInfo !=
null)
mInfo.SetValue(eo, DateTime.UtcNow,
null);
}
第三:如何解决IRepository,数据库模型和ViewModel之间的关系。
我们的项目是asp.net mvc项目,所以我们有ViewModel的概念,而我们采用了数据库优先的方式调用EntityFramwork,在添加完edmx后,我们也就有了数据库对象,这些数据库对象都是系统自动生成的,当然和数据库中的字段是一对一的关系,但ViewModel并不一定和数据表字段是一对一的关系。
问题:既然我们定义了IRepository接口,那么意味着MVC在调用数据时也会遵守此接口的契约,问题在于,IRepository在处理数据时,参数是数据库对象,而在MVC调用时我们是不太可能直接传递数据库对象的,(数据库对象属于DataAccess层,MVC属于UI层,this.entitySet.FirstOrDefault(filter);这其中的filter表达式中的实体需要是数据库对象。)它只有ViewModel的概念,所以我们需要将Query(Expression<Func<ViewModel, bool>> filter);转换成Query(Expression<Func<DataBaseModel, bool>> filter);
解决方案:利用自定义表达式来完成转达式树之间的转换,原理就是将ViewModel佣有的属性在DataBaseModel中遍历,如果有相同属性,就赋值。
比如:ViewModel为StudentModel,它有一个Name属性,DataBaseModel为Student,它也有一个Name属性,目标就是将p=>p.Name=="张三"(p的类型是StudentModel)转换成p=>p.Name=="张三"(p的类型是Student).
public
class ExpressionConverter<TToB>
{
//
Methods
public
static Expression<Func<TToB, TR>> Convert<TFrom, TR>(Expression<Func<TFrom, TR>> e)
{
ParameterExpression oldParameter = e.Parameters[
0];
ParameterExpression newParameter = Expression.Parameter(
typeof(TToB), oldParameter.Name);
ConversionVisitor<TToB> visitor =
new ConversionVisitor<TToB>(newParameter, oldParameter);
return Expression.Lambda<Func<TToB, TR>>(visitor.Visit(e.Body),
new ParameterExpression[] { newParameter });
}
//
Nested Types
private
class ConversionVisitor : ExpressionVisitor
{
//
Fields
private
readonly ParameterExpression newParameter;
private
readonly ParameterExpression oldParameter;
//
Methods
public ConversionVisitor(ParameterExpression newParameter, ParameterExpression oldParameter)
{
this.newParameter = newParameter;
this.oldParameter = oldParameter;
}
protected
override Expression VisitMember(MemberExpression node)
{
if (node.Expression !=
this.oldParameter)
{
return
base.VisitMember(node);
}
Expression expression =
this.Visit(node.Expression);
MemberInfo member =
this.newParameter.Type.GetMember(node.Member.Name).First<MemberInfo>();
return Expression.MakeMemberAccess(expression, member);
}
protected
override Expression VisitParameter(ParameterExpression node)
{
return
this.newParameter;
}
}
}
感叹表达树是多么的强大,几十行代码就能完成一些意想不到的效果,总之以上是我从自己项目出发,对于IRepository应用上的一些理解。