Entity Framework实用心得
2009-06-29 17:29 三把刷子 阅读(2587) 评论(2) 编辑 收藏 举报前一个阶段,因为项目需要,学习了一下Entity Framework,总的来说,这个产品还不是很成熟,微软在推这个产品的时候操之过急,里面有很多比较诡异的现象。另外,由于Entity Framework是使用一种非常依赖状态的方式去工作,那么必然和网页的无连接的工作方式会产生冲突。
假设,我们拥有这样3个表:User,UserInRole,Role
字段如下:
User - ID(PK), UserName(U), EMail, Password
UserInRole - UserID(PK,FK), RoleID(PK,FK)
Role - ID(PK), Name
那么来看看Entity Framework下,如果已知pk,怎么去update一些字段:
using (var context = new SomeContext())
{
User user = context.User.First(u => u.ID == 1);
user.Email = "user@test.com";
context.SaveChanges();
}
可以发现为了Update一个字段,不得不先去数据库取会这个实体,这个可以说是为了处理并发的安全,但是,很多时候,我们并不需要这种并发的安全,毕竟一次Update语句被拆成了一句Select再Update总归对性能会有影响。
那么能不能让Entity Framework不去Select而直接去Update哪?当然是可能的,可以把以前Select出来的实体Detach了以后保存下来,然后在需要的时候再Attach回去,但是这样做有个问题,网页是无连接的,如何保存这些Detach下来的实体哪?用Session?Cache?或者ViewState?Session和Cache会大量占用服务器内存(除非保存到Sql,但是这样做就没有意义了),ViewState则会迅速把页面的体积变大,同样导致性能变差。最后,也可以用存储过程去做,但是,这样就有无数的存储过程需要去完成。
所以,不得不另辟蹊径,找一条新的路线。如果一个User对象只保留一个ID到页面显然是可以接受的,而且因为ID是PK,所以数据库也能唯一确定记录。剩下来的就是如何让Entity Framework按照我们下腰的方式去工作了。
经过摸索之后,找到了一种方法,利用一个扩展方法就能简单的完成将自己随便new出来的对象Attach上去,并且Entity Framework会认为这个是数据库中已经存在的记录:
Code
public static class ObjectContextExtension
{
public static T AttachExistedEntity<T>(this ObjectContext context, T entity)
where T : EntityObject
{
EdmEntityTypeAttribute entityTypeAttr = (EdmEntityTypeAttribute)Attribute.GetCustomAttribute(typeof(T), typeof(EdmEntityTypeAttribute), false);
if (entityTypeAttr == null)
throw new NotSupportedException("T is not an entity.");
string entityFullname = context.DefaultContainerName + "." + entityTypeAttr.Name;
entity.EntityKey = new System.Data.EntityKey(entityFullname,
from p in typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
where p.GetGetMethod(false) != null
let attribute = (EdmScalarPropertyAttribute)Attribute.GetCustomAttribute(p, typeof(EdmScalarPropertyAttribute))
where attribute != null && attribute.EntityKeyProperty
select new KeyValuePair<string, object>(p.Name, p.GetValue(entity, null)));
context.Attach(entity);
context.ApplyPropertyChanges(entityTypeAttr.Name, entity);
return entity;
}
}
来看一下使用这个方法后,如何去更新数据库:
using (var context = new SomeContext())
{
User user = context.AttachExistedEntity(new User { ID = 1 });
user.Email = "user@test.com";
context.SaveChanges();
}
可以发现,这样就可以避免先从数据库中拿实体的代价,Entity Framework直接就去Update对应的语句了。
同样,对于插入,Entity Framework也有比较麻烦的地方,例如,现在需要新建一个用户,并且它需要有a和b两个角色,在Entity Framework下,我们需要这样写:
Code
using (var context = new SomeContext())
{
User user = new User()
{
UserName = "UserName",
Password = "abcd",
Email = "user@test.com",
};
context.AddToUser(user);
context.SaveChanges();
User userWithID = context.User.First(u => u.UserName = "UserName");
userWithID.Role.Add(context.Role.First(r => r.ID = 1));
userWithID.Role.Add(context.Role.First(r => r.ID = 2));
context.SaveChanges();
}
可以看到,需要先插入用户,然后再从数据库里面读取刚才的用户以及a和b两个角色,才能正确的完成,中间已经和数据库打了好几次交道了,性能自然就比较差。
为了完成这个操作,还需要再次增强我们的扩展方法:
Code
public static class ObjectContextExtension
{
public static T AttachExistedEntity<T>(this ObjectContext context, T entity)
where T : EntityObject
{
EdmEntityTypeAttribute entityTypeAttr = (EdmEntityTypeAttribute)Attribute.GetCustomAttribute(typeof(T), typeof(EdmEntityTypeAttribute), false);
if (entityTypeAttr == null)
throw new NotSupportedException("T is not an entity.");
string entityFullname = context.DefaultContainerName + "." + entityTypeAttr.Name;
entity.EntityKey = new System.Data.EntityKey(entityFullname,
from p in typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
where p.GetGetMethod(false) != null
let attribute = (EdmScalarPropertyAttribute)Attribute.GetCustomAttribute(p, typeof(EdmScalarPropertyAttribute))
where attribute != null && attribute.EntityKeyProperty
select new KeyValuePair<string, object>(p.Name, p.GetValue(entity, null)));
context.Attach(entity);
context.ApplyPropertyChanges(entityTypeAttr.Name, entity);
return entity;
}
public static void ApplyPropertyChanges<T>(this ObjectContext context, T entity)
where T : EntityObject
{
EdmEntityTypeAttribute entityTypeAttr = (EdmEntityTypeAttribute)Attribute.GetCustomAttribute(typeof(T), typeof(EdmEntityTypeAttribute), false);
if (entityTypeAttr == null)
throw new NotSupportedException("T is not an entity.");
context.ApplyPropertyChanges(entityTypeAttr.Name, entity);
}
public static EntityCollection<TElement> CreateCollection<T, TElement>(this T entity, Expression<Func<T, EntityCollection<TElement>>> expr, params TElement[] items)
where T : EntityObject
where TElement : EntityObject
{
if (expr.Body.NodeType != ExpressionType.MemberAccess)
throw new ArgumentException("Expression is not correct.", "expr");
var member = ((MemberExpression)expr.Body).Member;
PropertyInfo pi = member as PropertyInfo;
if (pi == null)
throw new ArgumentException("Expression is not correct.", "expr");
EdmRelationshipNavigationPropertyAttribute attribute = (EdmRelationshipNavigationPropertyAttribute)Attribute.GetCustomAttribute(pi, typeof(EdmRelationshipNavigationPropertyAttribute));
EntityCollection<TElement> result = new EntityCollection<TElement>();
RelationshipManager rm = RelationshipManager.Create(entity);
rm.InitializeRelatedCollection(attribute.RelationshipName, attribute.TargetRoleName, result);
foreach (var item in items)
result.Add(item);
return result;
}
}
现在来看看,如何不查询数据库,直接完成这个任务:
Code
using (var context = new SomeContext())
{
Role role1 = context.AttachExistedEntity(new Role { ID = 1 });
Role role2 = context.AttachExistedEntity(new Role { ID = 2 });
User user = new User()
{
UserName = "UserName",
Password = "abcd",
Email = "user@test.com",
};
// create a user-role related collection.
user.CreateCollection(u => u.Role, role1, role2);
context.AddToUser(user);
context.SaveChanges();
}
是不是简单多了。
然后是修改用户的角色,这个绝对是Entity Framework最雷的杰作:
Code
using (var context = new SomeContext())
{
context.User.First(u => u.ID == 1).Role.Remove(context.Role.First(r => r.ID == 1));
context.SaveChanges();
}
上面的代码根本就不工作(并且还不报错),来看看下面这个:
Code
using (var context = new SomeContext())
{
foreach (var role in context.User.Include("Role").First(u => u.ID == 1))
Console.WriteLine(role.ID);
context.User.First(u => u.ID == 1).Role.Remove(context.Role.First(r => r.ID == 1));
context.SaveChanges();
}
其实更新部分一点都没变,就是上面多了个查询,居然就工作了,当时就被雷傻了。。。
最后想通了,这个雷死人不偿命的结果是由于Entity Framework本身的缓存机制导致的,为了让更新部分在无论什么情况下都能工作,不得不修改成:
using (var context = new SomeContext())
{
context.User.Include("Role").First(u => u.ID == 1).Role.Remove(context.Role.First(r => r.ID == 1));
context.SaveChanges();
}
总的来说,太雷了。。。
好吧,忘掉这个雷人的方式,因为为了删除一个用户的角色,居然还要到数据库去查用户和角色,改造一下:
Code
using (var context = new SomeContext())
{
Role role1 = context.AttachExistedEntity(new Role { ID = 1 });
User user = context.AttachExistedEntity(new User { ID = 1 });
user.Role.Attach(role1);
user.Role.Remove(role1);
context.SaveChanges();
}
现在看起来舒服多了,唯一有点遗憾的是,必须要Attach再Remove,否则也会像前面的那样运行没问题,当时数据库又不更新。。。
最后,就是如何删除用户了,标准的写法就跳过了,直接看怎么做了(前提当然是已经知道这个用户的ID和它所在的所有角色的ID):
Code
using (var context = new SomeContext())
{
Role role1 = context.AttachExistedEntity(new Role { ID = 1 });
Role role2 = context.AttachExistedEntity(new Role { ID = 2 });
User user = context.AttachExistedEntity(new User { ID = 1 });
user.Role.Attach(role1);
user.Role.Attach(role2);
user.Role.Remove(role1);
user.Role.Remove(role2);
context.DeleteObject(user);
context.SaveChanges();
}