代码改变世界

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